Merge branch 'main' into lstein/enhance/configuration

This commit is contained in:
Lincoln Stein
2023-04-30 23:03:08 -04:00
committed by GitHub
84 changed files with 2896 additions and 2476 deletions

View File

@@ -65,6 +65,7 @@
"@emotion/styled": "^11.10.6",
"@fontsource/inter": "^4.5.15",
"@reduxjs/toolkit": "^1.9.5",
"@roarr/browser-log-writer": "^1.1.5",
"chakra-ui-contextmenu": "^1.0.5",
"dateformat": "^5.0.3",
"formik": "^2.2.9",
@@ -87,23 +88,28 @@
"react-konva": "^18.2.7",
"react-konva-utils": "^1.0.4",
"react-redux": "^8.0.5",
"react-rnd": "^10.4.1",
"react-transition-group": "^4.4.5",
"react-use": "^17.4.0",
"react-zoom-pan-pinch": "^3.0.7",
"reactflow": "^11.7.0",
"redux-deep-persist": "^1.0.7",
"redux-dynamic-middlewares": "^2.2.0",
"redux-persist": "^6.0.0",
"roarr": "^7.15.0",
"serialize-error": "^11.0.0",
"socket.io-client": "^4.6.0",
"use-image": "^1.1.0",
"uuid": "^9.0.0"
},
"peerDependencies": {
"@chakra-ui/cli": "^2.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-toolbelt": "^9.6.0",
"@chakra-ui/cli": "^2.4.0"
"ts-toolbelt": "^9.6.0"
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.0",
"@types/dateformat": "^5.0.0",
"@types/lodash-es": "^4.14.194",
"@types/node": "^18.16.2",

View File

@@ -527,10 +527,15 @@
"useCanvasBeta": "Use Canvas Beta Layout",
"enableImageDebugging": "Enable Image Debugging",
"useSlidersForAll": "Use Sliders For All Options",
"autoShowProgress": "Auto Show Progress Images",
"resetWebUI": "Reset Web UI",
"resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.",
"resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.",
"resetComplete": "Web UI has been reset. Refresh the page to reload."
"resetComplete": "Web UI has been reset. Refresh the page to reload.",
"consoleLogLevel": "Log Level",
"shouldLogToConsole": "Console Logging",
"developer": "Developer",
"general": "General"
},
"toast": {
"serverError": "Server Error",
@@ -641,5 +646,9 @@
"betaDarkenOutside": "Darken Outside",
"betaLimitToBox": "Limit To Box",
"betaPreserveMasked": "Preserve Masked"
},
"ui": {
"showProgressImages": "Show Progress Images",
"hideProgressImages": "Hide Progress Images"
}
}

View File

@@ -1,5 +1,4 @@
import ImageUploader from 'common/components/ImageUploader';
import Console from 'features/system/components/Console';
import ProgressBar from 'features/system/components/ProgressBar';
import SiteHeader from 'features/system/components/SiteHeader';
import InvokeTabs from 'features/ui/components/InvokeTabs';
@@ -27,6 +26,8 @@ import { PartialAppConfig } from 'app/types/invokeai';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useLogger } from 'app/logging/useLogger';
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
const DEFAULT_CONFIG = {};
@@ -37,6 +38,7 @@ interface Props extends PropsWithChildren {
const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
useToastWatcher();
useGlobalHotkeys();
const log = useLogger();
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
@@ -50,9 +52,9 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
const dispatch = useAppDispatch();
useEffect(() => {
console.log('Received config: ', config);
log.info({ namespace: 'App', data: config }, 'Received config');
dispatch(configChanged(config));
}, [dispatch, config]);
}, [dispatch, config, log]);
useEffect(() => {
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
@@ -63,7 +65,7 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
}, []);
return (
<Grid w="100vw" h="100vh" position="relative">
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
{isLightboxEnabled && <Lightbox />}
<ImageUploader>
<ProgressBar />
@@ -119,9 +121,7 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => {
<Portal>
<FloatingGalleryButton />
</Portal>
<Portal>
<Console />
</Portal>
<ProgressImagePreview />
</Grid>
);
};

View File

@@ -1,7 +1,7 @@
import React, { lazy, memo, PropsWithChildren, useEffect } from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { buildMiddleware, store } from 'app/store/store';
import { store } from 'app/store/store';
import { persistor } from '../store/persistor';
import { OpenAPI } from 'services/api';
import '@fontsource/inter/100.css';
@@ -19,6 +19,7 @@ import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { PartialAppConfig } from 'app/types/invokeai';
import '../../i18n';
import { socketMiddleware } from 'services/events/middleware';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@@ -50,7 +51,7 @@ const InvokeAIUI = ({ apiUrl, token, config, children }: Props) => {
// the `apiUrl`/`token` dynamically.
// rebuild socket middleware with token and apiUrl
addMiddleware(buildMiddleware());
addMiddleware(socketMiddleware());
}, [apiUrl, token]);
return (

View File

@@ -1,7 +1,5 @@
// TODO: use Enums?
import { InProgressImageType } from 'features/system/store/systemSlice';
export const DIFFUSERS_SCHEDULERS: Array<string> = [
'ddim',
'plms',
@@ -33,17 +31,8 @@ export const UPSCALING_LEVELS: Array<{ key: string; value: number }> = [
export const NUMPY_RAND_MIN = 0;
export const NUMPY_RAND_MAX = 4294967295;
export const NUMPY_RAND_MAX = 2147483647;
export const FACETOOL_TYPES = ['gfpgan', 'codeformer'] as const;
export const IN_PROGRESS_IMAGE_TYPES: Array<{
key: string;
value: InProgressImageType;
}> = [
{ key: 'None', value: 'none' },
{ key: 'Fast', value: 'latents' },
{ key: 'Accurate', value: 'full-res' },
];
export const NODE_MIN_WIDTH = 250;

View File

@@ -0,0 +1,94 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash-es';
import { useEffect } from 'react';
import { LogLevelName, ROARR, Roarr } from 'roarr';
import { createLogWriter } from '@roarr/browser-log-writer';
// Base logging context includes only the package name
const baseContext = { package: '@invoke-ai/invoke-ai-ui' };
// Create browser log writer
ROARR.write = createLogWriter();
// Module-scoped logger - can be imported and used anywhere
export let log = Roarr.child(baseContext);
// Translate human-readable log levels to numbers, used for log filtering
export const LOG_LEVEL_MAP: Record<LogLevelName, number> = {
trace: 10,
debug: 20,
info: 30,
warn: 40,
error: 50,
fatal: 60,
};
export const VALID_LOG_LEVELS = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
] as const;
export type InvokeLogLevel = (typeof VALID_LOG_LEVELS)[number];
const selector = createSelector(
systemSelector,
(system) => {
const { app_version, consoleLogLevel, shouldLogToConsole } = system;
return {
version: app_version,
consoleLogLevel,
shouldLogToConsole,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const useLogger = () => {
const { version, consoleLogLevel, shouldLogToConsole } =
useAppSelector(selector);
// The provided Roarr browser log writer uses localStorage to config logging to console
useEffect(() => {
if (shouldLogToConsole) {
// Enable console log output
localStorage.setItem('ROARR_LOG', 'true');
// Use a filter to show only logs of the given level
localStorage.setItem(
'ROARR_FILTER',
`context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`
);
} else {
// Disable console log output
localStorage.setItem('ROARR_LOG', 'false');
}
ROARR.write = createLogWriter();
}, [consoleLogLevel, shouldLogToConsole]);
// Update the module-scoped logger context as needed
useEffect(() => {
const newContext: Record<string, any> = {
...baseContext,
};
if (version) {
newContext.version = version;
}
log = Roarr.child(newContext);
}, [version]);
// Use the logger within components - no different than just importing it directly
return log;
};

View File

@@ -1,65 +1,67 @@
import { createAction } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import { GalleryCategory } from 'features/gallery/store/gallerySlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
// import { createAction } from '@reduxjs/toolkit';
// import * as InvokeAI from 'app/types/invokeai';
// import { GalleryCategory } from 'features/gallery/store/gallerySlice';
// import { InvokeTabName } from 'features/ui/store/tabMap';
/**
* We can't use redux-toolkit's createSlice() to make these actions,
* because they have no associated reducer. They only exist to dispatch
* requests to the server via socketio. These actions will be handled
* by the middleware.
*/
// /**
// * We can't use redux-toolkit's createSlice() to make these actions,
// * because they have no associated reducer. They only exist to dispatch
// * requests to the server via socketio. These actions will be handled
// * by the middleware.
// */
export const generateImage = createAction<InvokeTabName>(
'socketio/generateImage'
);
export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
export const runFacetool = createAction<InvokeAI._Image>(
'socketio/runFacetool'
);
export const deleteImage = createAction<InvokeAI._Image>(
'socketio/deleteImage'
);
export const requestImages = createAction<GalleryCategory>(
'socketio/requestImages'
);
export const requestNewImages = createAction<GalleryCategory>(
'socketio/requestNewImages'
);
export const cancelProcessing = createAction<undefined>(
'socketio/cancelProcessing'
);
// export const generateImage = createAction<InvokeTabName>(
// 'socketio/generateImage'
// );
// export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
// export const runFacetool = createAction<InvokeAI._Image>(
// 'socketio/runFacetool'
// );
// export const deleteImage = createAction<InvokeAI._Image>(
// 'socketio/deleteImage'
// );
// export const requestImages = createAction<GalleryCategory>(
// 'socketio/requestImages'
// );
// export const requestNewImages = createAction<GalleryCategory>(
// 'socketio/requestNewImages'
// );
// export const cancelProcessing = createAction<undefined>(
// 'socketio/cancelProcessing'
// );
export const requestSystemConfig = createAction<undefined>(
'socketio/requestSystemConfig'
);
// export const requestSystemConfig = createAction<undefined>(
// 'socketio/requestSystemConfig'
// );
export const searchForModels = createAction<string>('socketio/searchForModels');
// export const searchForModels = createAction<string>('socketio/searchForModels');
export const addNewModel = createAction<
InvokeAI.InvokeModelConfigProps | InvokeAI.InvokeDiffusersModelConfigProps
>('socketio/addNewModel');
// export const addNewModel = createAction<
// InvokeAI.InvokeModelConfigProps | InvokeAI.InvokeDiffusersModelConfigProps
// >('socketio/addNewModel');
export const deleteModel = createAction<string>('socketio/deleteModel');
// export const deleteModel = createAction<string>('socketio/deleteModel');
export const convertToDiffusers =
createAction<InvokeAI.InvokeModelConversionProps>(
'socketio/convertToDiffusers'
);
// export const convertToDiffusers =
// createAction<InvokeAI.InvokeModelConversionProps>(
// 'socketio/convertToDiffusers'
// );
export const mergeDiffusersModels =
createAction<InvokeAI.InvokeModelMergingProps>(
'socketio/mergeDiffusersModels'
);
// export const mergeDiffusersModels =
// createAction<InvokeAI.InvokeModelMergingProps>(
// 'socketio/mergeDiffusersModels'
// );
export const requestModelChange = createAction<string>(
'socketio/requestModelChange'
);
// export const requestModelChange = createAction<string>(
// 'socketio/requestModelChange'
// );
export const saveStagingAreaImageToGallery = createAction<string>(
'socketio/saveStagingAreaImageToGallery'
);
// export const saveStagingAreaImageToGallery = createAction<string>(
// 'socketio/saveStagingAreaImageToGallery'
// );
export const emptyTempFolder = createAction<undefined>(
'socketio/requestEmptyTempFolder'
);
// export const emptyTempFolder = createAction<undefined>(
// 'socketio/requestEmptyTempFolder'
// );
export default {};

View File

@@ -1,208 +1,209 @@
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import type { RootState } from 'app/store/store';
import {
frontendToBackendParameters,
FrontendToBackendParametersConfig,
} from 'common/util/parameterTranslation';
import dateFormat from 'dateformat';
import {
GalleryCategory,
GalleryState,
removeImage,
} from 'features/gallery/store/gallerySlice';
import {
addLogEntry,
generationRequested,
modelChangeRequested,
modelConvertRequested,
modelMergingRequested,
setIsProcessing,
} from 'features/system/store/systemSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { Socket } from 'socket.io-client';
// import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
// import * as InvokeAI from 'app/types/invokeai';
// import type { RootState } from 'app/store/store';
// import {
// frontendToBackendParameters,
// FrontendToBackendParametersConfig,
// } from 'common/util/parameterTranslation';
// import dateFormat from 'dateformat';
// import {
// GalleryCategory,
// GalleryState,
// removeImage,
// } from 'features/gallery/store/gallerySlice';
// import {
// generationRequested,
// modelChangeRequested,
// modelConvertRequested,
// modelMergingRequested,
// setIsProcessing,
// } from 'features/system/store/systemSlice';
// import { InvokeTabName } from 'features/ui/store/tabMap';
// import { Socket } from 'socket.io-client';
/**
* Returns an object containing all functions which use `socketio.emit()`.
* i.e. those which make server requests.
*/
const makeSocketIOEmitters = (
store: MiddlewareAPI<Dispatch<AnyAction>, RootState>,
socketio: Socket
) => {
// We need to dispatch actions to redux and get pieces of state from the store.
const { dispatch, getState } = store;
// /**
// * Returns an object containing all functions which use `socketio.emit()`.
// * i.e. those which make server requests.
// */
// const makeSocketIOEmitters = (
// store: MiddlewareAPI<Dispatch<AnyAction>, RootState>,
// socketio: Socket
// ) => {
// // We need to dispatch actions to redux and get pieces of state from the store.
// const { dispatch, getState } = store;
return {
emitGenerateImage: (generationMode: InvokeTabName) => {
dispatch(setIsProcessing(true));
// return {
// emitGenerateImage: (generationMode: InvokeTabName) => {
// dispatch(setIsProcessing(true));
const state: RootState = getState();
// const state: RootState = getState();
const {
generation: generationState,
postprocessing: postprocessingState,
system: systemState,
canvas: canvasState,
} = state;
// const {
// generation: generationState,
// postprocessing: postprocessingState,
// system: systemState,
// canvas: canvasState,
// } = state;
const frontendToBackendParametersConfig: FrontendToBackendParametersConfig =
{
generationMode,
generationState,
postprocessingState,
canvasState,
systemState,
};
// const frontendToBackendParametersConfig: FrontendToBackendParametersConfig =
// {
// generationMode,
// generationState,
// postprocessingState,
// canvasState,
// systemState,
// };
dispatch(generationRequested());
// dispatch(generationRequested());
const { generationParameters, esrganParameters, facetoolParameters } =
frontendToBackendParameters(frontendToBackendParametersConfig);
// const { generationParameters, esrganParameters, facetoolParameters } =
// frontendToBackendParameters(frontendToBackendParametersConfig);
socketio.emit(
'generateImage',
generationParameters,
esrganParameters,
facetoolParameters
);
// socketio.emit(
// 'generateImage',
// generationParameters,
// esrganParameters,
// facetoolParameters
// );
// we need to truncate the init_mask base64 else it takes up the whole log
// TODO: handle maintaining masks for reproducibility in future
if (generationParameters.init_mask) {
generationParameters.init_mask = generationParameters.init_mask
.substr(0, 64)
.concat('...');
}
if (generationParameters.init_img) {
generationParameters.init_img = generationParameters.init_img
.substr(0, 64)
.concat('...');
}
// // we need to truncate the init_mask base64 else it takes up the whole log
// // TODO: handle maintaining masks for reproducibility in future
// if (generationParameters.init_mask) {
// generationParameters.init_mask = generationParameters.init_mask
// .substr(0, 64)
// .concat('...');
// }
// if (generationParameters.init_img) {
// generationParameters.init_img = generationParameters.init_img
// .substr(0, 64)
// .concat('...');
// }
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Image generation requested: ${JSON.stringify({
...generationParameters,
...esrganParameters,
...facetoolParameters,
})}`,
})
);
},
emitRunESRGAN: (imageToProcess: InvokeAI._Image) => {
dispatch(setIsProcessing(true));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Image generation requested: ${JSON.stringify({
// ...generationParameters,
// ...esrganParameters,
// ...facetoolParameters,
// })}`,
// })
// );
// },
// emitRunESRGAN: (imageToProcess: InvokeAI._Image) => {
// dispatch(setIsProcessing(true));
const {
postprocessing: {
upscalingLevel,
upscalingDenoising,
upscalingStrength,
},
} = getState();
// const {
// postprocessing: {
// upscalingLevel,
// upscalingDenoising,
// upscalingStrength,
// },
// } = getState();
const esrganParameters = {
upscale: [upscalingLevel, upscalingDenoising, upscalingStrength],
};
socketio.emit('runPostprocessing', imageToProcess, {
type: 'esrgan',
...esrganParameters,
});
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `ESRGAN upscale requested: ${JSON.stringify({
file: imageToProcess.url,
...esrganParameters,
})}`,
})
);
},
emitRunFacetool: (imageToProcess: InvokeAI._Image) => {
dispatch(setIsProcessing(true));
// const esrganParameters = {
// upscale: [upscalingLevel, upscalingDenoising, upscalingStrength],
// };
// socketio.emit('runPostprocessing', imageToProcess, {
// type: 'esrgan',
// ...esrganParameters,
// });
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `ESRGAN upscale requested: ${JSON.stringify({
// file: imageToProcess.url,
// ...esrganParameters,
// })}`,
// })
// );
// },
// emitRunFacetool: (imageToProcess: InvokeAI._Image) => {
// dispatch(setIsProcessing(true));
const {
postprocessing: { facetoolType, facetoolStrength, codeformerFidelity },
} = getState();
// const {
// postprocessing: { facetoolType, facetoolStrength, codeformerFidelity },
// } = getState();
const facetoolParameters: Record<string, unknown> = {
facetool_strength: facetoolStrength,
};
// const facetoolParameters: Record<string, unknown> = {
// facetool_strength: facetoolStrength,
// };
if (facetoolType === 'codeformer') {
facetoolParameters.codeformer_fidelity = codeformerFidelity;
}
// if (facetoolType === 'codeformer') {
// facetoolParameters.codeformer_fidelity = codeformerFidelity;
// }
socketio.emit('runPostprocessing', imageToProcess, {
type: facetoolType,
...facetoolParameters,
});
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Face restoration (${facetoolType}) requested: ${JSON.stringify(
{
file: imageToProcess.url,
...facetoolParameters,
}
)}`,
})
);
},
emitDeleteImage: (imageToDelete: InvokeAI._Image) => {
const { url, uuid, category, thumbnail } = imageToDelete;
dispatch(removeImage(imageToDelete));
socketio.emit('deleteImage', url, thumbnail, uuid, category);
},
emitRequestImages: (category: GalleryCategory) => {
const gallery: GalleryState = getState().gallery;
const { earliest_mtime } = gallery.categories[category];
socketio.emit('requestImages', category, earliest_mtime);
},
emitRequestNewImages: (category: GalleryCategory) => {
const gallery: GalleryState = getState().gallery;
const { latest_mtime } = gallery.categories[category];
socketio.emit('requestLatestImages', category, latest_mtime);
},
emitCancelProcessing: () => {
socketio.emit('cancel');
},
emitRequestSystemConfig: () => {
socketio.emit('requestSystemConfig');
},
emitSearchForModels: (modelFolder: string) => {
socketio.emit('searchForModels', modelFolder);
},
emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => {
socketio.emit('addNewModel', modelConfig);
},
emitDeleteModel: (modelName: string) => {
socketio.emit('deleteModel', modelName);
},
emitConvertToDiffusers: (
modelToConvert: InvokeAI.InvokeModelConversionProps
) => {
dispatch(modelConvertRequested());
socketio.emit('convertToDiffusers', modelToConvert);
},
emitMergeDiffusersModels: (
modelMergeInfo: InvokeAI.InvokeModelMergingProps
) => {
dispatch(modelMergingRequested());
socketio.emit('mergeDiffusersModels', modelMergeInfo);
},
emitRequestModelChange: (modelName: string) => {
dispatch(modelChangeRequested());
socketio.emit('requestModelChange', modelName);
},
emitSaveStagingAreaImageToGallery: (url: string) => {
socketio.emit('requestSaveStagingAreaImageToGallery', url);
},
emitRequestEmptyTempFolder: () => {
socketio.emit('requestEmptyTempFolder');
},
};
};
// socketio.emit('runPostprocessing', imageToProcess, {
// type: facetoolType,
// ...facetoolParameters,
// });
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Face restoration (${facetoolType}) requested: ${JSON.stringify(
// {
// file: imageToProcess.url,
// ...facetoolParameters,
// }
// )}`,
// })
// );
// },
// emitDeleteImage: (imageToDelete: InvokeAI._Image) => {
// const { url, uuid, category, thumbnail } = imageToDelete;
// dispatch(removeImage(imageToDelete));
// socketio.emit('deleteImage', url, thumbnail, uuid, category);
// },
// emitRequestImages: (category: GalleryCategory) => {
// const gallery: GalleryState = getState().gallery;
// const { earliest_mtime } = gallery.categories[category];
// socketio.emit('requestImages', category, earliest_mtime);
// },
// emitRequestNewImages: (category: GalleryCategory) => {
// const gallery: GalleryState = getState().gallery;
// const { latest_mtime } = gallery.categories[category];
// socketio.emit('requestLatestImages', category, latest_mtime);
// },
// emitCancelProcessing: () => {
// socketio.emit('cancel');
// },
// emitRequestSystemConfig: () => {
// socketio.emit('requestSystemConfig');
// },
// emitSearchForModels: (modelFolder: string) => {
// socketio.emit('searchForModels', modelFolder);
// },
// emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => {
// socketio.emit('addNewModel', modelConfig);
// },
// emitDeleteModel: (modelName: string) => {
// socketio.emit('deleteModel', modelName);
// },
// emitConvertToDiffusers: (
// modelToConvert: InvokeAI.InvokeModelConversionProps
// ) => {
// dispatch(modelConvertRequested());
// socketio.emit('convertToDiffusers', modelToConvert);
// },
// emitMergeDiffusersModels: (
// modelMergeInfo: InvokeAI.InvokeModelMergingProps
// ) => {
// dispatch(modelMergingRequested());
// socketio.emit('mergeDiffusersModels', modelMergeInfo);
// },
// emitRequestModelChange: (modelName: string) => {
// dispatch(modelChangeRequested());
// socketio.emit('requestModelChange', modelName);
// },
// emitSaveStagingAreaImageToGallery: (url: string) => {
// socketio.emit('requestSaveStagingAreaImageToGallery', url);
// },
// emitRequestEmptyTempFolder: () => {
// socketio.emit('requestEmptyTempFolder');
// },
// };
// };
export default makeSocketIOEmitters;
// export default makeSocketIOEmitters;
export default {};

View File

@@ -1,501 +1,502 @@
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
import dateFormat from 'dateformat';
import i18n from 'i18n';
import { v4 as uuidv4 } from 'uuid';
// import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
// import dateFormat from 'dateformat';
// import i18n from 'i18n';
// import { v4 as uuidv4 } from 'uuid';
import * as InvokeAI from 'app/types/invokeai';
// import * as InvokeAI from 'app/types/invokeai';
import {
addLogEntry,
addToast,
errorOccurred,
processingCanceled,
setCurrentStatus,
setFoundModels,
setIsCancelable,
setIsConnected,
setIsProcessing,
setModelList,
setSearchFolder,
setSystemConfig,
setSystemStatus,
} from 'features/system/store/systemSlice';
// import {
// addToast,
// errorOccurred,
// processingCanceled,
// setCurrentStatus,
// setFoundModels,
// setIsCancelable,
// setIsConnected,
// setIsProcessing,
// setModelList,
// setSearchFolder,
// setSystemConfig,
// setSystemStatus,
// } from 'features/system/store/systemSlice';
import {
addGalleryImages,
addImage,
clearIntermediateImage,
GalleryState,
removeImage,
setIntermediateImage,
} from 'features/gallery/store/gallerySlice';
// import {
// addGalleryImages,
// addImage,
// clearIntermediateImage,
// GalleryState,
// removeImage,
// setIntermediateImage,
// } from 'features/gallery/store/gallerySlice';
import type { RootState } from 'app/store/store';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
import {
clearInitialImage,
initialImageSelected,
setInfillMethod,
// setInitialImage,
setMaskPath,
} from 'features/parameters/store/generationSlice';
import { tabMap } from 'features/ui/store/tabMap';
import {
requestImages,
requestNewImages,
requestSystemConfig,
} from './actions';
// import type { RootState } from 'app/store/store';
// import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
// import {
// clearInitialImage,
// initialImageSelected,
// setInfillMethod,
// // setInitialImage,
// setMaskPath,
// } from 'features/parameters/store/generationSlice';
// import { tabMap } from 'features/ui/store/tabMap';
// import {
// requestImages,
// requestNewImages,
// requestSystemConfig,
// } from './actions';
/**
* Returns an object containing listener callbacks for socketio events.
* TODO: This file is large, but simple. Should it be split up further?
*/
const makeSocketIOListeners = (
store: MiddlewareAPI<Dispatch<AnyAction>, RootState>
) => {
const { dispatch, getState } = store;
// /**
// * Returns an object containing listener callbacks for socketio events.
// * TODO: This file is large, but simple. Should it be split up further?
// */
// const makeSocketIOListeners = (
// store: MiddlewareAPI<Dispatch<AnyAction>, RootState>
// ) => {
// const { dispatch, getState } = store;
return {
/**
* Callback to run when we receive a 'connect' event.
*/
onConnect: () => {
try {
dispatch(setIsConnected(true));
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
dispatch(requestSystemConfig());
const gallery: GalleryState = getState().gallery;
// return {
// /**
// * Callback to run when we receive a 'connect' event.
// */
// onConnect: () => {
// try {
// dispatch(setIsConnected(true));
// dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
// dispatch(requestSystemConfig());
// const gallery: GalleryState = getState().gallery;
if (gallery.categories.result.latest_mtime) {
dispatch(requestNewImages('result'));
} else {
dispatch(requestImages('result'));
}
// if (gallery.categories.result.latest_mtime) {
// dispatch(requestNewImages('result'));
// } else {
// dispatch(requestImages('result'));
// }
if (gallery.categories.user.latest_mtime) {
dispatch(requestNewImages('user'));
} else {
dispatch(requestImages('user'));
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'disconnect' event.
*/
onDisconnect: () => {
try {
dispatch(setIsConnected(false));
dispatch(setCurrentStatus(i18n.t('common.statusDisconnected')));
// if (gallery.categories.user.latest_mtime) {
// dispatch(requestNewImages('user'));
// } else {
// dispatch(requestImages('user'));
// }
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive a 'disconnect' event.
// */
// onDisconnect: () => {
// try {
// dispatch(setIsConnected(false));
// dispatch(setCurrentStatus(i18n.t('common.statusDisconnected')));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Disconnected from server`,
level: 'warning',
})
);
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'generationResult' event.
*/
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
try {
const state = getState();
const { activeTab } = state.ui;
const { shouldLoopback } = state.postprocessing;
const { boundingBox: _, generationMode, ...rest } = data;
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Disconnected from server`,
// level: 'warning',
// })
// );
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive a 'generationResult' event.
// */
// onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
// try {
// const state = getState();
// const { activeTab } = state.ui;
// const { shouldLoopback } = state.postprocessing;
// const { boundingBox: _, generationMode, ...rest } = data;
const newImage = {
uuid: uuidv4(),
...rest,
};
// const newImage = {
// uuid: uuidv4(),
// ...rest,
// };
if (['txt2img', 'img2img'].includes(generationMode)) {
dispatch(
addImage({
category: 'result',
image: { ...newImage, category: 'result' },
})
);
}
// if (['txt2img', 'img2img'].includes(generationMode)) {
// dispatch(
// addImage({
// category: 'result',
// image: { ...newImage, category: 'result' },
// })
// );
// }
if (generationMode === 'unifiedCanvas' && data.boundingBox) {
const { boundingBox } = data;
dispatch(
addImageToStagingArea({
image: { ...newImage, category: 'temp' },
boundingBox,
})
);
// if (generationMode === 'unifiedCanvas' && data.boundingBox) {
// const { boundingBox } = data;
// dispatch(
// addImageToStagingArea({
// image: { ...newImage, category: 'temp' },
// boundingBox,
// })
// );
if (state.canvas.shouldAutoSave) {
dispatch(
addImage({
image: { ...newImage, category: 'result' },
category: 'result',
})
);
}
}
// if (state.canvas.shouldAutoSave) {
// dispatch(
// addImage({
// image: { ...newImage, category: 'result' },
// category: 'result',
// })
// );
// }
// }
// TODO: fix
// if (shouldLoopback) {
// const activeTabName = tabMap[activeTab];
// switch (activeTabName) {
// case 'img2img': {
// dispatch(initialImageSelected(newImage.uuid));
// // dispatch(setInitialImage(newImage));
// break;
// }
// }
// }
// // TODO: fix
// // if (shouldLoopback) {
// // const activeTabName = tabMap[activeTab];
// // switch (activeTabName) {
// // case 'img2img': {
// // dispatch(initialImageSelected(newImage.uuid));
// // // dispatch(setInitialImage(newImage));
// // break;
// // }
// // }
// // }
dispatch(clearIntermediateImage());
// dispatch(clearIntermediateImage());
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Image generated: ${data.url}`,
})
);
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'intermediateResult' event.
*/
onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
try {
dispatch(
setIntermediateImage({
uuid: uuidv4(),
...data,
category: 'result',
})
);
if (!data.isBase64) {
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Intermediate image generated: ${data.url}`,
})
);
}
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive an 'esrganResult' event.
*/
onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
try {
dispatch(
addImage({
category: 'result',
image: {
uuid: uuidv4(),
...data,
category: 'result',
},
})
);
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Image generated: ${data.url}`,
// })
// );
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive a 'intermediateResult' event.
// */
// onIntermediateResult: (data: InvokeAI.ImageResultResponse) => {
// try {
// dispatch(
// setIntermediateImage({
// uuid: uuidv4(),
// ...data,
// category: 'result',
// })
// );
// if (!data.isBase64) {
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Intermediate image generated: ${data.url}`,
// })
// );
// }
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive an 'esrganResult' event.
// */
// onPostprocessingResult: (data: InvokeAI.ImageResultResponse) => {
// try {
// dispatch(
// addImage({
// category: 'result',
// image: {
// uuid: uuidv4(),
// ...data,
// category: 'result',
// },
// })
// );
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Postprocessed: ${data.url}`,
})
);
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'progressUpdate' event.
* TODO: Add additional progress phases
*/
onProgressUpdate: (data: InvokeAI.SystemStatus) => {
try {
dispatch(setIsProcessing(true));
dispatch(setSystemStatus(data));
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'progressUpdate' event.
*/
onError: (data: InvokeAI.ErrorResponse) => {
const { message, additionalData } = data;
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Postprocessed: ${data.url}`,
// })
// );
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive a 'progressUpdate' event.
// * TODO: Add additional progress phases
// */
// onProgressUpdate: (data: InvokeAI.SystemStatus) => {
// try {
// dispatch(setIsProcessing(true));
// dispatch(setSystemStatus(data));
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive a 'progressUpdate' event.
// */
// onError: (data: InvokeAI.ErrorResponse) => {
// const { message, additionalData } = data;
if (additionalData) {
// TODO: handle more data than short message
}
// if (additionalData) {
// // TODO: handle more data than short message
// }
try {
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Server error: ${message}`,
level: 'error',
})
);
dispatch(errorOccurred());
dispatch(clearIntermediateImage());
} catch (e) {
console.error(e);
}
},
/**
* Callback to run when we receive a 'galleryImages' event.
*/
onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
const { images, areMoreImagesAvailable, category } = data;
// try {
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Server error: ${message}`,
// level: 'error',
// })
// );
// dispatch(errorOccurred());
// dispatch(clearIntermediateImage());
// } catch (e) {
// console.error(e);
// }
// },
// /**
// * Callback to run when we receive a 'galleryImages' event.
// */
// onGalleryImages: (data: InvokeAI.GalleryImagesResponse) => {
// const { images, areMoreImagesAvailable, category } = data;
/**
* the logic here ideally would be in the reducer but we have a side effect:
* generating a uuid. so the logic needs to be here, outside redux.
*/
// /**
// * the logic here ideally would be in the reducer but we have a side effect:
// * generating a uuid. so the logic needs to be here, outside redux.
// */
// Generate a UUID for each image
const preparedImages = images.map((image): InvokeAI._Image => {
return {
uuid: uuidv4(),
...image,
};
});
// // Generate a UUID for each image
// const preparedImages = images.map((image): InvokeAI._Image => {
// return {
// uuid: uuidv4(),
// ...image,
// };
// });
dispatch(
addGalleryImages({
images: preparedImages,
areMoreImagesAvailable,
category,
})
);
// dispatch(
// addGalleryImages({
// images: preparedImages,
// areMoreImagesAvailable,
// category,
// })
// );
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Loaded ${images.length} images`,
})
);
},
/**
* Callback to run when we receive a 'processingCanceled' event.
*/
onProcessingCanceled: () => {
dispatch(processingCanceled());
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Loaded ${images.length} images`,
// })
// );
// },
// /**
// * Callback to run when we receive a 'processingCanceled' event.
// */
// onProcessingCanceled: () => {
// dispatch(processingCanceled());
const { intermediateImage } = getState().gallery;
// const { intermediateImage } = getState().gallery;
if (intermediateImage) {
if (!intermediateImage.isBase64) {
dispatch(
addImage({
category: 'result',
image: intermediateImage,
})
);
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Intermediate image saved: ${intermediateImage.url}`,
})
);
}
dispatch(clearIntermediateImage());
}
// if (intermediateImage) {
// if (!intermediateImage.isBase64) {
// dispatch(
// addImage({
// category: 'result',
// image: intermediateImage,
// })
// );
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Intermediate image saved: ${intermediateImage.url}`,
// })
// );
// }
// dispatch(clearIntermediateImage());
// }
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Processing canceled`,
level: 'warning',
})
);
},
/**
* Callback to run when we receive a 'imageDeleted' event.
*/
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
const { url } = data;
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Processing canceled`,
// level: 'warning',
// })
// );
// },
// /**
// * Callback to run when we receive a 'imageDeleted' event.
// */
// onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
// const { url } = data;
// remove image from gallery
dispatch(removeImage(data));
// // remove image from gallery
// dispatch(removeImage(data));
// remove references to image in options
const {
generation: { initialImage, maskPath },
} = getState();
// // remove references to image in options
// const {
// generation: { initialImage, maskPath },
// } = getState();
if (
initialImage === url ||
(initialImage as InvokeAI._Image)?.url === url
) {
dispatch(clearInitialImage());
}
// if (
// initialImage === url ||
// (initialImage as InvokeAI._Image)?.url === url
// ) {
// dispatch(clearInitialImage());
// }
if (maskPath === url) {
dispatch(setMaskPath(''));
}
// if (maskPath === url) {
// dispatch(setMaskPath(''));
// }
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Image deleted: ${url}`,
})
);
},
onSystemConfig: (data: InvokeAI.SystemConfig) => {
dispatch(setSystemConfig(data));
if (!data.infill_methods.includes('patchmatch')) {
dispatch(setInfillMethod(data.infill_methods[0]));
}
},
onFoundModels: (data: InvokeAI.FoundModelResponse) => {
const { search_folder, found_models } = data;
dispatch(setSearchFolder(search_folder));
dispatch(setFoundModels(found_models));
},
onNewModelAdded: (data: InvokeAI.ModelAddedResponse) => {
const { new_model_name, model_list, update } = data;
dispatch(setModelList(model_list));
dispatch(setIsProcessing(false));
dispatch(setCurrentStatus(i18n.t('modelManager.modelAdded')));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model Added: ${new_model_name}`,
level: 'info',
})
);
dispatch(
addToast({
title: !update
? `${i18n.t('modelManager.modelAdded')}: ${new_model_name}`
: `${i18n.t('modelManager.modelUpdated')}: ${new_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelDeleted: (data: InvokeAI.ModelDeletedResponse) => {
const { deleted_model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setIsProcessing(false));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `${i18n.t(
'modelManager.modelAdded'
)}: ${deleted_model_name}`,
level: 'info',
})
);
dispatch(
addToast({
title: `${i18n.t(
'modelManager.modelEntryDeleted'
)}: ${deleted_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelConverted: (data: InvokeAI.ModelConvertedResponse) => {
const { converted_model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus(i18n.t('common.statusModelConverted')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model converted: ${converted_model_name}`,
level: 'info',
})
);
dispatch(
addToast({
title: `${i18n.t(
'modelManager.modelConverted'
)}: ${converted_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelsMerged: (data: InvokeAI.ModelsMergedResponse) => {
const { merged_models, merged_model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus(i18n.t('common.statusMergedModels')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Models merged: ${merged_models}`,
level: 'info',
})
);
dispatch(
addToast({
title: `${i18n.t('modelManager.modelsMerged')}: ${merged_model_name}`,
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
const { model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setCurrentStatus(i18n.t('common.statusModelChanged')));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model changed: ${model_name}`,
level: 'info',
})
);
},
onModelChangeFailed: (data: InvokeAI.ModelChangeResponse) => {
const { model_name, model_list } = data;
dispatch(setModelList(model_list));
dispatch(setIsProcessing(false));
dispatch(setIsCancelable(true));
dispatch(errorOccurred());
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Model change failed: ${model_name}`,
level: 'error',
})
);
},
onTempFolderEmptied: () => {
dispatch(
addToast({
title: i18n.t('toast.tempFoldersEmptied'),
status: 'success',
duration: 2500,
isClosable: true,
})
);
},
};
};
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Image deleted: ${url}`,
// })
// );
// },
// onSystemConfig: (data: InvokeAI.SystemConfig) => {
// dispatch(setSystemConfig(data));
// if (!data.infill_methods.includes('patchmatch')) {
// dispatch(setInfillMethod(data.infill_methods[0]));
// }
// },
// onFoundModels: (data: InvokeAI.FoundModelResponse) => {
// const { search_folder, found_models } = data;
// dispatch(setSearchFolder(search_folder));
// dispatch(setFoundModels(found_models));
// },
// onNewModelAdded: (data: InvokeAI.ModelAddedResponse) => {
// const { new_model_name, model_list, update } = data;
// dispatch(setModelList(model_list));
// dispatch(setIsProcessing(false));
// dispatch(setCurrentStatus(i18n.t('modelManager.modelAdded')));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Model Added: ${new_model_name}`,
// level: 'info',
// })
// );
// dispatch(
// addToast({
// title: !update
// ? `${i18n.t('modelManager.modelAdded')}: ${new_model_name}`
// : `${i18n.t('modelManager.modelUpdated')}: ${new_model_name}`,
// status: 'success',
// duration: 2500,
// isClosable: true,
// })
// );
// },
// onModelDeleted: (data: InvokeAI.ModelDeletedResponse) => {
// const { deleted_model_name, model_list } = data;
// dispatch(setModelList(model_list));
// dispatch(setIsProcessing(false));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `${i18n.t(
// 'modelManager.modelAdded'
// )}: ${deleted_model_name}`,
// level: 'info',
// })
// );
// dispatch(
// addToast({
// title: `${i18n.t(
// 'modelManager.modelEntryDeleted'
// )}: ${deleted_model_name}`,
// status: 'success',
// duration: 2500,
// isClosable: true,
// })
// );
// },
// onModelConverted: (data: InvokeAI.ModelConvertedResponse) => {
// const { converted_model_name, model_list } = data;
// dispatch(setModelList(model_list));
// dispatch(setCurrentStatus(i18n.t('common.statusModelConverted')));
// dispatch(setIsProcessing(false));
// dispatch(setIsCancelable(true));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Model converted: ${converted_model_name}`,
// level: 'info',
// })
// );
// dispatch(
// addToast({
// title: `${i18n.t(
// 'modelManager.modelConverted'
// )}: ${converted_model_name}`,
// status: 'success',
// duration: 2500,
// isClosable: true,
// })
// );
// },
// onModelsMerged: (data: InvokeAI.ModelsMergedResponse) => {
// const { merged_models, merged_model_name, model_list } = data;
// dispatch(setModelList(model_list));
// dispatch(setCurrentStatus(i18n.t('common.statusMergedModels')));
// dispatch(setIsProcessing(false));
// dispatch(setIsCancelable(true));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Models merged: ${merged_models}`,
// level: 'info',
// })
// );
// dispatch(
// addToast({
// title: `${i18n.t('modelManager.modelsMerged')}: ${merged_model_name}`,
// status: 'success',
// duration: 2500,
// isClosable: true,
// })
// );
// },
// onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
// const { model_name, model_list } = data;
// dispatch(setModelList(model_list));
// dispatch(setCurrentStatus(i18n.t('common.statusModelChanged')));
// dispatch(setIsProcessing(false));
// dispatch(setIsCancelable(true));
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Model changed: ${model_name}`,
// level: 'info',
// })
// );
// },
// onModelChangeFailed: (data: InvokeAI.ModelChangeResponse) => {
// const { model_name, model_list } = data;
// dispatch(setModelList(model_list));
// dispatch(setIsProcessing(false));
// dispatch(setIsCancelable(true));
// dispatch(errorOccurred());
// dispatch(
// addLogEntry({
// timestamp: dateFormat(new Date(), 'isoDateTime'),
// message: `Model change failed: ${model_name}`,
// level: 'error',
// })
// );
// },
// onTempFolderEmptied: () => {
// dispatch(
// addToast({
// title: i18n.t('toast.tempFoldersEmptied'),
// status: 'success',
// duration: 2500,
// isClosable: true,
// })
// );
// },
// };
// };
export default makeSocketIOListeners;
// export default makeSocketIOListeners;
export default {};

View File

@@ -1,246 +1,248 @@
import { Middleware } from '@reduxjs/toolkit';
import { io } from 'socket.io-client';
// import { Middleware } from '@reduxjs/toolkit';
// import { io } from 'socket.io-client';
import makeSocketIOEmitters from './emitters';
import makeSocketIOListeners from './listeners';
// import makeSocketIOEmitters from './emitters';
// import makeSocketIOListeners from './listeners';
import * as InvokeAI from 'app/types/invokeai';
// import * as InvokeAI from 'app/types/invokeai';
/**
* Creates a socketio middleware to handle communication with server.
*
* Special `socketio/actionName` actions are created in actions.ts and
* exported for use by the application, which treats them like any old
* action, using `dispatch` to dispatch them.
*
* These actions are intercepted here, where `socketio.emit()` calls are
* made on their behalf - see `emitters.ts`. The emitter functions
* are the outbound communication to the server.
*
* Listeners are also established here - see `listeners.ts`. The listener
* functions receive communication from the server and usually dispatch
* some new action to handle whatever data was sent from the server.
*/
export const socketioMiddleware = () => {
const { origin } = new URL(window.location.href);
// /**
// * Creates a socketio middleware to handle communication with server.
// *
// * Special `socketio/actionName` actions are created in actions.ts and
// * exported for use by the application, which treats them like any old
// * action, using `dispatch` to dispatch them.
// *
// * These actions are intercepted here, where `socketio.emit()` calls are
// * made on their behalf - see `emitters.ts`. The emitter functions
// * are the outbound communication to the server.
// *
// * Listeners are also established here - see `listeners.ts`. The listener
// * functions receive communication from the server and usually dispatch
// * some new action to handle whatever data was sent from the server.
// */
// export const socketioMiddleware = () => {
// const { origin } = new URL(window.location.href);
const socketio = io(origin, {
timeout: 60000,
path: `${window.location.pathname}socket.io`,
});
// const socketio = io(origin, {
// timeout: 60000,
// path: `${window.location.pathname}socket.io`,
// });
socketio.disconnect();
// socketio.disconnect();
let areListenersSet = false;
// let areListenersSet = false;
const middleware: Middleware = (store) => (next) => (action) => {
const {
onConnect,
onDisconnect,
onError,
onPostprocessingResult,
onGenerationResult,
onIntermediateResult,
onProgressUpdate,
onGalleryImages,
onProcessingCanceled,
onImageDeleted,
onSystemConfig,
onModelChanged,
onFoundModels,
onNewModelAdded,
onModelDeleted,
onModelConverted,
onModelsMerged,
onModelChangeFailed,
onTempFolderEmptied,
} = makeSocketIOListeners(store);
// const middleware: Middleware = (store) => (next) => (action) => {
// const {
// onConnect,
// onDisconnect,
// onError,
// onPostprocessingResult,
// onGenerationResult,
// onIntermediateResult,
// onProgressUpdate,
// onGalleryImages,
// onProcessingCanceled,
// onImageDeleted,
// onSystemConfig,
// onModelChanged,
// onFoundModels,
// onNewModelAdded,
// onModelDeleted,
// onModelConverted,
// onModelsMerged,
// onModelChangeFailed,
// onTempFolderEmptied,
// } = makeSocketIOListeners(store);
const {
emitGenerateImage,
emitRunESRGAN,
emitRunFacetool,
emitDeleteImage,
emitRequestImages,
emitRequestNewImages,
emitCancelProcessing,
emitRequestSystemConfig,
emitSearchForModels,
emitAddNewModel,
emitDeleteModel,
emitConvertToDiffusers,
emitMergeDiffusersModels,
emitRequestModelChange,
emitSaveStagingAreaImageToGallery,
emitRequestEmptyTempFolder,
} = makeSocketIOEmitters(store, socketio);
// const {
// emitGenerateImage,
// emitRunESRGAN,
// emitRunFacetool,
// emitDeleteImage,
// emitRequestImages,
// emitRequestNewImages,
// emitCancelProcessing,
// emitRequestSystemConfig,
// emitSearchForModels,
// emitAddNewModel,
// emitDeleteModel,
// emitConvertToDiffusers,
// emitMergeDiffusersModels,
// emitRequestModelChange,
// emitSaveStagingAreaImageToGallery,
// emitRequestEmptyTempFolder,
// } = makeSocketIOEmitters(store, socketio);
/**
* If this is the first time the middleware has been called (e.g. during store setup),
* initialize all our socket.io listeners.
*/
if (!areListenersSet) {
socketio.on('connect', () => onConnect());
// /**
// * If this is the first time the middleware has been called (e.g. during store setup),
// * initialize all our socket.io listeners.
// */
// if (!areListenersSet) {
// socketio.on('connect', () => onConnect());
socketio.on('disconnect', () => onDisconnect());
// socketio.on('disconnect', () => onDisconnect());
socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data));
// socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data));
socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) =>
onGenerationResult(data)
);
// socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) =>
// onGenerationResult(data)
// );
socketio.on(
'postprocessingResult',
(data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data)
);
// socketio.on(
// 'postprocessingResult',
// (data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data)
// );
socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) =>
onIntermediateResult(data)
);
// socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) =>
// onIntermediateResult(data)
// );
socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) =>
onProgressUpdate(data)
);
// socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) =>
// onProgressUpdate(data)
// );
socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) =>
onGalleryImages(data)
);
// socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) =>
// onGalleryImages(data)
// );
socketio.on('processingCanceled', () => {
onProcessingCanceled();
});
// socketio.on('processingCanceled', () => {
// onProcessingCanceled();
// });
socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => {
onImageDeleted(data);
});
// socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => {
// onImageDeleted(data);
// });
socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
onSystemConfig(data);
});
// socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => {
// onSystemConfig(data);
// });
socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => {
onFoundModels(data);
});
// socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => {
// onFoundModels(data);
// });
socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => {
onNewModelAdded(data);
});
// socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => {
// onNewModelAdded(data);
// });
socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => {
onModelDeleted(data);
});
// socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => {
// onModelDeleted(data);
// });
socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => {
onModelConverted(data);
});
// socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => {
// onModelConverted(data);
// });
socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => {
onModelsMerged(data);
});
// socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => {
// onModelsMerged(data);
// });
socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => {
onModelChanged(data);
});
// socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => {
// onModelChanged(data);
// });
socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => {
onModelChangeFailed(data);
});
// socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => {
// onModelChangeFailed(data);
// });
socketio.on('tempFolderEmptied', () => {
onTempFolderEmptied();
});
// socketio.on('tempFolderEmptied', () => {
// onTempFolderEmptied();
// });
areListenersSet = true;
}
// areListenersSet = true;
// }
/**
* Handle redux actions caught by middleware.
*/
switch (action.type) {
case 'socketio/generateImage': {
emitGenerateImage(action.payload);
break;
}
// /**
// * Handle redux actions caught by middleware.
// */
// switch (action.type) {
// case 'socketio/generateImage': {
// emitGenerateImage(action.payload);
// break;
// }
case 'socketio/runESRGAN': {
emitRunESRGAN(action.payload);
break;
}
// case 'socketio/runESRGAN': {
// emitRunESRGAN(action.payload);
// break;
// }
case 'socketio/runFacetool': {
emitRunFacetool(action.payload);
break;
}
// case 'socketio/runFacetool': {
// emitRunFacetool(action.payload);
// break;
// }
case 'socketio/deleteImage': {
emitDeleteImage(action.payload);
break;
}
// case 'socketio/deleteImage': {
// emitDeleteImage(action.payload);
// break;
// }
case 'socketio/requestImages': {
emitRequestImages(action.payload);
break;
}
// case 'socketio/requestImages': {
// emitRequestImages(action.payload);
// break;
// }
case 'socketio/requestNewImages': {
emitRequestNewImages(action.payload);
break;
}
// case 'socketio/requestNewImages': {
// emitRequestNewImages(action.payload);
// break;
// }
case 'socketio/cancelProcessing': {
emitCancelProcessing();
break;
}
// case 'socketio/cancelProcessing': {
// emitCancelProcessing();
// break;
// }
case 'socketio/requestSystemConfig': {
emitRequestSystemConfig();
break;
}
// case 'socketio/requestSystemConfig': {
// emitRequestSystemConfig();
// break;
// }
case 'socketio/searchForModels': {
emitSearchForModels(action.payload);
break;
}
// case 'socketio/searchForModels': {
// emitSearchForModels(action.payload);
// break;
// }
case 'socketio/addNewModel': {
emitAddNewModel(action.payload);
break;
}
// case 'socketio/addNewModel': {
// emitAddNewModel(action.payload);
// break;
// }
case 'socketio/deleteModel': {
emitDeleteModel(action.payload);
break;
}
// case 'socketio/deleteModel': {
// emitDeleteModel(action.payload);
// break;
// }
case 'socketio/convertToDiffusers': {
emitConvertToDiffusers(action.payload);
break;
}
// case 'socketio/convertToDiffusers': {
// emitConvertToDiffusers(action.payload);
// break;
// }
case 'socketio/mergeDiffusersModels': {
emitMergeDiffusersModels(action.payload);
break;
}
// case 'socketio/mergeDiffusersModels': {
// emitMergeDiffusersModels(action.payload);
// break;
// }
case 'socketio/requestModelChange': {
emitRequestModelChange(action.payload);
break;
}
// case 'socketio/requestModelChange': {
// emitRequestModelChange(action.payload);
// break;
// }
case 'socketio/saveStagingAreaImageToGallery': {
emitSaveStagingAreaImageToGallery(action.payload);
break;
}
// case 'socketio/saveStagingAreaImageToGallery': {
// emitSaveStagingAreaImageToGallery(action.payload);
// break;
// }
case 'socketio/requestEmptyTempFolder': {
emitRequestEmptyTempFolder();
break;
}
}
// case 'socketio/requestEmptyTempFolder': {
// emitRequestEmptyTempFolder();
// break;
// }
// }
next(action);
};
// next(action);
// };
return middleware;
};
// return middleware;
// };
export default {};

View File

@@ -19,8 +19,6 @@ import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice';
import nodesReducer from 'features/nodes/store/nodesSlice';
import { socketioMiddleware } from '../socketio/middleware';
import { socketMiddleware } from 'services/events/middleware';
import { canvasDenylist } from 'features/canvas/store/canvasPersistDenylist';
import { galleryDenylist } from 'features/gallery/store/galleryPersistDenylist';
import { generationDenylist } from 'features/parameters/store/generationPersistDenylist';
@@ -28,8 +26,10 @@ import { lightboxDenylist } from 'features/lightbox/store/lightboxPersistDenylis
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/systemPersistsDenylist';
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';
/**
* redux-persist provides an easy and reliable way to persist state across reloads.
@@ -73,28 +73,25 @@ const rootPersistConfig = getPersistConfig({
...modelsDenylist,
...nodesDenylist,
...postprocessingDenylist,
// ...resultsDenylist,
'results',
...resultsDenylist,
...systemDenylist,
...uiDenylist,
// ...uploadsDenylist,
'uploads',
...uploadsDenylist,
'hotkeys',
'config',
],
debounce: 300,
});
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();
}
}
// export function buildMiddleware() {
// if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') {
// return socketMiddleware();
// } else {
// return socketioMiddleware();
// }
// }
export const store = configureStore({
reducer: persistedReducer,

View File

@@ -111,24 +111,9 @@ export type FacetoolMetadata = CommonPostProcessedImageMetadata & {
export type PostProcessedImageMetadata = ESRGANMetadata | FacetoolMetadata;
// Metadata includes the system config and image metadata.
export type Metadata = SystemGenerationMetadata & {
image: GeneratedImageMetadata | PostProcessedImageMetadata;
};
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata
export type _Image = {
uuid: string;
url: string;
thumbnail: string;
mtime: number;
metadata?: Metadata;
width: number;
height: number;
category: GalleryCategory;
isBase64?: boolean;
dreamPrompt?: 'string';
name?: string;
};
// export type Metadata = SystemGenerationMetadata & {
// image: GeneratedImageMetadata | PostProcessedImageMetadata;
// };
/**
* ResultImage
@@ -141,40 +126,35 @@ export type Image = {
metadata: ImageResponseMetadata;
};
// GalleryImages is an array of Image.
export type GalleryImages = {
images: Array<_Image>;
};
/**
* Types related to the system status.
*/
// This represents the processing status of the backend.
export type SystemStatus = {
isProcessing: boolean;
currentStep: number;
totalSteps: number;
currentIteration: number;
totalIterations: number;
currentStatus: string;
currentStatusHasSteps: boolean;
hasError: boolean;
};
// // This represents the processing status of the backend.
// export type SystemStatus = {
// isProcessing: boolean;
// currentStep: number;
// totalSteps: number;
// currentIteration: number;
// totalIterations: number;
// currentStatus: string;
// currentStatusHasSteps: boolean;
// hasError: boolean;
// };
export type SystemGenerationMetadata = {
model: string;
model_weights?: string;
model_id?: string;
model_hash: string;
app_id: string;
app_version: string;
};
// export type SystemGenerationMetadata = {
// model: string;
// model_weights?: string;
// model_id?: string;
// model_hash: string;
// app_id: string;
// app_version: string;
// };
export type SystemConfig = SystemGenerationMetadata & {
model_list: ModelList;
infill_methods: string[];
};
// export type SystemConfig = SystemGenerationMetadata & {
// model_list: ModelList;
// infill_methods: string[];
// };
export type ModelStatus = 'active' | 'cached' | 'not loaded';
@@ -286,9 +266,9 @@ export type FoundModelResponse = {
found_models: FoundModel[];
};
export type SystemStatusResponse = SystemStatus;
// export type SystemStatusResponse = SystemStatus;
export type SystemConfigResponse = SystemConfig;
// export type SystemConfigResponse = SystemConfig;
export type ImageResultResponse = Omit<_Image, 'uuid'> & {
boundingBox?: IRect;
@@ -310,27 +290,10 @@ export type ErrorResponse = {
additionalData?: string;
};
export type GalleryImagesResponse = {
images: Array<Omit<_Image, 'uuid'>>;
areMoreImagesAvailable: boolean;
category: GalleryCategory;
};
export type ImageDeletedResponse = {
uuid: string;
url: string;
category: GalleryCategory;
};
export type ImageUrlResponse = {
url: string;
};
// export type UploadImagePayload = {
// file: File;
// destination?: ImageUploadDestination;
// };
export type UploadOutpaintingMergeImagePayload = {
dataURL: string;
name: string;

View File

@@ -16,13 +16,23 @@ type IAISelectProps = SelectProps & {
validValues:
| Array<number | string>
| Array<{ key: string; value: string | number }>;
horizontal?: boolean;
spaceEvenly?: boolean;
};
/**
* Customized Chakra FormControl + Select multi-part component.
*/
const IAISelect = (props: IAISelectProps) => {
const { label, isDisabled, validValues, tooltip, tooltipProps, ...rest } =
props;
const {
label,
isDisabled,
validValues,
tooltip,
tooltipProps,
horizontal,
spaceEvenly,
...rest
} = props;
return (
<FormControl
isDisabled={isDisabled}
@@ -32,10 +42,28 @@ const IAISelect = (props: IAISelectProps) => {
e.nativeEvent.stopPropagation();
e.nativeEvent.cancelBubble = true;
}}
sx={
horizontal
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 4,
}
: {}
}
>
{label && <FormLabel>{label}</FormLabel>}
{label && (
<FormLabel sx={spaceEvenly ? { flexBasis: 0, flexGrow: 1 } : {}}>
{label}
</FormLabel>
)}
<Tooltip label={tooltip} {...tooltipProps}>
<Select {...rest}>
<Select
{...rest}
rootProps={{ sx: spaceEvenly ? { flexBasis: 0, flexGrow: 1 } : {} }}
>
{validValues.map((opt) => {
return typeof opt === 'string' || typeof opt === 'number' ? (
<IAIOption key={opt} value={opt}>

View File

@@ -233,7 +233,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
hidden={hideTooltip}
{...sliderTooltipProps}
>
<SliderThumb {...sliderThumbProps} />
<SliderThumb {...sliderThumbProps} zIndex={0} />
</Tooltip>
</Slider>

View File

@@ -1,32 +1,11 @@
import { Badge, Box, ButtonGroup, Flex } from '@chakra-ui/react';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { useCallback } from 'react';
import IAIIconButton from 'common/components/IAIIconButton';
import { FaUndo, FaUpload } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { Badge, Box, Flex } from '@chakra-ui/react';
import { Image } from 'app/types/invokeai';
type ImageToImageOverlayProps = {
setIsLoaded: (isLoaded: boolean) => void;
image: Image;
};
const ImageToImageOverlay = ({
setIsLoaded,
image,
}: ImageToImageOverlayProps) => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage());
setIsLoaded(false);
}, [dispatch, setIsLoaded]);
const ImageToImageOverlay = ({ image }: ImageToImageOverlayProps) => {
return (
<Box
sx={{

View File

@@ -1,34 +1,13 @@
import {
Box,
ButtonGroup,
Collapse,
Flex,
Heading,
HStack,
Image,
Spacer,
Text,
useDisclosure,
VStack,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import IAIButton from 'common/components/IAIButton';
import ImageFit from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageFit';
import ImageToImageStrength from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength';
import { ButtonGroup, Flex, Spacer, Text } from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import { useTranslation } from 'react-i18next';
import { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { RootState } from 'app/store/store';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
const ImageToImageSettingsHeader = () => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const dispatch = useAppDispatch();
const { t } = useTranslation();

View File

@@ -1,6 +1,6 @@
import { ButtonGroup, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
// import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';

View File

@@ -1,7 +1,7 @@
import { AnyAction, ThunkAction } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import { RootState } from 'app/store/store';
import { addImage } from 'features/gallery/store/gallerySlice';
// import { addImage } from 'features/gallery/store/gallerySlice';
import {
addToast,
setCurrentStatus,

View File

@@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash-es';
import { get, isEqual, isNumber, isString } from 'lodash-es';
import {
ButtonGroup,
@@ -10,7 +10,7 @@ import {
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { runESRGAN, runFacetool } from 'app/socketio/actions';
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
@@ -63,11 +63,11 @@ import {
} from '../store/gallerySelectors';
import DeleteImageModal from './DeleteImageModal';
import { useCallback } from 'react';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
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';
const currentImageButtonsSelector = createSelector(
[
@@ -112,6 +112,8 @@ const currentImageButtonsSelector = createSelector(
isLightboxOpen,
shouldHidePreview,
image,
seed: image?.metadata?.invokeai?.node?.seed,
prompt: image?.metadata?.invokeai?.node?.prompt,
};
},
{
@@ -161,16 +163,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const toast = useToast();
const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts();
const handleClickUseAsInitialImage = useCallback(() => {
if (!image) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
dispatch(initialImageSelected(image.name));
// dispatch(setInitialImage(currentImage));
// dispatch(setActiveTab('img2img'));
}, [dispatch, image, isLightboxOpen]);
const { recallPrompt, recallSeed, sendToImageToImage } = useParameters();
const handleCopyImage = useCallback(async () => {
if (!image?.url) {
@@ -217,30 +211,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
});
}, [toast, shouldTransformUrls, getUrl, t, image]);
useHotkeys(
'shift+i',
() => {
if (image) {
handleClickUseAsInitialImage();
toast({
title: t('toast.sentToImageToImage'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: t('toast.imageNotLoaded'),
description: t('toast.imageNotLoadedDesc'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
},
[image]
);
const handlePreviewVisibility = useCallback(() => {
dispatch(setShouldHidePreview(!shouldHidePreview));
}, [dispatch, shouldHidePreview]);
@@ -259,7 +229,8 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'a',
() => {
if (['txt2img', 'img2img'].includes(image?.metadata?.sd_metadata?.type)) {
const type = image?.metadata?.invokeai?.node?.types;
if (isString(type) && ['txt2img', 'img2img'].includes(type)) {
handleClickUseAllParameters();
toast({
title: t('toast.parametersSet'),
@@ -280,63 +251,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
[image]
);
const handleClickUseSeed = () => {
image?.metadata && dispatch(setSeed(image.metadata.sd_metadata.seed));
};
const handleUseSeed = useCallback(() => {
recallSeed(image?.metadata?.invokeai?.node?.seed);
}, [image, recallSeed]);
useHotkeys(
's',
() => {
if (image?.metadata?.sd_metadata?.seed) {
handleClickUseSeed();
toast({
title: t('toast.seedSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: t('toast.seedNotSet'),
description: t('toast.seedNotSetDesc'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
},
[image]
);
useHotkeys('s', handleUseSeed, [image]);
const handleClickUsePrompt = useCallback(() => {
if (image?.metadata?.sd_metadata?.prompt) {
setBothPrompts(image?.metadata?.sd_metadata?.prompt);
}
}, [image?.metadata?.sd_metadata?.prompt, setBothPrompts]);
const handleUsePrompt = useCallback(() => {
recallPrompt(image?.metadata?.invokeai?.node?.prompt);
}, [image, recallPrompt]);
useHotkeys(
'p',
() => {
if (image?.metadata?.sd_metadata?.prompt) {
handleClickUsePrompt();
toast({
title: t('toast.promptSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
} else {
toast({
title: t('toast.promptNotSet'),
description: t('toast.promptNotSetDesc'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
},
[image]
);
useHotkeys('p', handleUsePrompt, [image]);
const handleSendToImageToImage = useCallback(() => {
sendToImageToImage(image);
}, [image, sendToImageToImage]);
useHotkeys('shift+i', handleSendToImageToImage, [image]);
const handleClickUpscale = useCallback(() => {
// selectedImage && dispatch(runESRGAN(selectedImage));
@@ -496,7 +427,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
>
<IAIButton
size="sm"
onClick={handleClickUseAsInitialImage}
onClick={handleSendToImageToImage}
leftIcon={<FaShare />}
>
{t('parameters.sendToImg2Img')}
@@ -570,16 +501,16 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={<FaQuoteRight />}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!image?.metadata?.sd_metadata?.prompt}
onClick={handleClickUsePrompt}
isDisabled={!image?.metadata?.invokeai?.node?.prompt}
onClick={handleUsePrompt}
/>
<IAIIconButton
icon={<FaSeedling />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!image?.metadata?.sd_metadata?.seed}
onClick={handleClickUseSeed}
isDisabled={!image?.metadata?.invokeai?.node?.seed}
onClick={handleUseSeed}
/>
<IAIIconButton

View File

@@ -17,31 +17,11 @@ export const imagesSelector = createSelector(
[uiSelector, selectedImageSelector, systemSelector],
(ui, selectedImage, system) => {
const { shouldShowImageDetails, shouldHidePreview } = ui;
const { progressImage } = system;
// TODO: Clean this up, this is really gross
const imageToDisplay = progressImage
? {
url: progressImage.dataURL,
width: progressImage.width,
height: progressImage.height,
isProgressImage: true,
image: progressImage,
}
: selectedImage
? {
url: selectedImage.url,
width: selectedImage.metadata.width,
height: selectedImage.metadata.height,
isProgressImage: false,
image: selectedImage,
}
: null;
return {
shouldShowImageDetails,
shouldHidePreview,
imageToDisplay,
image: selectedImage,
};
},
{
@@ -52,7 +32,7 @@ export const imagesSelector = createSelector(
);
const CurrentImagePreview = () => {
const { shouldShowImageDetails, imageToDisplay, shouldHidePreview } =
const { shouldShowImageDetails, image, shouldHidePreview } =
useAppSelector(imagesSelector);
const { getUrl } = useGetUrl();
@@ -66,54 +46,37 @@ const CurrentImagePreview = () => {
height: '100%',
}}
>
{imageToDisplay && (
{image && (
<Image
src={
shouldHidePreview
? undefined
: imageToDisplay.isProgressImage
? imageToDisplay.url
: getUrl(imageToDisplay.url)
}
width={imageToDisplay.width}
height={imageToDisplay.height}
fallback={
shouldHidePreview ? (
<CurrentImageHidden />
) : !imageToDisplay.isProgressImage ? (
<CurrentImageFallback />
) : undefined
}
src={shouldHidePreview ? undefined : getUrl(image.url)}
width={image.metadata.width}
height={image.metadata.height}
fallback={shouldHidePreview ? <CurrentImageHidden /> : undefined}
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
imageRendering: imageToDisplay.isProgressImage
? 'pixelated'
: 'initial',
borderRadius: 'base',
}}
/>
)}
{shouldShowImageDetails && image && 'metadata' in image && (
<Box
sx={{
position: 'absolute',
top: '0',
width: '100%',
height: '100%',
borderRadius: 'base',
overflow: 'scroll',
}}
>
<ImageMetadataViewer image={image} />
</Box>
)}
{!shouldShowImageDetails && <NextPrevImageButtons />}
{shouldShowImageDetails &&
imageToDisplay &&
'metadata' in imageToDisplay.image && (
<Box
sx={{
position: 'absolute',
top: '0',
width: '100%',
height: '100%',
borderRadius: 'base',
overflow: 'scroll',
}}
>
<ImageMetadataViewer image={imageToDisplay.image} />
</Box>
)}
</Flex>
);
};

View File

@@ -5,65 +5,37 @@ import {
Image,
MenuItem,
MenuList,
Text,
useDisclosure,
useTheme,
useToast,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
imageSelected,
setCurrentImage,
} from 'features/gallery/store/gallerySlice';
import {
initialImageSelected,
setAllImageToImageParameters,
setAllParameters,
setSeed,
} from 'features/parameters/store/generationSlice';
import { DragEvent, memo, useState } from 'react';
import {
FaCheck,
FaExpand,
FaLink,
FaShare,
FaTrash,
FaTrashAlt,
} from 'react-icons/fa';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { DragEvent, memo, useCallback, useState } from 'react';
import { FaCheck, FaExpand, FaShare, FaTrash } from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal';
import { ContextMenu } from 'chakra-ui-contextmenu';
import * as InvokeAI from 'app/types/invokeai';
import {
resizeAndScaleCanvas,
setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice';
import { resizeAndScaleCanvas } from 'features/canvas/store/canvasSlice';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useTranslation } from 'react-i18next';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import IAIIconButton from 'common/components/IAIIconButton';
import { useGetUrl } from 'common/util/getUrl';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { BiZoomIn } from 'react-icons/bi';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { imageDeleted } from 'services/thunks/image';
import { createSelector } from '@reduxjs/toolkit';
import { systemSelector } from 'features/system/store/systemSelectors';
import { configSelector } from 'features/system/store/configSelectors';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash-es';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useParameters } from 'features/parameters/hooks/useParameters';
export const selector = createSelector(
[
gallerySelector,
systemSelector,
configSelector,
lightboxSelector,
activeTabNameSelector,
],
(gallery, system, config, lightbox, activeTabName) => {
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
(gallery, system, lightbox, activeTabName) => {
const {
galleryImageObjectFit,
galleryImageMinimumWidth,
@@ -71,7 +43,6 @@ export const selector = createSelector(
} = gallery;
const { isLightboxOpen } = lightbox;
const { disabledFeatures } = config;
const { isConnected, isProcessing, shouldConfirmOnDelete } = system;
return {
@@ -82,7 +53,6 @@ export const selector = createSelector(
shouldUseSingleGalleryColumn,
activeTabName,
isLightboxOpen,
disabledFeatures,
};
},
{
@@ -113,14 +83,15 @@ const HoverableImage = memo((props: HoverableImageProps) => {
galleryImageMinimumWidth,
canDeleteImage,
shouldUseSingleGalleryColumn,
disabledFeatures,
shouldConfirmOnDelete,
} = useAppSelector(selector);
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const { image, isSelected } = props;
const { url, thumbnail, name, metadata } = image;
const { getUrl } = useGetUrl();
@@ -130,53 +101,62 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const toast = useToast();
const { direction } = useTheme();
const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts();
const { isFeatureEnabled: isLightboxEnabled } = useFeatureStatus('lightbox');
const { recallSeed, recallPrompt, sendToImageToImage, recallInitialImage } =
useParameters();
const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false);
const handleInitiateDelete = () => {
// Immediately deletes an image
const handleDelete = useCallback(() => {
if (canDeleteImage && image) {
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
}
}, [dispatch, image, canDeleteImage]);
// Opens the alert dialog to check if user is sure they want to delete
const handleInitiateDelete = useCallback(() => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
};
}, [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]);
const handleDelete = () => {
if (canDeleteImage && image) {
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
}
};
const handleSelectImage = useCallback(() => {
dispatch(imageSelected(image));
}, [image, dispatch]);
const handleUsePrompt = () => {
if (typeof image.metadata?.invokeai?.node?.prompt === 'string') {
setBothPrompts(image.metadata?.invokeai?.node?.prompt);
}
toast({
title: t('toast.promptSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
};
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move';
},
[image]
);
const handleUseSeed = () => {
typeof image.metadata.invokeai?.node?.seed === 'number' &&
dispatch(setSeed(image.metadata.invokeai?.node?.seed));
toast({
title: t('toast.seedSet'),
status: 'success',
duration: 2500,
isClosable: true,
});
};
// Recall parameters handlers
const handleRecallPrompt = useCallback(() => {
recallPrompt(image.metadata?.invokeai?.node?.prompt);
}, [image, recallPrompt]);
const handleSendToImageToImage = () => {
dispatch(initialImageSelected(image.name));
};
const handleRecallSeed = useCallback(() => {
recallSeed(image.metadata.invokeai?.node?.seed);
}, [image, recallSeed]);
const handleSendToImageToImage = useCallback(() => {
sendToImageToImage(image);
}, [image, sendToImageToImage]);
const handleRecallInitialImage = useCallback(() => {
recallInitialImage(image.metadata.invokeai?.node?.image);
}, [image, recallInitialImage]);
/**
* TODO: the rest of these
*/
const handleSendToCanvas = () => {
// dispatch(setInitialCanvasImage(image));
@@ -205,41 +185,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
// });
};
const handleUseInitialImage = async () => {
// if (metadata.invokeai?.node?.image?.init_image_path) {
// const response = await fetch(
// metadata.invokeai?.node?.image?.init_image_path
// );
// if (response.ok) {
// dispatch(setAllImageToImageParameters(metadata?.invokeai?.node));
// toast({
// title: t('toast.initialImageSet'),
// status: 'success',
// duration: 2500,
// isClosable: true,
// });
// return;
// }
// }
// toast({
// title: t('toast.initialImageNotSet'),
// description: t('toast.initialImageNotSetDesc'),
// status: 'error',
// duration: 2500,
// isClosable: true,
// });
};
const handleSelectImage = () => {
dispatch(imageSelected(image.name));
};
const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move';
};
const handleLightBox = () => {
// dispatch(setCurrentImage(image));
// dispatch(setIsLightboxOpen(true));
@@ -254,21 +199,21 @@ const HoverableImage = memo((props: HoverableImageProps) => {
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList>
<MenuList sx={{ visibility: 'visible !important' }}>
<MenuItem
icon={<ExternalLinkIcon />}
onClickCapture={handleOpenInNewTab}
>
{t('common.openInNewTab')}
</MenuItem>
{!disabledFeatures.includes('lightbox') && (
{isLightboxEnabled && (
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
)}
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUsePrompt}
onClickCapture={handleRecallPrompt}
isDisabled={image?.metadata?.invokeai?.node?.prompt === undefined}
>
{t('parameters.usePrompt')}
@@ -276,14 +221,14 @@ const HoverableImage = memo((props: HoverableImageProps) => {
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseSeed}
onClickCapture={handleRecallSeed}
isDisabled={image?.metadata?.invokeai?.node?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseInitialImage}
onClickCapture={handleRecallInitialImage}
isDisabled={image?.metadata?.invokeai?.node?.type !== 'img2img'}
>
{t('parameters.useInitImg')}

View File

@@ -1,5 +1,5 @@
import { ButtonGroup, Flex, Grid, Icon, Image, Text } from '@chakra-ui/react';
import { requestImages } from 'app/socketio/actions';
// import { requestImages } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAICheckbox from 'common/components/IAICheckbox';
@@ -49,7 +49,7 @@ const gallerySelector = createSelector(
(uploads, results, gallery) => {
const { currentCategory } = gallery;
return currentCategory === 'result'
return currentCategory === 'results'
? {
images: resultsAdapter.getSelectors().selectAll(results),
isLoading: results.isLoading,
@@ -72,7 +72,6 @@ const ImageGalleryContent = () => {
const {
// images,
currentCategory,
currentImageUuid,
shouldPinGallery,
galleryImageMinimumWidth,
galleryGridTemplateColumns,
@@ -80,6 +79,7 @@ const ImageGalleryContent = () => {
shouldAutoSwitchToNewImages,
// areMoreImagesAvailable,
shouldUseSingleGalleryColumn,
selectedImage,
} = useAppSelector(imageGallerySelector);
const { images, areMoreImagesAvailable, isLoading } =
@@ -89,11 +89,11 @@ const ImageGalleryContent = () => {
// dispatch(requestImages(currentCategory));
// };
const handleClickLoadMore = () => {
if (currentCategory === 'result') {
if (currentCategory === 'results') {
dispatch(receivedResultImagesPage());
}
if (currentCategory === 'user') {
if (currentCategory === 'uploads') {
dispatch(receivedUploadImagesPage());
}
};
@@ -147,34 +147,34 @@ const ImageGalleryContent = () => {
<IAIIconButton
aria-label={t('gallery.showGenerations')}
tooltip={t('gallery.showGenerations')}
isChecked={currentCategory === 'result'}
isChecked={currentCategory === 'results'}
role="radio"
icon={<FaImage />}
onClick={() => dispatch(setCurrentCategory('result'))}
onClick={() => dispatch(setCurrentCategory('results'))}
/>
<IAIIconButton
aria-label={t('gallery.showUploads')}
tooltip={t('gallery.showUploads')}
role="radio"
isChecked={currentCategory === 'user'}
isChecked={currentCategory === 'uploads'}
icon={<FaUser />}
onClick={() => dispatch(setCurrentCategory('user'))}
onClick={() => dispatch(setCurrentCategory('uploads'))}
/>
</>
) : (
<>
<IAIButton
size="sm"
isChecked={currentCategory === 'result'}
onClick={() => dispatch(setCurrentCategory('result'))}
isChecked={currentCategory === 'results'}
onClick={() => dispatch(setCurrentCategory('results'))}
flexGrow={1}
>
{t('gallery.generations')}
</IAIButton>
<IAIButton
size="sm"
isChecked={currentCategory === 'user'}
onClick={() => dispatch(setCurrentCategory('user'))}
isChecked={currentCategory === 'uploads'}
onClick={() => dispatch(setCurrentCategory('uploads'))}
flexGrow={1}
>
{t('gallery.uploads')}
@@ -251,7 +251,7 @@ const ImageGalleryContent = () => {
>
{images.map((image) => {
const { name } = image;
const isSelected = currentImageUuid === name;
const isSelected = selectedImage?.name === name;
return (
<HoverableImage
key={`${name}-${image.thumbnail}`}

View File

@@ -1,8 +1,8 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import {
selectNextImage,
selectPrevImage,
// selectNextImage,
// selectPrevImage,
setGalleryImageMinimumWidth,
} from 'features/gallery/store/gallerySlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
@@ -110,28 +110,6 @@ export const ImageGalleryPanel = () => {
[shouldPinGallery]
);
useHotkeys(
'left',
() => {
dispatch(selectPrevImage());
},
{
enabled: !isStaging || activeTabName !== 'unifiedCanvas',
},
[isStaging, activeTabName]
);
useHotkeys(
'right',
() => {
dispatch(selectNextImage());
},
{
enabled: !isStaging || activeTabName !== 'unifiedCanvas',
},
[isStaging, activeTabName]
);
useHotkeys(
'shift+g',
() => {

View File

@@ -159,6 +159,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
_dark: {
bg: 'blackAlpha.600',
},
overflow: 'scroll',
}}
>
<Flex gap={2}>

View File

@@ -1,16 +1,14 @@
import { ChakraProps, Flex, Grid, IconButton } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isEqual } from 'lodash-es';
import { useState } from 'react';
import { clamp, isEqual } from 'lodash-es';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { gallerySelector } from '../store/gallerySelectors';
import {
GalleryCategory,
selectNextImage,
selectPrevImage,
} from '../store/gallerySlice';
import { RootState } from 'app/store/store';
import { imageSelected } from '../store/gallerySlice';
import { useHotkeys } from 'react-hotkeys-hook';
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
height: '100%',
@@ -23,24 +21,47 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
};
export const nextPrevImageButtonsSelector = createSelector(
gallerySelector,
(gallery) => {
const { currentImage } = gallery;
[(state: RootState) => state, gallerySelector],
(state, gallery) => {
const { selectedImage, currentCategory } = gallery;
const tempImages =
gallery.categories[
currentImage ? (currentImage.category as GalleryCategory) : 'result'
].images;
if (!selectedImage) {
return {
isOnFirstImage: true,
isOnLastImage: true,
};
}
const currentImageIndex = tempImages.findIndex(
(i) => i.uuid === gallery?.currentImage?.uuid
const currentImageIndex = state[currentCategory].ids.findIndex(
(i) => i === selectedImage.name
);
const imagesLength = tempImages.length;
const nextImageIndex = clamp(
currentImageIndex + 1,
0,
state[currentCategory].ids.length - 1
);
const prevImageIndex = clamp(
currentImageIndex - 1,
0,
state[currentCategory].ids.length - 1
);
const nextImageId = state[currentCategory].ids[nextImageIndex];
const prevImageId = state[currentCategory].ids[prevImageIndex];
const nextImage = state[currentCategory].entities[nextImageId];
const prevImage = state[currentCategory].entities[prevImageId];
const imagesLength = state[currentCategory].ids.length;
return {
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
nextImage,
prevImage,
};
},
{
@@ -54,34 +75,48 @@ const NextPrevImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { isOnFirstImage, isOnLastImage } = useAppSelector(
nextPrevImageButtonsSelector
);
const { isOnFirstImage, isOnLastImage, nextImage, prevImage } =
useAppSelector(nextPrevImageButtonsSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const handleCurrentImagePreviewMouseOver = () => {
const handleCurrentImagePreviewMouseOver = useCallback(() => {
setShouldShowNextPrevButtons(true);
};
}, []);
const handleCurrentImagePreviewMouseOut = () => {
const handleCurrentImagePreviewMouseOut = useCallback(() => {
setShouldShowNextPrevButtons(false);
};
}, []);
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handlePrevImage = useCallback(() => {
dispatch(imageSelected(prevImage));
}, [dispatch, prevImage]);
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
const handleNextImage = useCallback(() => {
dispatch(imageSelected(nextImage));
}, [dispatch, nextImage]);
useHotkeys(
'left',
() => {
handlePrevImage();
},
[prevImage]
);
useHotkeys(
'right',
() => {
handleNextImage();
},
[nextImage]
);
return (
<Flex
sx={{
justifyContent: 'space-between',
zIndex: 1,
height: '100%',
width: '100%',
pointerEvents: 'none',
@@ -100,7 +135,7 @@ const NextPrevImageButtons = () => {
aria-label={t('accessibility.previousImage')}
icon={<FaAngleLeft size={64} />}
variant="unstyled"
onClick={handleClickPrevButton}
onClick={handlePrevImage}
boxSize={16}
sx={nextPrevButtonStyles}
/>
@@ -119,7 +154,7 @@ const NextPrevImageButtons = () => {
aria-label={t('accessibility.nextImage')}
icon={<FaAngleRight size={64} />}
variant="unstyled"
onClick={handleClickNextButton}
onClick={handleNextImage}
boxSize={16}
sx={nextPrevButtonStyles}
/>

View File

@@ -22,17 +22,22 @@ import {
export const gallerySelector = (state: RootState) => state.gallery;
export const imageGallerySelector = createSelector(
[gallerySelector, uiSelector, lightboxSelector, activeTabNameSelector],
(gallery, ui, lightbox, activeTabName) => {
[
(state: RootState) => state,
gallerySelector,
uiSelector,
lightboxSelector,
activeTabNameSelector,
],
(state, gallery, ui, lightbox, activeTabName) => {
const {
categories,
currentCategory,
currentImageUuid,
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
galleryWidth,
shouldUseSingleGalleryColumn,
selectedImage,
} = gallery;
const { shouldPinGallery } = ui;
@@ -40,7 +45,6 @@ export const imageGallerySelector = createSelector(
const { isLightboxOpen } = lightbox;
return {
currentImageUuid,
shouldPinGallery,
galleryImageMinimumWidth,
galleryImageObjectFit,
@@ -49,9 +53,7 @@ export const imageGallerySelector = createSelector(
: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
shouldAutoSwitchToNewImages,
currentCategory,
images: categories[currentCategory].images,
areMoreImagesAvailable:
categories[currentCategory].areMoreImagesAvailable,
images: state[currentCategory].entities,
galleryWidth,
shouldEnableResize:
isLightboxOpen ||
@@ -59,6 +61,7 @@ export const imageGallerySelector = createSelector(
? false
: true,
shouldUseSingleGalleryColumn,
selectedImage,
};
},
{
@@ -69,16 +72,16 @@ export const imageGallerySelector = createSelector(
);
export const selectedImageSelector = createSelector(
[gallerySelector, selectResultsEntities, selectUploadsEntities],
(gallery, allResults, allUploads) => {
const selectedImageName = gallery.selectedImageName;
[(state: RootState) => state, gallerySelector],
(state, gallery) => {
const selectedImage = gallery.selectedImage;
if (selectedImageName in allResults) {
return allResults[selectedImageName];
if (selectedImage?.type === 'results') {
return selectResultsById(state, selectedImage.name);
}
if (selectedImageName in allUploads) {
return allUploads[selectedImageName];
if (selectedImage?.type === 'uploads') {
return selectUploadsById(state, selectedImage.name);
}
}
);

View File

@@ -1,259 +1,45 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import { invocationComplete } from 'services/events/actions';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { clamp } from 'lodash-es';
import { isImageOutput } from 'services/types/guards';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageUploaded } from 'services/thunks/image';
export type GalleryCategory = 'user' | 'result';
export type AddImagesPayload = {
images: Array<InvokeAI._Image>;
areMoreImagesAvailable: boolean;
category: GalleryCategory;
};
import { SelectedImage } from 'features/parameters/store/generationSlice';
type GalleryImageObjectFitType = 'contain' | 'cover';
export type Gallery = {
images: InvokeAI._Image[];
latest_mtime?: number;
earliest_mtime?: number;
areMoreImagesAvailable: boolean;
};
export interface GalleryState {
/**
* The selected image's unique name
* Use `selectedImageSelector` to access the image
* The selected image
*/
selectedImageName: string;
/**
* The currently selected image
* @deprecated See `state.gallery.selectedImageName`
*/
currentImage?: InvokeAI._Image;
/**
* The currently selected image's uuid.
* @deprecated See `state.gallery.selectedImageName`, use `selectedImageSelector` to access the image
*/
currentImageUuid: string;
/**
* The current progress image
* @deprecated See `state.system.progressImage`
*/
intermediateImage?: InvokeAI._Image & {
boundingBox?: IRect;
generationMode?: InvokeTabName;
};
selectedImage?: SelectedImage;
galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean;
categories: {
user: Gallery;
result: Gallery;
};
currentCategory: GalleryCategory;
galleryWidth: number;
shouldUseSingleGalleryColumn: boolean;
currentCategory: 'results' | 'uploads';
}
const initialState: GalleryState = {
selectedImageName: '',
currentImageUuid: '',
selectedImage: undefined,
galleryImageMinimumWidth: 64,
galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true,
currentCategory: 'result',
categories: {
user: {
images: [],
latest_mtime: undefined,
earliest_mtime: undefined,
areMoreImagesAvailable: true,
},
result: {
images: [],
latest_mtime: undefined,
earliest_mtime: undefined,
areMoreImagesAvailable: true,
},
},
galleryWidth: 300,
shouldUseSingleGalleryColumn: false,
currentCategory: 'results',
};
export const gallerySlice = createSlice({
name: 'gallery',
initialState,
reducers: {
imageSelected: (state, action: PayloadAction<string>) => {
state.selectedImageName = action.payload;
},
setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
state.currentImage = action.payload;
state.currentImageUuid = action.payload.uuid;
},
removeImage: (
imageSelected: (
state,
action: PayloadAction<InvokeAI.ImageDeletedResponse>
action: PayloadAction<SelectedImage | undefined>
) => {
const { uuid, category } = action.payload;
const tempImages = state.categories[category as GalleryCategory].images;
const newImages = tempImages.filter((image) => image.uuid !== uuid);
if (uuid === state.currentImageUuid) {
/**
* We are deleting the currently selected image.
*
* We want the new currentl selected image to be under the cursor in the
* gallery, so we need to do some fanagling. The currently selected image
* is set by its UUID, not its index in the image list.
*
* Get the currently selected image's index.
*/
const imageToDeleteIndex = tempImages.findIndex(
(image) => image.uuid === uuid
);
/**
* New current image needs to be in the same spot, but because the gallery
* is sorted in reverse order, the new current image's index will actuall be
* one less than the deleted image's index.
*
* Clamp the new index to ensure it is valid..
*/
const newCurrentImageIndex = clamp(
imageToDeleteIndex,
0,
newImages.length - 1
);
state.currentImage = newImages.length
? newImages[newCurrentImageIndex]
: undefined;
state.currentImageUuid = newImages.length
? newImages[newCurrentImageIndex].uuid
: '';
}
state.categories[category as GalleryCategory].images = newImages;
},
addImage: (
state,
action: PayloadAction<{
image: InvokeAI._Image;
category: GalleryCategory;
}>
) => {
const { image: newImage, category } = action.payload;
const { uuid, url, mtime } = newImage;
const tempCategory = state.categories[category as GalleryCategory];
// Do not add duplicate images
if (tempCategory.images.find((i) => i.url === url && i.mtime === mtime)) {
return;
}
tempCategory.images.unshift(newImage);
if (state.shouldAutoSwitchToNewImages) {
state.currentImageUuid = uuid;
state.currentImage = newImage;
state.currentCategory = category;
}
state.intermediateImage = undefined;
tempCategory.latest_mtime = mtime;
},
setIntermediateImage: (
state,
action: PayloadAction<
InvokeAI._Image & {
boundingBox?: IRect;
generationMode?: InvokeTabName;
}
>
) => {
state.intermediateImage = action.payload;
},
clearIntermediateImage: (state) => {
state.intermediateImage = undefined;
},
selectNextImage: (state) => {
const { currentImage } = state;
if (!currentImage) return;
const tempImages =
state.categories[currentImage.category as GalleryCategory].images;
if (currentImage) {
const currentImageIndex = tempImages.findIndex(
(i) => i.uuid === currentImage.uuid
);
if (currentImageIndex < tempImages.length - 1) {
const newCurrentImage = tempImages[currentImageIndex + 1];
state.currentImage = newCurrentImage;
state.currentImageUuid = newCurrentImage.uuid;
}
}
},
selectPrevImage: (state) => {
const { currentImage } = state;
if (!currentImage) return;
const tempImages =
state.categories[currentImage.category as GalleryCategory].images;
if (currentImage) {
const currentImageIndex = tempImages.findIndex(
(i) => i.uuid === currentImage.uuid
);
if (currentImageIndex > 0) {
const newCurrentImage = tempImages[currentImageIndex - 1];
state.currentImage = newCurrentImage;
state.currentImageUuid = newCurrentImage.uuid;
}
}
},
addGalleryImages: (state, action: PayloadAction<AddImagesPayload>) => {
const { images, areMoreImagesAvailable, category } = action.payload;
const tempImages = state.categories[category].images;
// const prevImages = category === 'user' ? state.userImages : state.resultImages
if (images.length > 0) {
// Filter images that already exist in the gallery
const newImages = images.filter(
(newImage) =>
!tempImages.find(
(i) => i.url === newImage.url && i.mtime === newImage.mtime
)
);
state.categories[category].images = tempImages
.concat(newImages)
.sort((a, b) => b.mtime - a.mtime);
if (!state.currentImage) {
const newCurrentImage = images[0];
state.currentImage = newCurrentImage;
state.currentImageUuid = newCurrentImage.uuid;
}
// keep track of the timestamps of latest and earliest images received
state.categories[category].latest_mtime = images[0].mtime;
state.categories[category].earliest_mtime =
images[images.length - 1].mtime;
}
if (areMoreImagesAvailable !== undefined) {
state.categories[category].areMoreImagesAvailable =
areMoreImagesAvailable;
}
state.selectedImage = action.payload;
},
setGalleryImageMinimumWidth: (state, action: PayloadAction<number>) => {
state.galleryImageMinimumWidth = action.payload;
@@ -267,7 +53,10 @@ export const gallerySlice = createSlice({
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
state.shouldAutoSwitchToNewImages = action.payload;
},
setCurrentCategory: (state, action: PayloadAction<GalleryCategory>) => {
setCurrentCategory: (
state,
action: PayloadAction<'results' | 'uploads'>
) => {
state.currentCategory = action.payload;
},
setGalleryWidth: (state, action: PayloadAction<number>) => {
@@ -286,9 +75,11 @@ export const gallerySlice = createSlice({
*/
builder.addCase(invocationComplete, (state, action) => {
const { data } = action.payload;
if (isImageOutput(data.result)) {
state.selectedImageName = data.result.image.image_name;
state.intermediateImage = undefined;
if (isImageOutput(data.result) && state.shouldAutoSwitchToNewImages) {
state.selectedImage = {
name: data.result.image.image_name,
type: 'results',
};
}
});
@@ -299,27 +90,19 @@ export const gallerySlice = createSlice({
const { response } = action.payload;
const uploadedImage = deserializeImageResponse(response);
state.selectedImageName = uploadedImage.name;
state.selectedImage = { name: uploadedImage.name, type: 'uploads' };
});
},
});
export const {
imageSelected,
addImage,
clearIntermediateImage,
removeImage,
setCurrentImage,
addGalleryImages,
setIntermediateImage,
selectNextImage,
selectPrevImage,
setGalleryImageMinimumWidth,
setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages,
setCurrentCategory,
setGalleryWidth,
setShouldUseSingleGalleryColumn,
setCurrentCategory,
} = gallerySlice.actions;
export default gallerySlice.reducer;

View File

@@ -5,7 +5,7 @@ import { ResultsState } from './resultsSlice';
*
* Currently denylisting results slice entirely, see persist config in store.ts
*/
const itemsToDenylist: (keyof ResultsState)[] = [];
const itemsToDenylist: (keyof ResultsState)[] = ['isLoading'];
export const resultsDenylist = itemsToDenylist.map(
(denylistItem) => `results.${denylistItem}`

View File

@@ -65,7 +65,7 @@ const resultsSlice = createSlice({
deserializeImageResponse(image)
);
resultsAdapter.addMany(state, resultImages);
resultsAdapter.setMany(state, resultImages);
state.page = page;
state.pages = pages;
@@ -107,7 +107,7 @@ const resultsSlice = createSlice({
},
};
resultsAdapter.addOne(state, image);
resultsAdapter.setOne(state, image);
}
});

View File

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

View File

@@ -53,7 +53,7 @@ const uploadsSlice = createSlice({
const images = items.map((image) => deserializeImageResponse(image));
uploadsAdapter.addMany(state, images);
uploadsAdapter.setMany(state, images);
state.page = page;
state.pages = pages;
@@ -69,7 +69,7 @@ const uploadsSlice = createSlice({
const uploadedImage = deserializeImageResponse(response);
uploadsAdapter.addOne(state, uploadedImage);
uploadsAdapter.setOne(state, uploadedImage);
});
/**

View File

@@ -16,6 +16,8 @@ import { receivedOpenAPISchema } from 'services/thunks/schema';
import { isFulfilledAnyGraphBuilt } from 'services/thunks/session';
import { InvocationTemplate, InvocationValue } from '../types/types';
import { parseSchema } from '../util/parseSchema';
import { log } from 'app/logging/useLogger';
import { size } from 'lodash-es';
export type NodesState = {
nodes: Node<InvocationValue>[];
@@ -85,7 +87,12 @@ const nodesSlice = createSlice({
parsedOpenAPISchema: (state, action: PayloadAction<OpenAPIV3.Document>) => {
try {
const parsedSchema = parseSchema(action.payload);
console.debug('Parsed schema: ', parsedSchema);
// TODO: Achtung! Side effect in a reducer!
log.info(
{ namespace: 'schema', nodes: parsedSchema },
`Parsed ${size(parsedSchema)} nodes`
);
state.invocationTemplates = parsedSchema;
} catch (err) {
console.error(err);

View File

@@ -5,7 +5,6 @@ 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 { selectResultsById } from 'features/gallery/store/resultsSlice';
import {
clearInitialImage,
initialImageSelected,
@@ -16,15 +15,13 @@ 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';
const initialImagePreviewSelector = createSelector(
[(state: RootState) => state],
(state) => {
const { initialImage } = state.generation;
const image = selectResultsById(state, initialImage as string);
const selector = createSelector(
[initialImageSelector],
(initialImage) => {
return {
initialImage: image,
initialImage,
};
},
{ memoizeOptions: { resultEqualityCheck: isEqual } }
@@ -34,7 +31,7 @@ const InitialImagePreview = () => {
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const { initialImage } = useAppSelector(initialImagePreviewSelector);
const { initialImage } = useAppSelector(selector);
const { getUrl } = useGetUrl();
const dispatch = useAppDispatch();
const { t } = useTranslation();
@@ -71,7 +68,7 @@ const InitialImagePreview = () => {
return;
}
dispatch(initialImageSelected(image.name));
dispatch(initialImageSelected({ name, type }));
},
[getImageByNameAndType, dispatch]
);
@@ -116,12 +113,7 @@ const InitialImagePreview = () => {
</Flex>
}
/>
{isLoaded && (
<ImageToImageOverlay
setIsLoaded={setIsLoaded}
image={initialImage}
/>
)}
{isLoaded && <ImageToImageOverlay image={initialImage} />}
</Box>
)}
{!initialImage?.url && <SelectImagePlaceholder />}

View File

@@ -15,9 +15,9 @@ const AnimatedImageToImagePanel = () => {
<AnimatePresence>
{isImageToImageEnabled && (
<motion.div
initial={{ opacity: 0, scaleX: 0, width: 0 }}
animate={{ opacity: 1, scaleX: 1, width: '28rem' }}
exit={{ opacity: 0, scaleX: 0, width: 0 }}
initial={{ opacity: 0, scale: 0, width: 0 }}
animate={{ opacity: 1, scale: 1, width: '28rem' }}
exit={{ opacity: 0, scale: 0, width: 0 }}
transition={{ type: 'spring', bounce: 0, duration: 0.35 }}
>
<Box sx={{ h: 'full', w: 'full', pl: 4 }}>

View File

@@ -8,7 +8,7 @@ import {
SystemState,
cancelScheduled,
cancelTypeChanged,
CancelType,
CancelStrategy,
} from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es';
import { useCallback, memo } from 'react';
@@ -87,7 +87,7 @@ const CancelButton = (
const handleCancelTypeChanged = useCallback(
(value: string | string[]) => {
const newCancelType = Array.isArray(value) ? value[0] : value;
dispatch(cancelTypeChanged(newCancelType as CancelType));
dispatch(cancelTypeChanged(newCancelType as CancelStrategy));
},
[dispatch]
);

View File

@@ -1,6 +1,5 @@
import { Box } from '@chakra-ui/react';
import { readinessSelector } from 'app/selectors/readinessSelector';
import { generateImage } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
import IAIIconButton, {
@@ -8,10 +7,11 @@ import IAIIconButton, {
} from 'common/components/IAIIconButton';
import { clampSymmetrySteps } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaPlay } from 'react-icons/fa';
import { generateGraphBuilt, sessionCreated } from 'services/thunks/session';
import { generateGraphBuilt } from 'services/thunks/session';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
@@ -24,19 +24,16 @@ export default function InvokeButton(props: InvokeButton) {
const { isReady } = useAppSelector(readinessSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const handleClickGenerate = () => {
// dispatch(generateImage(activeTabName));
const handleInvoke = useCallback(() => {
dispatch(clampSymmetrySteps());
dispatch(generateGraphBuilt());
};
}, [dispatch]);
const { t } = useTranslation();
useHotkeys(
['ctrl+enter', 'meta+enter'],
() => {
dispatch(clampSymmetrySteps());
dispatch(generateImage(activeTabName));
},
handleInvoke,
{
enabled: () => isReady,
preventDefault: true,
@@ -53,7 +50,7 @@ export default function InvokeButton(props: InvokeButton) {
type="submit"
icon={<FaPlay />}
isDisabled={!isReady}
onClick={handleClickGenerate}
onClick={handleInvoke}
flexGrow={1}
w="100%"
tooltip={t('parameters.invoke')}
@@ -66,7 +63,7 @@ export default function InvokeButton(props: InvokeButton) {
aria-label={t('parameters.invoke')}
type="submit"
isDisabled={!isReady}
onClick={handleClickGenerate}
onClick={handleInvoke}
flexGrow={1}
w="100%"
colorScheme="accent"

View File

@@ -0,0 +1,67 @@
import { Flex, Icon, Image } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { isEqual } from 'lodash-es';
import { memo } from 'react';
import { FaImage } from 'react-icons/fa';
const selector = createSelector(
[systemSelector],
(system) => {
const { progressImage } = system;
return {
progressImage,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const ProgressImage = () => {
const { progressImage } = useAppSelector(selector);
return progressImage ? (
<Flex
sx={{
position: 'relative',
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Image
draggable={false}
src={progressImage.dataURL}
width={progressImage.width}
height={progressImage.height}
sx={{
position: 'absolute',
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
borderRadius: 'base',
p: 2,
}}
/>
</Flex>
) : (
<Flex
sx={{
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Icon color="base.400" boxSize={32} as={FaImage} />
</Flex>
);
};
export default memo(ProgressImage);

View File

@@ -0,0 +1,167 @@
import { Flex, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { memo } from 'react';
import { FaStopwatch } from 'react-icons/fa';
import { uiSelector } from 'features/ui/store/uiSelectors';
import IAIIconButton from 'common/components/IAIIconButton';
import { CloseIcon } from '@chakra-ui/icons';
import { useTranslation } from 'react-i18next';
import {
floatingProgressImageMoved,
floatingProgressImageResized,
setShouldShowProgressImages,
} from 'features/ui/store/uiSlice';
import { Rnd } from 'react-rnd';
import { Rect } from 'features/ui/store/uiTypes';
import { isEqual } from 'lodash';
import ProgressImage from './ProgressImage';
const selector = createSelector(
[systemSelector, uiSelector],
(system, ui) => {
const { isProcessing } = system;
const {
floatingProgressImageRect,
shouldShowProgressImages,
shouldAutoShowProgressImages,
} = ui;
const showProgressWindow =
shouldAutoShowProgressImages && isProcessing
? true
: shouldShowProgressImages;
return {
floatingProgressImageRect,
showProgressWindow,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const ProgressImagePreview = () => {
const dispatch = useAppDispatch();
const { showProgressWindow, floatingProgressImageRect } =
useAppSelector(selector);
const { t } = useTranslation();
return (
<>
{' '}
<IAIIconButton
onClick={() =>
dispatch(setShouldShowProgressImages(!showProgressWindow))
}
tooltip={t('ui.showProgressImages')}
isChecked={showProgressWindow}
sx={{
position: 'absolute',
bottom: 4,
insetInlineStart: 4,
}}
aria-label={t('ui.showProgressImages')}
icon={<FaStopwatch />}
/>
{showProgressWindow && (
<Rnd
bounds="window"
minHeight={200}
minWidth={200}
size={{
width: floatingProgressImageRect.width,
height: floatingProgressImageRect.height,
}}
position={{
x: floatingProgressImageRect.x,
y: floatingProgressImageRect.y,
}}
onDragStop={(e, d) => {
dispatch(floatingProgressImageMoved({ x: d.x, y: d.y }));
}}
onResizeStop={(e, direction, ref, delta, position) => {
const newRect: Partial<Rect> = {};
console.log(
ref.style.width,
ref.style.height,
position.x,
position.y
);
if (ref.style.width) {
newRect.width = ref.style.width;
}
if (ref.style.height) {
newRect.height = ref.style.height;
}
if (position.x) {
newRect.x = position.x;
}
if (position.x) {
newRect.y = position.y;
}
dispatch(floatingProgressImageResized(newRect));
}}
>
<Flex
sx={{
position: 'relative',
w: 'full',
h: 'full',
alignItems: 'center',
justifyContent: 'center',
flexDir: 'column',
boxShadow: 'dark-lg',
bg: 'base.800',
borderRadius: 'base',
}}
>
<Flex
sx={{
w: 'full',
alignItems: 'center',
justifyContent: 'center',
p: 1.5,
pl: 4,
pr: 3,
bg: 'base.700',
borderTopRadius: 'base',
}}
>
<Flex
sx={{
flexGrow: 1,
userSelect: 'none',
cursor: 'move',
}}
>
<Text fontSize="sm" fontWeight={500}>
Progress Images
</Text>
</Flex>
<IAIIconButton
onClick={() => dispatch(setShouldShowProgressImages(false))}
aria-label={t('ui.hideProgressImages')}
size="xs"
icon={<CloseIcon />}
variant="ghost"
/>
</Flex>
<ProgressImage />
</Flex>
</Rnd>
)}
</>
);
};
export default memo(ProgressImagePreview);

View File

@@ -1,5 +1,4 @@
import { Box, FormControl, Textarea } from '@chakra-ui/react';
import { generateImage } from 'app/socketio/actions';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ChangeEvent, KeyboardEvent, useRef } from 'react';
@@ -8,6 +7,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { readinessSelector } from 'app/selectors/readinessSelector';
import {
GenerationState,
clampSymmetrySteps,
setPrompt,
} from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
@@ -15,6 +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 { generateGraphBuilt } from 'services/thunks/session';
const promptInputSelector = createSelector(
[(state: RootState) => state.generation, activeTabNameSelector],
@@ -36,7 +37,7 @@ const promptInputSelector = createSelector(
*/
const PromptInput = () => {
const dispatch = useAppDispatch();
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
const { prompt } = useAppSelector(promptInputSelector);
const { isReady } = useAppSelector(readinessSelector);
const promptRef = useRef<HTMLTextAreaElement>(null);
@@ -58,7 +59,8 @@ const PromptInput = () => {
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
e.preventDefault();
dispatch(generateImage(activeTabName));
dispatch(clampSymmetrySteps());
dispatch(generateGraphBuilt());
}
};

View File

@@ -0,0 +1,129 @@
import { UseToastOptions, useToast } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
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 { isImage, isImageField } from 'services/types/guards';
import { NUMPY_RAND_MAX } from 'app/constants';
export const useParameters = () => {
const dispatch = useAppDispatch();
const toast = useToast();
const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts();
/**
* Sets prompt with toast
*/
const recallPrompt = useCallback(
(prompt: unknown) => {
if (!isString(prompt)) {
toast({
title: t('toast.promptNotSet'),
description: t('toast.promptNotSetDesc'),
status: 'warning',
duration: 2500,
isClosable: true,
});
return;
}
setBothPrompts(prompt);
toast({
title: t('toast.promptSet'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
[t, toast, setBothPrompts]
);
/**
* Sets seed with toast
*/
const recallSeed = useCallback(
(seed: unknown) => {
const s = Number(seed);
if (!isFinite(s) || (isFinite(s) && !(s >= 0 && s <= NUMPY_RAND_MAX))) {
toast({
title: t('toast.seedNotSet'),
description: t('toast.seedNotSetDesc'),
status: 'warning',
duration: 2500,
isClosable: true,
});
return;
}
dispatch(setSeed(s));
toast({
title: t('toast.seedSet'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
[t, toast, dispatch]
);
/**
* Sets initial image with toast
*/
const recallInitialImage = useCallback(
async (image: unknown) => {
if (!isImageField(image)) {
toast({
title: t('toast.initialImageNotSet'),
description: t('toast.initialImageNotSetDesc'),
status: 'warning',
duration: 2500,
isClosable: true,
});
return;
}
dispatch(
initialImageSelected({ name: image.image_name, type: image.image_type })
);
toast({
title: t('toast.initialImageSet'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
[t, toast, dispatch]
);
/**
* 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;
}
dispatch(initialImageSelected({ name: image.name, type: image.type }));
toast({
title: t('toast.sentToImageToImage'),
status: 'info',
duration: 2500,
isClosable: true,
});
},
[t, toast, dispatch]
);
return { recallPrompt, recallSeed, recallInitialImage, sendToImageToImage };
};

View File

@@ -1,10 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import {
selectResultsById,
selectResultsEntities,
} from 'features/gallery/store/resultsSlice';
import { selectResultsById } from 'features/gallery/store/resultsSlice';
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
import { isEqual } from 'lodash-es';
@@ -25,11 +21,14 @@ export const mayGenerateMultipleImagesSelector = createSelector(
export const initialImageSelector = createSelector(
[(state: RootState) => state, generationSelector],
(state, generation) => {
const { initialImage: initialImageName } = generation;
const { initialImage } = generation;
return (
selectResultsById(state, initialImageName as string) ??
selectUploadsById(state, initialImageName as string)
);
if (initialImage?.type === 'results') {
return selectResultsById(state, initialImage.name);
}
if (initialImage?.type === 'uploads') {
return selectUploadsById(state, initialImage.name);
}
}
);

View File

@@ -5,13 +5,19 @@ import { getPromptAndNegative } from 'common/util/getPromptAndNegative';
import promptToString from 'common/util/promptToString';
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?: InvokeAI._Image | string; // can be an Image or url
initialImage?: SelectedImage; // can be an Image or url
iterations: number;
maskPath: string;
perlin: number;
@@ -345,7 +351,7 @@ export const generationSlice = createSlice({
setVerticalSymmetrySteps: (state, action: PayloadAction<number>) => {
state.verticalSymmetrySteps = action.payload;
},
initialImageSelected: (state, action: PayloadAction<string>) => {
initialImageSelected: (state, action: PayloadAction<SelectedImage>) => {
state.initialImage = action.payload;
state.isImageToImageEnabled = true;
},

View File

@@ -1,4 +1,4 @@
import { emptyTempFolder } from 'app/socketio/actions';
// import { emptyTempFolder } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIAlertDialog from 'common/components/IAIAlertDialog';
import IAIButton from 'common/components/IAIButton';

View File

@@ -1,197 +0,0 @@
import { Flex, Text, Tooltip } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import {
errorSeen,
setShouldShowLogViewer,
SystemState,
} from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es';
import { Resizable } from 're-resizable';
import { memo, useLayoutEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
import { systemSelector } from '../store/systemSelectors';
const logSelector = createSelector(
systemSelector,
(system: SystemState) => system.log,
{
memoizeOptions: {
// We don't need a deep equality check for this selector.
resultEqualityCheck: (a, b) => a.length === b.length,
},
}
);
const consoleSelector = createSelector(
systemSelector,
(system: SystemState) => {
return {
shouldShowLogViewer: system.shouldShowLogViewer,
hasError: system.hasError,
wasErrorSeen: system.wasErrorSeen,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
/**
* Basic log viewer, floats on bottom of page.
*/
const Console = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const log = useAppSelector(logSelector);
const { shouldShowLogViewer, hasError, wasErrorSeen } =
useAppSelector(consoleSelector);
// Rudimentary autoscroll
const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true);
const viewerRef = useRef<HTMLDivElement>(null);
/**
* If autoscroll is on, scroll to the bottom when:
* - log updates
* - viewer is toggled
*
* Also scroll to the bottom whenever autoscroll is turned on.
*/
useLayoutEffect(() => {
if (viewerRef.current !== null && shouldAutoscroll) {
viewerRef.current.scrollTop = viewerRef.current.scrollHeight;
}
}, [shouldAutoscroll, log, shouldShowLogViewer]);
const handleClickLogViewerToggle = () => {
dispatch(errorSeen());
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
};
useHotkeys(
'`',
() => {
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
},
[shouldShowLogViewer]
);
useHotkeys('esc', () => {
dispatch(setShouldShowLogViewer(false));
});
const handleOnScroll = () => {
if (!viewerRef.current) return;
if (
shouldAutoscroll &&
viewerRef.current.scrollTop <
viewerRef.current.scrollHeight - viewerRef.current.clientHeight
) {
setShouldAutoscroll(false);
}
};
return (
<>
{shouldShowLogViewer && (
<Resizable
defaultSize={{
width: '100%',
height: 200,
}}
style={{
display: 'flex',
position: 'fixed',
insetInlineStart: 0,
bottom: 0,
zIndex: 1,
}}
maxHeight="90vh"
>
<Flex
sx={{
flexDirection: 'column',
width: '100vw',
overflow: 'auto',
direction: 'column',
fontFamily: 'monospace',
pt: 0,
pr: 4,
pb: 4,
pl: 12,
borderTopWidth: 5,
bg: 'base.850',
borderColor: 'base.700',
zIndex: 2,
}}
ref={viewerRef}
onScroll={handleOnScroll}
>
{log.map((entry, i) => {
const { timestamp, message, level } = entry;
const colorScheme = level === 'info' ? 'base' : level;
return (
<Flex
key={i}
sx={{
gap: 2,
color: `${colorScheme}.300`,
}}
>
<Text fontWeight="600">{timestamp}:</Text>
<Text wordBreak="break-all">{message}</Text>
</Flex>
);
})}
</Flex>
</Resizable>
)}
{shouldShowLogViewer && (
<Tooltip
hasArrow
label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}
>
<IAIIconButton
size="sm"
aria-label={t('accessibility.toggleAutoscroll')}
icon={<FaAngleDoubleDown />}
onClick={() => setShouldAutoscroll(!shouldAutoscroll)}
isChecked={shouldAutoscroll}
sx={{
position: 'fixed',
insetInlineStart: 2,
bottom: 12,
zIndex: 1,
}}
/>
</Tooltip>
)}
<Tooltip
hasArrow
label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}
>
<IAIIconButton
size="sm"
aria-label={t('accessibility.toggleLogViewer')}
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
onClick={handleClickLogViewerToggle}
sx={{
position: 'fixed',
insetInlineStart: 2,
bottom: 2,
zIndex: 1,
}}
colorScheme={hasError || !wasErrorSeen ? 'error' : 'base'}
/>
</Tooltip>
</>
);
};
export default memo(Console);

View File

@@ -17,7 +17,7 @@ import React from 'react';
import SearchModels from './SearchModels';
import { addNewModel } from 'app/socketio/actions';
// import { addNewModel } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';

View File

@@ -8,7 +8,7 @@ import {
VStack,
} from '@chakra-ui/react';
import { InvokeDiffusersModelConfigProps } from 'app/types/invokeai';
import { addNewModel } from 'app/socketio/actions';
// import { addNewModel } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIInput from 'common/components/IAIInput';

View File

@@ -17,7 +17,7 @@ import {
VStack,
} from '@chakra-ui/react';
import { addNewModel } from 'app/socketio/actions';
// import { addNewModel } from 'app/socketio/actions';
import { Field, Formik } from 'formik';
import { useTranslation } from 'react-i18next';

View File

@@ -9,7 +9,7 @@ import { systemSelector } from 'features/system/store/systemSelectors';
import { Flex, FormControl, FormLabel, Text, VStack } from '@chakra-ui/react';
import { addNewModel } from 'app/socketio/actions';
// import { addNewModel } from 'app/socketio/actions';
import { Field, Formik } from 'formik';
import { useTranslation } from 'react-i18next';

View File

@@ -13,7 +13,7 @@ import {
Tooltip,
useDisclosure,
} from '@chakra-ui/react';
import { mergeDiffusersModels } from 'app/socketio/actions';
// import { mergeDiffusersModels } from 'app/socketio/actions';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';

View File

@@ -7,7 +7,7 @@ import {
UnorderedList,
Tooltip,
} from '@chakra-ui/react';
import { convertToDiffusers } from 'app/socketio/actions';
// import { convertToDiffusers } from 'app/socketio/actions';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIAlertDialog from 'common/components/IAIAlertDialog';

View File

@@ -1,7 +1,7 @@
import { DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { Box, Button, Flex, Spacer, Text, Tooltip } from '@chakra-ui/react';
import { ModelStatus } from 'app/types/invokeai';
import { deleteModel, requestModelChange } from 'app/socketio/actions';
// import { deleteModel, requestModelChange } from 'app/socketio/actions';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIAlertDialog from 'common/components/IAIAlertDialog';

View File

@@ -20,7 +20,7 @@ import { useTranslation } from 'react-i18next';
import { FaSearch, FaTrash } from 'react-icons/fa';
import { addNewModel, searchForModels } from 'app/socketio/actions';
// import { addNewModel, searchForModels } from 'app/socketio/actions';
import {
setFoundModels,
setSearchFolder,

View File

@@ -1,7 +1,6 @@
import {
ChakraProps,
Flex,
Grid,
Heading,
Modal,
ModalBody,
@@ -14,57 +13,59 @@ import {
useDisclosure,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { IN_PROGRESS_IMAGE_TYPES } from 'app/constants';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISelect from 'common/components/IAISelect';
import IAISwitch from 'common/components/IAISwitch';
import { systemSelector } from 'features/system/store/systemSelectors';
import {
InProgressImageType,
consoleLogLevelChanged,
setEnableImageDebugging,
setSaveIntermediatesInterval,
setShouldConfirmOnDelete,
setShouldDisplayGuides,
setShouldDisplayInProgressType,
shouldLogToConsoleChanged,
SystemState,
} from 'features/system/store/systemSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import {
setShouldAutoShowProgressImages,
setShouldUseCanvasBetaLayout,
setShouldUseSliders,
} from 'features/ui/store/uiSlice';
import { UIState } from 'features/ui/store/uiTypes';
import { isEqual, map } from 'lodash-es';
import { isEqual } from 'lodash-es';
import { persistor } from 'app/store/persistor';
import { ChangeEvent, cloneElement, ReactElement } from 'react';
import { ChangeEvent, cloneElement, ReactElement, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { VALID_LOG_LEVELS } from 'app/logging/useLogger';
import { LogLevelName } from 'roarr';
const selector = createSelector(
[systemSelector, uiSelector],
(system: SystemState, ui: UIState) => {
const {
shouldDisplayInProgressType,
shouldConfirmOnDelete,
shouldDisplayGuides,
model_list,
saveIntermediatesInterval,
enableImageDebugging,
consoleLogLevel,
shouldLogToConsole,
} = system;
const { shouldUseCanvasBetaLayout, shouldUseSliders } = ui;
const {
shouldUseCanvasBetaLayout,
shouldUseSliders,
shouldAutoShowProgressImages,
} = ui;
return {
shouldDisplayInProgressType,
shouldConfirmOnDelete,
shouldDisplayGuides,
models: map(model_list, (_model, key) => key),
saveIntermediatesInterval,
enableImageDebugging,
shouldUseCanvasBetaLayout,
shouldUseSliders,
shouldAutoShowProgressImages,
consoleLogLevel,
shouldLogToConsole,
};
},
{
@@ -77,6 +78,7 @@ const modalSectionStyles: ChakraProps['sx'] = {
gap: 2,
p: 4,
bg: 'base.900',
borderRadius: 'base',
};
type SettingsModalProps = {
@@ -94,8 +96,6 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const steps = useAppSelector((state: RootState) => state.generation.steps);
const {
isOpen: isSettingsModalOpen,
onOpen: onSettingsModalOpen,
@@ -109,31 +109,40 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
} = useDisclosure();
const {
shouldDisplayInProgressType,
shouldConfirmOnDelete,
shouldDisplayGuides,
saveIntermediatesInterval,
enableImageDebugging,
shouldUseCanvasBetaLayout,
shouldUseSliders,
shouldAutoShowProgressImages,
consoleLogLevel,
shouldLogToConsole,
} = useAppSelector(selector);
/**
* Resets localstorage, then opens a secondary modal informing user to
* refresh their browser.
* */
const handleClickResetWebUI = () => {
const handleClickResetWebUI = useCallback(() => {
persistor.purge().then(() => {
onSettingsModalClose();
onRefreshModalOpen();
});
};
}, [onSettingsModalClose, onRefreshModalOpen]);
const handleChangeIntermediateSteps = (value: number) => {
if (value > steps) value = steps;
if (value < 1) value = 1;
dispatch(setSaveIntermediatesInterval(value));
};
const handleLogLevelChanged = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => {
dispatch(consoleLogLevelChanged(e.target.value as LogLevelName));
},
[dispatch]
);
const handleLogToConsoleChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(shouldLogToConsoleChanged(e.target.checked));
},
[dispatch]
);
return (
<>
@@ -145,38 +154,17 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
isOpen={isSettingsModalOpen}
onClose={onSettingsModalClose}
size="xl"
isCentered
>
<ModalOverlay />
<ModalContent paddingInlineEnd={4}>
<ModalHeader>{t('common.settingsLabel')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Grid gap={4}>
<Flex sx={{ gap: 4, flexDirection: 'column' }}>
<Flex sx={modalSectionStyles}>
<IAISelect
label={t('settings.displayInProgress')}
validValues={IN_PROGRESS_IMAGE_TYPES}
value={shouldDisplayInProgressType}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
dispatch(
setShouldDisplayInProgressType(
e.target.value as InProgressImageType
)
)
}
/>
{shouldDisplayInProgressType === 'full-res' && (
<IAINumberInput
label={t('settings.saveSteps')}
min={1}
max={steps}
step={1}
onChange={handleChangeIntermediateSteps}
value={saveIntermediatesInterval}
width="auto"
textAlign="center"
/>
)}
<Heading size="sm">{t('settings.general')}</Heading>
<IAISwitch
label={t('settings.confirmOnDelete')}
isChecked={shouldConfirmOnDelete}
@@ -205,12 +193,31 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
dispatch(setShouldUseSliders(e.target.checked))
}
/>
<IAISwitch
label={t('settings.autoShowProgress')}
isChecked={shouldAutoShowProgressImages}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldAutoShowProgressImages(e.target.checked))
}
/>
</Flex>
<Flex sx={modalSectionStyles}>
<Heading size="sm" style={{ fontWeight: 'bold' }}>
Developer
</Heading>
<Heading size="sm">{t('settings.developer')}</Heading>
<IAISwitch
label={t('settings.shouldLogToConsole')}
isChecked={shouldLogToConsole}
onChange={handleLogToConsoleChanged}
/>
<IAISelect
horizontal
spaceEvenly
isDisabled={!shouldLogToConsole}
label={t('settings.consoleLogLevel')}
onChange={handleLogLevelChanged}
value={consoleLogLevel}
validValues={VALID_LOG_LEVELS.concat()}
/>
<IAISwitch
label={t('settings.enableImageDebugging')}
isChecked={enableImageDebugging}
@@ -228,7 +235,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
<Text>{t('settings.resetWebUIDesc1')}</Text>
<Text>{t('settings.resetWebUIDesc2')}</Text>
</Flex>
</Grid>
</Flex>
</ModalBody>
<ModalFooter>

View File

@@ -1,22 +1,34 @@
import { Text, Tooltip } from '@chakra-ui/react';
import { Flex, Icon, Text } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { errorSeen, SystemState } from 'features/system/store/systemSlice';
import { useAppSelector } from 'app/store/storeHooks';
import { isEqual } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { systemSelector } from '../store/systemSelectors';
import { ResourceKey } from 'i18next';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useRef } from 'react';
import { FaCircle } from 'react-icons/fa';
import { useHoverDirty } from 'react-use';
const statusIndicatorSelector = createSelector(
systemSelector,
(system: SystemState) => {
(system) => {
const {
isConnected,
isProcessing,
statusTranslationKey,
currentIteration,
totalIterations,
currentStatusHasSteps,
} = system;
return {
isConnected: system.isConnected,
isProcessing: system.isProcessing,
currentIteration: system.currentIteration,
totalIterations: system.totalIterations,
currentStatus: system.currentStatus,
hasError: system.hasError,
wasErrorSeen: system.wasErrorSeen,
isConnected,
isProcessing,
currentIteration,
totalIterations,
statusTranslationKey,
currentStatusHasSteps,
};
},
{
@@ -30,64 +42,69 @@ const StatusIndicator = () => {
isProcessing,
currentIteration,
totalIterations,
currentStatus,
hasError,
wasErrorSeen,
statusTranslationKey,
currentStatusHasSteps,
} = useAppSelector(statusIndicatorSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const ref = useRef(null);
let statusIdentifier;
if (isConnected && !hasError) {
statusIdentifier = 'ok';
} else {
statusIdentifier = 'error';
}
let statusMessage = currentStatus;
if (isProcessing) {
statusIdentifier = 'working';
}
if (statusMessage)
const statusColorScheme = useMemo(() => {
if (isProcessing) {
if (totalIterations > 1) {
statusMessage = `${t(
statusMessage as keyof typeof t
)} (${currentIteration}/${totalIterations})`;
}
return 'working';
}
const tooltipLabel =
hasError && !wasErrorSeen
? 'Click to clear, check logs for details'
: undefined;
const statusIndicatorCursor =
hasError && !wasErrorSeen ? 'pointer' : 'initial';
const handleClickStatusIndicator = () => {
if (hasError || !wasErrorSeen) {
dispatch(errorSeen());
if (isConnected) {
return 'ok';
}
};
return 'error';
}, [isProcessing, isConnected]);
const iterationsText = useMemo(() => {
if (!(currentIteration && totalIterations)) {
return;
}
return ` (${currentIteration}/${totalIterations})`;
}, [currentIteration, totalIterations]);
const isHovered = useHoverDirty(ref);
return (
<Tooltip label={tooltipLabel}>
<Text
cursor={statusIndicatorCursor}
onClick={handleClickStatusIndicator}
sx={{
fontSize: 'sm',
fontWeight: '600',
color: `${statusIdentifier}.400`,
}}
>
{t(statusMessage as keyof typeof t)}
</Text>
</Tooltip>
<Flex ref={ref} h="full" px={2} alignItems="center" gap={5}>
<AnimatePresence>
{isHovered && (
<motion.div
key="statusText"
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
transition: { duration: 0.15 },
}}
exit={{
opacity: 0,
transition: { delay: 0.8 },
}}
>
<Text
sx={{
fontSize: 'sm',
fontWeight: '600',
color: `${statusColorScheme}.400`,
pb: '1px',
userSelect: 'none',
}}
>
{t(statusTranslationKey as ResourceKey)}
{iterationsText}
</Text>
</motion.div>
)}
</AnimatePresence>
<Icon as={FaCircle} boxSize="0.5rem" color={`${statusColorScheme}.400`} />
</Flex>
);
};

View File

@@ -17,7 +17,6 @@ const itemsToDenylist: (keyof SystemState)[] = [
'totalSteps',
'openModel',
'isCancelScheduled',
// 'sessionId',
'progressImage',
'wereModelsReceived',
'wasSchemaParsed',

View File

@@ -1,9 +1,10 @@
import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react';
import { UseToastOptions } from '@chakra-ui/react';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/types/invokeai';
import {
generatorProgress,
graphExecutionStateComplete,
invocationComplete,
invocationError,
invocationStarted,
@@ -13,62 +14,37 @@ import {
socketUnsubscribed,
} from 'services/events/actions';
import i18n from 'i18n';
import { isImageOutput } from 'services/types/guards';
import { ProgressImage } from 'services/events/types';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { makeToast } from '../hooks/useToastWatcher';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session';
import { receivedModels } from 'services/thunks/model';
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
import { LogLevelName } from 'roarr';
import { InvokeLogLevel } from 'app/logging/useLogger';
import { TFuncKey } from 'i18next';
import { t } from 'i18next';
export type LogLevel = 'info' | 'warning' | 'error';
export type CancelStrategy = 'immediate' | 'scheduled';
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
}
export interface Log {
[index: number]: LogEntry;
}
export type InProgressImageType = 'none' | 'full-res' | 'latents';
export type CancelType = 'immediate' | 'scheduled';
export interface SystemState
extends InvokeAI.SystemStatus,
InvokeAI.SystemConfig {
shouldDisplayInProgressType: InProgressImageType;
log: Array<LogEntry>;
shouldShowLogViewer: boolean;
export interface SystemState {
isGFPGANAvailable: boolean;
isESRGANAvailable: boolean;
isConnected: boolean;
socketId: string;
isProcessing: boolean;
shouldConfirmOnDelete: boolean;
openAccordions: ExpandedIndex;
currentStep: number;
totalSteps: number;
currentIteration: number;
totalIterations: number;
currentStatus: string;
currentStatusHasSteps: boolean;
shouldDisplayGuides: boolean;
wasErrorSeen: boolean;
isCancelable: boolean;
saveIntermediatesInterval: number;
enableImageDebugging: boolean;
toastQueue: UseToastOptions[];
searchFolder: string | null;
foundModels: InvokeAI.FoundModel[] | null;
openModel: string | null;
cancelOptions: {
cancelType: CancelType;
cancelAfter: number | null;
};
/**
* The current progress image
*/
@@ -80,7 +56,7 @@ export interface SystemState
/**
* Cancel strategy
*/
cancelType: CancelType;
cancelType: CancelStrategy;
/**
* Whether or not a scheduled cancelation is pending
*/
@@ -97,81 +73,57 @@ export interface SystemState
* Whether or not the OpenAPI schema was received and parsed
*/
wasSchemaParsed: boolean;
/**
* The console output logging level
*/
consoleLogLevel: InvokeLogLevel;
shouldLogToConsole: boolean;
statusTranslationKey: TFuncKey;
canceledSession: string;
}
const initialSystemState: SystemState = {
isConnected: false,
isProcessing: false,
log: [],
shouldShowLogViewer: false,
shouldDisplayInProgressType: 'latents',
shouldDisplayGuides: true,
isGFPGANAvailable: true,
isESRGANAvailable: true,
socketId: '',
shouldConfirmOnDelete: true,
openAccordions: [0],
currentStep: 0,
totalSteps: 0,
currentIteration: 0,
totalIterations: 0,
currentStatus: i18n.isInitialized
? i18n.t('common.statusDisconnected')
: 'Disconnected',
currentStatusHasSteps: false,
model: '',
model_id: '',
model_hash: '',
app_id: '',
app_version: '',
model_list: {},
infill_methods: [],
hasError: false,
wasErrorSeen: true,
isCancelable: true,
saveIntermediatesInterval: 5,
enableImageDebugging: false,
toastQueue: [],
searchFolder: null,
foundModels: null,
openModel: null,
cancelOptions: {
cancelType: 'immediate',
cancelAfter: null,
},
progressImage: null,
sessionId: null,
cancelType: 'immediate',
isCancelScheduled: false,
subscribedNodeIds: [],
// shouldTransformUrls: false,
// disabledTabs: [],
// disabledFeatures: [],
wereModelsReceived: false,
wasSchemaParsed: false,
consoleLogLevel: 'error',
shouldLogToConsole: true,
statusTranslationKey: 'common.statusDisconnected',
canceledSession: '',
};
export const systemSlice = createSlice({
name: 'system',
initialState: initialSystemState,
reducers: {
setShouldDisplayInProgressType: (
state,
action: PayloadAction<InProgressImageType>
) => {
state.shouldDisplayInProgressType = action.payload;
},
setIsProcessing: (state, action: PayloadAction<boolean>) => {
state.isProcessing = action.payload;
},
setCurrentStatus: (state, action: PayloadAction<string>) => {
state.currentStatus = action.payload;
},
setSystemStatus: (state, action: PayloadAction<InvokeAI.SystemStatus>) => {
return { ...state, ...action.payload };
setCurrentStatus: (state, action: PayloadAction<TFuncKey>) => {
state.statusTranslationKey = action.payload;
},
errorOccurred: (state) => {
state.hasError = true;
state.isProcessing = false;
state.isCancelable = true;
state.currentStep = 0;
@@ -179,37 +131,7 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStatus = i18n.t('common.statusError');
state.wasErrorSeen = false;
},
errorSeen: (state) => {
state.hasError = false;
state.wasErrorSeen = true;
state.currentStatus = state.isConnected
? i18n.t('common.statusConnected')
: i18n.t('common.statusDisconnected');
},
addLogEntry: (
state,
action: PayloadAction<{
timestamp: string;
message: string;
level?: LogLevel;
}>
) => {
const { timestamp, message, level } = action.payload;
const logLevel = level || 'info';
const entry: LogEntry = {
timestamp,
message,
level: logLevel,
};
state.log.push(entry);
},
setShouldShowLogViewer: (state, action: PayloadAction<boolean>) => {
state.shouldShowLogViewer = action.payload;
state.statusTranslationKey = 'common.statusError';
},
setIsConnected: (state, action: PayloadAction<boolean>) => {
state.isConnected = action.payload;
@@ -220,23 +142,10 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.hasError = false;
},
setSocketId: (state, action: PayloadAction<string>) => {
state.socketId = action.payload;
},
setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => {
state.shouldConfirmOnDelete = action.payload;
},
setOpenAccordions: (state, action: PayloadAction<ExpandedIndex>) => {
state.openAccordions = action.payload;
},
setSystemConfig: (state, action: PayloadAction<InvokeAI.SystemConfig>) => {
return {
...state,
...action.payload,
};
},
setShouldDisplayGuides: (state, action: PayloadAction<boolean>) => {
state.shouldDisplayGuides = action.payload;
},
@@ -248,7 +157,7 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStatus = i18n.t('common.statusProcessingCanceled');
state.statusTranslationKey = 'common.statusProcessingCanceled';
},
generationRequested: (state) => {
state.isProcessing = true;
@@ -258,38 +167,29 @@ export const systemSlice = createSlice({
state.currentIteration = 0;
state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStatus = i18n.t('common.statusPreparing');
},
setModelList: (
state,
action: PayloadAction<InvokeAI.ModelList | Record<string, never>>
) => {
state.model_list = action.payload;
state.statusTranslationKey = 'common.statusPreparing';
},
setIsCancelable: (state, action: PayloadAction<boolean>) => {
state.isCancelable = action.payload;
},
modelChangeRequested: (state) => {
state.currentStatus = i18n.t('common.statusLoadingModel');
state.statusTranslationKey = 'common.statusLoadingModel';
state.isCancelable = false;
state.isProcessing = true;
state.currentStatusHasSteps = false;
},
modelConvertRequested: (state) => {
state.currentStatus = i18n.t('common.statusConvertingModel');
state.statusTranslationKey = 'common.statusConvertingModel';
state.isCancelable = false;
state.isProcessing = true;
state.currentStatusHasSteps = false;
},
modelMergingRequested: (state) => {
state.currentStatus = i18n.t('common.statusMergingModels');
state.statusTranslationKey = 'common.statusMergingModels';
state.isCancelable = false;
state.isProcessing = true;
state.currentStatusHasSteps = false;
},
setSaveIntermediatesInterval: (state, action: PayloadAction<number>) => {
state.saveIntermediatesInterval = action.payload;
},
setEnableImageDebugging: (state, action: PayloadAction<boolean>) => {
state.enableImageDebugging = action.payload;
},
@@ -299,9 +199,12 @@ export const systemSlice = createSlice({
clearToastQueue: (state) => {
state.toastQueue = [];
},
setProcessingIndeterminateTask: (state, action: PayloadAction<string>) => {
setProcessingIndeterminateTask: (
state,
action: PayloadAction<TFuncKey>
) => {
state.isProcessing = true;
state.currentStatus = action.payload;
state.statusTranslationKey = action.payload;
state.currentStatusHasSteps = false;
},
setSearchFolder: (state, action: PayloadAction<string | null>) => {
@@ -316,12 +219,6 @@ export const systemSlice = createSlice({
setOpenModel: (state, action: PayloadAction<string | null>) => {
state.openModel = action.payload;
},
setCancelType: (state, action: PayloadAction<CancelType>) => {
state.cancelOptions.cancelType = action.payload;
},
setCancelAfter: (state, action: PayloadAction<number | null>) => {
state.cancelOptions.cancelAfter = action.payload;
},
/**
* A cancel was scheduled
*/
@@ -337,7 +234,7 @@ export const systemSlice = createSlice({
/**
* The cancel type was changed
*/
cancelTypeChanged: (state, action: PayloadAction<CancelType>) => {
cancelTypeChanged: (state, action: PayloadAction<CancelStrategy>) => {
state.cancelType = action.payload;
},
/**
@@ -346,6 +243,12 @@ export const systemSlice = createSlice({
subscribedNodeIdsSet: (state, action: PayloadAction<string[]>) => {
state.subscribedNodeIds = action.payload;
},
consoleLogLevelChanged: (state, action: PayloadAction<LogLevelName>) => {
state.consoleLogLevel = action.payload;
},
shouldLogToConsoleChanged: (state, action: PayloadAction<boolean>) => {
state.shouldLogToConsole = action.payload;
},
},
extraReducers(builder) {
/**
@@ -353,7 +256,7 @@ export const systemSlice = createSlice({
*/
builder.addCase(socketSubscribed, (state, action) => {
state.sessionId = action.payload.sessionId;
console.log(`Subscribed to session ${action.payload.sessionId}`);
state.canceledSession = '';
});
/**
@@ -368,14 +271,15 @@ export const systemSlice = createSlice({
*/
builder.addCase(socketConnected, (state, action) => {
const { timestamp } = action.payload;
state.isConnected = true;
state.currentStatus = i18n.t('common.statusConnected');
state.log.push({
timestamp,
message: `Connected to server`,
level: 'info',
});
state.isCancelable = true;
state.isProcessing = false;
state.currentStatusHasSteps = false;
state.currentStep = 0;
state.totalSteps = 0;
state.currentIteration = 0;
state.totalIterations = 0;
state.statusTranslationKey = 'common.statusConnected';
});
/**
@@ -385,22 +289,28 @@ export const systemSlice = createSlice({
const { timestamp } = action.payload;
state.isConnected = false;
state.currentStatus = i18n.t('common.statusDisconnected');
state.log.push({
timestamp,
message: `Disconnected from server`,
level: 'error',
});
state.isProcessing = false;
state.isCancelable = true;
state.currentStatusHasSteps = false;
state.currentStep = 0;
state.totalSteps = 0;
// state.currentIteration = 0;
// state.totalIterations = 0;
state.statusTranslationKey = 'common.statusDisconnected';
});
/**
* Invocation Started
*/
builder.addCase(invocationStarted, (state) => {
state.isProcessing = true;
builder.addCase(invocationStarted, (state, action) => {
state.isCancelable = true;
state.isProcessing = true;
state.currentStatusHasSteps = false;
state.currentStatus = i18n.t('common.statusGenerating');
state.currentStep = 0;
state.totalSteps = 0;
// state.currentIteration = 0;
// state.totalIterations = 0;
state.statusTranslationKey = 'common.statusGenerating';
});
/**
@@ -416,10 +326,15 @@ export const systemSlice = createSlice({
graph_execution_state_id,
} = action.payload.data;
state.isProcessing = true;
state.isCancelable = true;
// state.currentIteration = 0;
// state.totalIterations = 0;
state.currentStatusHasSteps = true;
state.currentStep = step + 1; // TODO: step starts at -1, think this is a bug
state.totalSteps = total_steps;
state.progressImage = progress_image ?? null;
state.statusTranslationKey = 'common.statusGenerating';
});
/**
@@ -428,19 +343,16 @@ export const systemSlice = createSlice({
builder.addCase(invocationComplete, (state, action) => {
const { data, timestamp } = action.payload;
state.isProcessing = false;
// state.currentIteration = 0;
// state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStep = 0;
state.totalSteps = 0;
state.progressImage = null;
state.currentStatus = i18n.t('common.statusProcessingComplete');
state.statusTranslationKey = 'common.statusProcessingComplete';
// TODO: handle logging for other invocation types
if (isImageOutput(data.result)) {
state.log.push({
timestamp,
message: `Generated: ${data.result.image.image_name}`,
level: 'info',
});
if (state.canceledSession === data.graph_execution_state_id) {
state.isProcessing = false;
state.isCancelable = true;
}
});
@@ -450,25 +362,18 @@ export const systemSlice = createSlice({
builder.addCase(invocationError, (state, action) => {
const { data, timestamp } = action.payload;
state.log.push({
timestamp,
message: `Server error: ${data.error}`,
level: 'error',
});
state.wasErrorSeen = true;
state.progressImage = null;
state.isProcessing = false;
state.isCancelable = true;
// state.currentIteration = 0;
// state.totalIterations = 0;
state.currentStatusHasSteps = false;
state.currentStep = 0;
state.totalSteps = 0;
state.statusTranslationKey = 'common.statusError';
state.toastQueue.push(
makeToast({ title: i18n.t('toast.serverError'), status: 'error' })
makeToast({ title: t('toast.serverError'), status: 'error' })
);
state.log.push({
timestamp,
message: `Server error: ${data.error}`,
level: 'error',
});
});
/**
@@ -476,7 +381,7 @@ export const systemSlice = createSlice({
*/
builder.addCase(sessionInvoked.pending, (state) => {
state.currentStatus = i18n.t('common.statusPreparing');
state.statusTranslationKey = 'common.statusPreparing';
});
/**
@@ -485,29 +390,38 @@ export const systemSlice = createSlice({
builder.addCase(sessionCanceled.fulfilled, (state, action) => {
const { timestamp } = action.payload;
state.canceledSession = action.meta.arg.sessionId;
state.isProcessing = false;
state.isCancelable = false;
state.isCancelScheduled = false;
state.currentStep = 0;
state.totalSteps = 0;
state.progressImage = null;
state.statusTranslationKey = 'common.statusConnected';
state.toastQueue.push(
makeToast({ title: i18n.t('toast.canceled'), status: 'warning' })
makeToast({ title: t('toast.canceled'), status: 'warning' })
);
});
state.log.push({
timestamp,
message: `Processing canceled`,
level: 'warning',
});
/**
* Session Canceled
*/
builder.addCase(graphExecutionStateComplete, (state, action) => {
const { timestamp } = action.payload;
state.isProcessing = false;
state.isCancelable = false;
state.isCancelScheduled = false;
state.currentStep = 0;
state.totalSteps = 0;
state.statusTranslationKey = 'common.statusConnected';
});
/**
* Initial Image Selected
*/
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage')));
state.toastQueue.push(makeToast(t('toast.sentToImageToImage')));
});
/**
@@ -527,27 +441,17 @@ export const systemSlice = createSlice({
});
export const {
setShouldDisplayInProgressType,
setIsProcessing,
addLogEntry,
setShouldShowLogViewer,
setIsConnected,
setSocketId,
setShouldConfirmOnDelete,
setOpenAccordions,
setSystemStatus,
setCurrentStatus,
setSystemConfig,
setShouldDisplayGuides,
processingCanceled,
errorOccurred,
errorSeen,
setModelList,
setIsCancelable,
modelChangeRequested,
modelConvertRequested,
modelMergingRequested,
setSaveIntermediatesInterval,
setEnableImageDebugging,
generationRequested,
addToast,
@@ -556,12 +460,12 @@ export const {
setSearchFolder,
setFoundModels,
setOpenModel,
setCancelType,
setCancelAfter,
cancelScheduled,
scheduledCancelAborted,
cancelTypeChanged,
subscribedNodeIdsSet,
consoleLogLevelChanged,
shouldLogToConsoleChanged,
} = systemSlice.actions;
export default systemSlice.reducer;

View File

@@ -1,10 +1,12 @@
import { Box, Flex } from '@chakra-ui/react';
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview';
const GenerateContent = () => {
return (
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
borderRadius: 'base',

View File

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

View File

@@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { setActiveTabReducer } from './extraReducers';
import { InvokeTabName, tabMap } from './tabMap';
import { AddNewModelType, UIState } from './uiTypes';
import { AddNewModelType, Coordinates, Rect, UIState } from './uiTypes';
const initialUIState: UIState = {
activeTab: 0,
@@ -21,6 +21,9 @@ const initialUIState: UIState = {
openLinearAccordionItems: [],
openGenerateAccordionItems: [],
openUnifiedCanvasAccordionItems: [],
floatingProgressImageRect: { x: 0, y: 0, width: 0, height: 0 },
shouldShowProgressImages: false,
shouldAutoShowProgressImages: false,
};
const initialState: UIState = initialUIState;
@@ -105,6 +108,30 @@ export const uiSlice = createSlice({
state.openUnifiedCanvasAccordionItems = action.payload;
}
},
floatingProgressImageMoved: (state, action: PayloadAction<Coordinates>) => {
state.floatingProgressImageRect = {
...state.floatingProgressImageRect,
...action.payload,
};
},
floatingProgressImageResized: (
state,
action: PayloadAction<Partial<Rect>>
) => {
state.floatingProgressImageRect = {
...state.floatingProgressImageRect,
...action.payload,
};
},
setShouldShowProgressImages: (state, action: PayloadAction<boolean>) => {
state.shouldShowProgressImages = action.payload;
},
setShouldAutoShowProgressImages: (
state,
action: PayloadAction<boolean>
) => {
state.shouldAutoShowProgressImages = action.payload;
},
},
});
@@ -128,6 +155,10 @@ export const {
toggleParametersPanel,
toggleGalleryPanel,
openAccordionItemsChanged,
floatingProgressImageMoved,
floatingProgressImageResized,
setShouldShowProgressImages,
setShouldAutoShowProgressImages,
} = uiSlice.actions;
export default uiSlice.reducer;

View File

@@ -1,7 +1,17 @@
import { InvokeTabName } from './tabMap';
export type AddNewModelType = 'ckpt' | 'diffusers' | null;
export type Coordinates = {
x: number;
y: number;
};
export type Dimensions = {
width: number | string;
height: number | string;
};
export type Rect = Coordinates & Dimensions;
export interface UIState {
activeTab: number;
currentTheme: string;
@@ -19,4 +29,7 @@ export interface UIState {
openLinearAccordionItems: number[];
openGenerateAccordionItems: number[];
openUnifiedCanvasAccordionItems: number[];
floatingProgressImageRect: Rect;
shouldShowProgressImages: boolean;
shouldAutoShowProgressImages: boolean;
}

View File

@@ -22,15 +22,13 @@ export interface OnCancel {
}
export class CancelablePromise<T> implements Promise<T> {
readonly [Symbol.toStringTag]!: string;
private _isResolved: boolean;
private _isRejected: boolean;
private _isCancelled: boolean;
private readonly _cancelHandlers: (() => void)[];
private readonly _promise: Promise<T>;
private _resolve?: (value: T | PromiseLike<T>) => void;
private _reject?: (reason?: any) => void;
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
@@ -39,78 +37,82 @@ export class CancelablePromise<T> implements Promise<T> {
onCancel: OnCancel
) => void
) {
this._isResolved = false;
this._isRejected = false;
this._isCancelled = false;
this._cancelHandlers = [];
this._promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this._isResolved = true;
this._resolve?.(value);
this.#isResolved = true;
this.#resolve?.(value);
};
const onReject = (reason?: any): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this._isRejected = true;
this._reject?.(reason);
this.#isRejected = true;
this.#reject?.(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this._isResolved || this._isRejected || this._isCancelled) {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this._cancelHandlers.push(cancelHandler);
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this._isResolved,
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this._isRejected,
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this._isCancelled,
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this._promise.then(onFulfilled, onRejected);
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this._promise.catch(onRejected);
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this._promise.finally(onFinally);
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this._isResolved || this._isRejected || this._isCancelled) {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this._isCancelled = true;
if (this._cancelHandlers.length) {
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this._cancelHandlers) {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
@@ -118,11 +120,11 @@ export class CancelablePromise<T> implements Promise<T> {
return;
}
}
this._cancelHandlers.length = 0;
this._reject?.(new CancelError('Request aborted'));
this.#cancelHandlers.length = 0;
this.#reject?.(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this._isCancelled;
return this.#isCancelled;
}
}

View File

@@ -58,7 +58,9 @@ export type { PasteImageInvocation } from './models/PasteImageInvocation';
export type { PromptOutput } from './models/PromptOutput';
export type { RandomRangeInvocation } from './models/RandomRangeInvocation';
export type { RangeInvocation } from './models/RangeInvocation';
export type { ResizeLatentsInvocation } from './models/ResizeLatentsInvocation';
export type { RestoreFaceInvocation } from './models/RestoreFaceInvocation';
export type { ScaleLatentsInvocation } from './models/ScaleLatentsInvocation';
export type { ShowImageInvocation } from './models/ShowImageInvocation';
export type { SubtractInvocation } from './models/SubtractInvocation';
export type { TextToImageInvocation } from './models/TextToImageInvocation';
@@ -119,7 +121,9 @@ export { $PasteImageInvocation } from './schemas/$PasteImageInvocation';
export { $PromptOutput } from './schemas/$PromptOutput';
export { $RandomRangeInvocation } from './schemas/$RandomRangeInvocation';
export { $RangeInvocation } from './schemas/$RangeInvocation';
export { $ResizeLatentsInvocation } from './schemas/$ResizeLatentsInvocation';
export { $RestoreFaceInvocation } from './schemas/$RestoreFaceInvocation';
export { $ScaleLatentsInvocation } from './schemas/$ScaleLatentsInvocation';
export { $ShowImageInvocation } from './schemas/$ShowImageInvocation';
export { $SubtractInvocation } from './schemas/$SubtractInvocation';
export { $TextToImageInvocation } from './schemas/$TextToImageInvocation';

View File

@@ -25,7 +25,9 @@ import type { ParamIntInvocation } from './ParamIntInvocation';
import type { PasteImageInvocation } from './PasteImageInvocation';
import type { RandomRangeInvocation } from './RandomRangeInvocation';
import type { RangeInvocation } from './RangeInvocation';
import type { ResizeLatentsInvocation } from './ResizeLatentsInvocation';
import type { RestoreFaceInvocation } from './RestoreFaceInvocation';
import type { ScaleLatentsInvocation } from './ScaleLatentsInvocation';
import type { ShowImageInvocation } from './ShowImageInvocation';
import type { SubtractInvocation } from './SubtractInvocation';
import type { TextToImageInvocation } from './TextToImageInvocation';
@@ -40,7 +42,7 @@ export type Graph = {
/**
* The nodes in this graph
*/
nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation)>;
nodes?: Record<string, (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation)>;
/**
* The connections between nodes and their fields in this graph
*/

View File

@@ -17,10 +17,6 @@ export type LatentsToLatentsInvocation = {
* The prompt to generate an image from
*/
prompt?: string;
/**
* The seed to use (-1 for a random seed)
*/
seed?: number;
/**
* The noise to use
*/
@@ -29,14 +25,6 @@ export type LatentsToLatentsInvocation = {
* The number of steps to use to generate the image
*/
steps?: number;
/**
* The width of the resulting image
*/
width?: number;
/**
* The height of the resulting image
*/
height?: number;
/**
* The Classifier-Free Guidance, higher values may result in a result closer to the prompt
*/

View File

@@ -0,0 +1,37 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { LatentsField } from './LatentsField';
/**
* Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.
*/
export type ResizeLatentsInvocation = {
/**
* The id of this node. Must be unique among all nodes.
*/
id: string;
type?: 'lresize';
/**
* The latents to resize
*/
latents?: LatentsField;
/**
* The width to resize to (px)
*/
width: number;
/**
* The height to resize to (px)
*/
height: number;
/**
* The interpolation mode
*/
mode?: 'nearest' | 'linear' | 'bilinear' | 'bicubic' | 'trilinear' | 'area' | 'nearest-exact';
/**
* Whether or not to antialias (applied in bilinear and bicubic modes only)
*/
antialias?: boolean;
};

View File

@@ -0,0 +1,33 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { LatentsField } from './LatentsField';
/**
* Scales latents by a given factor.
*/
export type ScaleLatentsInvocation = {
/**
* The id of this node. Must be unique among all nodes.
*/
id: string;
type?: 'lscale';
/**
* The latents to scale
*/
latents?: LatentsField;
/**
* The factor by which to scale the latents
*/
scale_factor: number;
/**
* The interpolation mode
*/
mode?: 'nearest' | 'linear' | 'bilinear' | 'bicubic' | 'trilinear' | 'area' | 'nearest-exact';
/**
* Whether or not to antialias (applied in bilinear and bicubic modes only)
*/
antialias?: boolean;
};

View File

@@ -17,10 +17,6 @@ export type TextToLatentsInvocation = {
* The prompt to generate an image from
*/
prompt?: string;
/**
* The seed to use (-1 for a random seed)
*/
seed?: number;
/**
* The noise to use
*/
@@ -29,14 +25,6 @@ export type TextToLatentsInvocation = {
* The number of steps to use to generate the image
*/
steps?: number;
/**
* The width of the resulting image
*/
width?: number;
/**
* The height of the resulting image
*/
height?: number;
/**
* The Classifier-Free Guidance, higher values may result in a result closer to the prompt
*/

View File

@@ -33,6 +33,10 @@ export const $Graph = {
type: 'TextToLatentsInvocation',
}, {
type: 'LatentsToImageInvocation',
}, {
type: 'ResizeLatentsInvocation',
}, {
type: 'ScaleLatentsInvocation',
}, {
type: 'AddInvocation',
}, {

View File

@@ -16,12 +16,6 @@ export const $LatentsToLatentsInvocation = {
type: 'string',
description: `The prompt to generate an image from`,
},
seed: {
type: 'number',
description: `The seed to use (-1 for a random seed)`,
maximum: 4294967295,
minimum: -1,
},
noise: {
type: 'all-of',
description: `The noise to use`,
@@ -33,16 +27,6 @@ export const $LatentsToLatentsInvocation = {
type: 'number',
description: `The number of steps to use to generate the image`,
},
width: {
type: 'number',
description: `The width of the resulting image`,
multipleOf: 64,
},
height: {
type: 'number',
description: `The height of the resulting image`,
multipleOf: 64,
},
cfg_scale: {
type: 'number',
description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`,

View File

@@ -0,0 +1,44 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ResizeLatentsInvocation = {
description: `Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.`,
properties: {
id: {
type: 'string',
description: `The id of this node. Must be unique among all nodes.`,
isRequired: true,
},
type: {
type: 'Enum',
},
latents: {
type: 'all-of',
description: `The latents to resize`,
contains: [{
type: 'LatentsField',
}],
},
width: {
type: 'number',
description: `The width to resize to (px)`,
isRequired: true,
minimum: 64,
multipleOf: 8,
},
height: {
type: 'number',
description: `The height to resize to (px)`,
isRequired: true,
minimum: 64,
multipleOf: 8,
},
mode: {
type: 'Enum',
},
antialias: {
type: 'boolean',
description: `Whether or not to antialias (applied in bilinear and bicubic modes only)`,
},
},
} as const;

View File

@@ -0,0 +1,35 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ScaleLatentsInvocation = {
description: `Scales latents by a given factor.`,
properties: {
id: {
type: 'string',
description: `The id of this node. Must be unique among all nodes.`,
isRequired: true,
},
type: {
type: 'Enum',
},
latents: {
type: 'all-of',
description: `The latents to scale`,
contains: [{
type: 'LatentsField',
}],
},
scale_factor: {
type: 'number',
description: `The factor by which to scale the latents`,
isRequired: true,
},
mode: {
type: 'Enum',
},
antialias: {
type: 'boolean',
description: `Whether or not to antialias (applied in bilinear and bicubic modes only)`,
},
},
} as const;

View File

@@ -16,12 +16,6 @@ export const $TextToLatentsInvocation = {
type: 'string',
description: `The prompt to generate an image from`,
},
seed: {
type: 'number',
description: `The seed to use (-1 for a random seed)`,
maximum: 4294967295,
minimum: -1,
},
noise: {
type: 'all-of',
description: `The noise to use`,
@@ -33,16 +27,6 @@ export const $TextToLatentsInvocation = {
type: 'number',
description: `The number of steps to use to generate the image`,
},
width: {
type: 'number',
description: `The width of the resulting image`,
multipleOf: 64,
},
height: {
type: 'number',
description: `The height of the resulting image`,
multipleOf: 64,
},
cfg_scale: {
type: 'number',
description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`,

View File

@@ -27,7 +27,9 @@ import type { ParamIntInvocation } from '../models/ParamIntInvocation';
import type { PasteImageInvocation } from '../models/PasteImageInvocation';
import type { RandomRangeInvocation } from '../models/RandomRangeInvocation';
import type { RangeInvocation } from '../models/RangeInvocation';
import type { ResizeLatentsInvocation } from '../models/ResizeLatentsInvocation';
import type { RestoreFaceInvocation } from '../models/RestoreFaceInvocation';
import type { ScaleLatentsInvocation } from '../models/ScaleLatentsInvocation';
import type { ShowImageInvocation } from '../models/ShowImageInvocation';
import type { SubtractInvocation } from '../models/SubtractInvocation';
import type { TextToImageInvocation } from '../models/TextToImageInvocation';
@@ -142,7 +144,7 @@ export class SessionsService {
* The id of the session
*/
sessionId: string,
requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation),
requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation),
}): CancelablePromise<string> {
return __request(OpenAPI, {
method: 'POST',
@@ -179,7 +181,7 @@ export class SessionsService {
* The path to the node in the graph
*/
nodePath: string,
requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation),
requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | ResizeLatentsInvocation | ScaleLatentsInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation),
}): CancelablePromise<GraphExecutionState> {
return __request(OpenAPI, {
method: 'PUT',

View File

@@ -1,6 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import {
GeneratorProgressEvent,
GraphExecutionStateCompleteEvent,
InvocationCompleteEvent,
InvocationErrorEvent,
InvocationStartedEvent,
@@ -45,6 +46,10 @@ export const invocationError = createAction<
BaseSocketPayload & { data: InvocationErrorEvent }
>('socket/invocationError');
export const graphExecutionStateComplete = createAction<
BaseSocketPayload & { data: GraphExecutionStateCompleteEvent }
>('socket/graphExecutionStateComplete');
export const generatorProgress = createAction<
BaseSocketPayload & { data: GeneratorProgressEvent }
>('socket/generatorProgress');

View File

@@ -6,33 +6,23 @@ import {
ServerToClientEvents,
} from 'services/events/types';
import {
generatorProgress,
invocationComplete,
invocationError,
invocationStarted,
socketConnected,
socketDisconnected,
socketReset,
socketSubscribed,
socketUnsubscribed,
} from './actions';
import {
receivedResultImagesPage,
receivedUploadImagesPage,
} from 'services/thunks/gallery';
import { AppDispatch, RootState } from 'app/store/store';
import { getTimestamp } from 'common/util/getTimestamp';
import {
sessionInvoked,
isFulfilledSessionCreatedAction,
sessionCanceled,
} from 'services/thunks/session';
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';
import { setEventListeners } from 'services/events/util/setEventListeners';
import { log } from 'app/logging/useLogger';
const socketioLog = log.child({ namespace: 'socketio' });
export const socketMiddleware = () => {
let areListenersSet = false;
@@ -67,92 +57,27 @@ export const socketMiddleware = () => {
(store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => {
const { dispatch, getState } = store;
// Nothing dispatches `socketReset` actions yet
// if (socketReset.match(action)) {
// const { sessionId } = getState().system;
// if (sessionId) {
// socket.emit('unsubscribe', { session: sessionId });
// dispatch(
// socketUnsubscribed({ sessionId, timestamp: getTimestamp() })
// );
// }
// if (socket.connected) {
// socket.disconnect();
// dispatch(socketDisconnected({ timestamp: getTimestamp() }));
// }
// socket.removeAllListeners();
// areListenersSet = false;
// }
// Set listeners for `connect` and `disconnect` events once
// Must happen in middleware to get access to `dispatch`
if (!areListenersSet) {
socket.on('connect', () => {
dispatch(socketConnected({ timestamp: getTimestamp() }));
const { results, uploads, models, nodes, config, system } =
getState();
const { disabledTabs } = config;
// These thunks need to be dispatch in middleware; cannot handle in a reducer
if (!results.ids.length) {
dispatch(receivedResultImagesPage());
}
if (!uploads.ids.length) {
dispatch(receivedUploadImagesPage());
}
if (!models.ids.length) {
dispatch(receivedModels());
}
if (!nodes.schema && !disabledTabs.includes('nodes')) {
dispatch(receivedOpenAPISchema());
}
if (system.sessionId) {
console.log(`Re-subscribing to session ${system.sessionId}`);
socket.emit('subscribe', { session: system.sessionId });
dispatch(
socketSubscribed({
sessionId: system.sessionId,
timestamp: getTimestamp(),
})
);
setEventListeners({ socket, store });
}
});
socket.on('disconnect', () => {
dispatch(socketDisconnected({ timestamp: getTimestamp() }));
});
setEventListeners({ store, socket, log: socketioLog });
areListenersSet = true;
// must manually connect
socket.connect();
}
// Everything else only happens once we have created a session
if (isFulfilledSessionCreatedAction(action)) {
const sessionId = action.payload.id;
const sessionLog = socketioLog.child({ sessionId });
const oldSessionId = getState().system.sessionId;
// const subscribedNodeIds = getState().system.subscribedNodeIds;
// const shouldHandleEvent = (id: string): boolean => {
// if (subscribedNodeIds.length === 1 && subscribedNodeIds[0] === '*') {
// return true;
// }
// return subscribedNodeIds.includes(id);
// };
if (oldSessionId) {
// Unsubscribe when invocations complete
sessionLog.debug(
{ oldSessionId },
`Unsubscribed from old session (${oldSessionId})`
);
socket.emit('unsubscribe', {
session: oldSessionId,
});
@@ -163,29 +88,18 @@ export const socketMiddleware = () => {
timestamp: getTimestamp(),
})
);
const listenersToRemove: (keyof ServerToClientEvents)[] = [
'invocation_started',
'generator_progress',
'invocation_error',
'invocation_complete',
];
// Remove listeners for these events; we need to set them up fresh whenever we subscribe
listenersToRemove.forEach((event: keyof ServerToClientEvents) => {
socket.removeAllListeners(event);
});
}
const sessionId = action.payload.id;
sessionLog.debug(`Subscribe to new session (${sessionId})`);
socket.emit('subscribe', { session: sessionId });
dispatch(
socketSubscribed({
sessionId: sessionId,
timestamp: getTimestamp(),
})
);
setEventListeners({ socket, store });
// Finally we actually invoke the session, starting processing
dispatch(sessionInvoked({ sessionId }));
@@ -211,7 +125,6 @@ export const socketMiddleware = () => {
}
}
// Always pass the action on so other middleware and reducers can handle it
next(action);
};

View File

@@ -1,4 +1,4 @@
import { Graph, GraphExecutionState } from '../api';
import { Graph, GraphExecutionState, InvokeAIMetadata } from '../api';
/**
* A progress image, we get one for each step in the generation
@@ -17,6 +17,12 @@ export type AnyInvocation = NonNullable<Graph['nodes']>[string];
export type AnyResult = GraphExecutionState['results'][string];
export type BaseNode = {
id: string;
type: string;
[key: string]: NonNullable<InvokeAIMetadata['node']>[string];
};
/**
* A `generator_progress` socket.io event.
*
@@ -24,7 +30,7 @@ export type AnyResult = GraphExecutionState['results'][string];
*/
export type GeneratorProgressEvent = {
graph_execution_state_id: string;
node: AnyInvocation;
node: BaseNode;
source_node_id: string;
progress_image?: ProgressImage;
step: number;
@@ -40,7 +46,7 @@ export type GeneratorProgressEvent = {
*/
export type InvocationCompleteEvent = {
graph_execution_state_id: string;
node: AnyInvocation;
node: BaseNode;
source_node_id: string;
result: AnyResult;
};
@@ -52,7 +58,7 @@ export type InvocationCompleteEvent = {
*/
export type InvocationErrorEvent = {
graph_execution_state_id: string;
node: AnyInvocation;
node: BaseNode;
source_node_id: string;
error: string;
};
@@ -64,7 +70,7 @@ export type InvocationErrorEvent = {
*/
export type InvocationStartedEvent = {
graph_execution_state_id: string;
node: AnyInvocation;
node: BaseNode;
source_node_id: string;
};

View File

@@ -5,34 +5,144 @@ import { sessionCanceled } from 'services/thunks/session';
import { Socket } from 'socket.io-client';
import {
generatorProgress,
graphExecutionStateComplete,
invocationComplete,
invocationError,
invocationStarted,
socketConnected,
socketDisconnected,
socketSubscribed,
} from '../actions';
import { ClientToServerEvents, ServerToClientEvents } from '../types';
import { Logger } from 'roarr';
import { JsonObject } from 'roarr/dist/types';
import {
receivedResultImagesPage,
receivedUploadImagesPage,
} from 'services/thunks/gallery';
import { receivedModels } from 'services/thunks/model';
import { receivedOpenAPISchema } from 'services/thunks/schema';
type SetEventListenersArg = {
socket: Socket<ServerToClientEvents, ClientToServerEvents>;
store: MiddlewareAPI<AppDispatch, RootState>;
log: Logger<JsonObject>;
};
export const setEventListeners = (arg: SetEventListenersArg) => {
const { socket, store } = arg;
const { socket, store, log } = arg;
const { dispatch, getState } = store;
// Set up listeners for the present subscription
/**
* Connect
*/
socket.on('connect', () => {
log.debug('Connected');
dispatch(socketConnected({ timestamp: getTimestamp() }));
const { results, uploads, models, nodes, config, system } = getState();
const { disabledTabs } = config;
// These thunks need to be dispatch in middleware; cannot handle in a reducer
if (!results.ids.length) {
dispatch(receivedResultImagesPage());
}
if (!uploads.ids.length) {
dispatch(receivedUploadImagesPage());
}
if (!models.ids.length) {
dispatch(receivedModels());
}
if (!nodes.schema && !disabledTabs.includes('nodes')) {
dispatch(receivedOpenAPISchema());
}
if (system.sessionId) {
log.debug(
{ sessionId: system.sessionId },
`Subscribed to existing session (${system.sessionId})`
);
socket.emit('subscribe', { session: system.sessionId });
dispatch(
socketSubscribed({
sessionId: system.sessionId,
timestamp: getTimestamp(),
})
);
}
});
/**
* Disconnect
*/
socket.on('disconnect', () => {
log.debug('Disconnected');
dispatch(socketDisconnected({ timestamp: getTimestamp() }));
});
/**
* Invocation started
*/
socket.on('invocation_started', (data) => {
if (getState().system.canceledSession === data.graph_execution_state_id) {
log.trace(
{ data, sessionId: data.graph_execution_state_id },
`Ignored invocation started (${data.node.type}) for canceled session (${data.graph_execution_state_id})`
);
return;
}
log.info(
{ data, sessionId: data.graph_execution_state_id },
`Invocation started (${data.node.type})`
);
dispatch(invocationStarted({ data, timestamp: getTimestamp() }));
});
/**
* Generator progress
*/
socket.on('generator_progress', (data) => {
if (getState().system.canceledSession === data.graph_execution_state_id) {
log.trace(
{ data, sessionId: data.graph_execution_state_id },
`Ignored generator progress (${data.node.type}) for canceled session (${data.graph_execution_state_id})`
);
return;
}
log.trace(
{ data, sessionId: data.graph_execution_state_id },
`Generator progress (${data.node.type})`
);
dispatch(generatorProgress({ data, timestamp: getTimestamp() }));
});
/**
* Invocation error
*/
socket.on('invocation_error', (data) => {
log.error(
{ data, sessionId: data.graph_execution_state_id },
`Invocation error (${data.node.type})`
);
dispatch(invocationError({ data, timestamp: getTimestamp() }));
});
/**
* Invocation complete
*/
socket.on('invocation_complete', (data) => {
log.info(
{ data, sessionId: data.graph_execution_state_id },
`Invocation complete (${data.node.type})`
);
const sessionId = data.graph_execution_state_id;
const { cancelType, isCancelScheduled } = getState().system;
@@ -51,4 +161,15 @@ export const setEventListeners = (arg: SetEventListenersArg) => {
})
);
});
/**
* Graph complete
*/
socket.on('graph_execution_state_complete', (data) => {
log.info(
{ data, sessionId: data.graph_execution_state_id },
`Graph execution state complete (${data.graph_execution_state_id})`
);
dispatch(graphExecutionStateComplete({ data, timestamp: getTimestamp() }));
});
};

View File

@@ -1,8 +1,11 @@
import { log } from 'app/logging/useLogger';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { ImagesService } from 'services/api';
export const IMAGES_PER_PAGE = 20;
const galleryLog = log.child({ namespace: 'gallery' });
export const receivedResultImagesPage = createAppAsyncThunk(
'results/receivedResultImagesPage',
async (_arg, { getState }) => {
@@ -12,6 +15,8 @@ export const receivedResultImagesPage = createAppAsyncThunk(
perPage: IMAGES_PER_PAGE,
});
galleryLog.info({ response }, `Received ${response.items.length} results`);
return response;
}
);
@@ -25,6 +30,8 @@ export const receivedUploadImagesPage = createAppAsyncThunk(
perPage: IMAGES_PER_PAGE,
});
galleryLog.info({ response }, `Received ${response.items.length} uploads`);
return response;
}
);

View File

@@ -1,10 +1,13 @@
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import { log } from 'app/logging/useLogger';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { clamp } from 'lodash-es';
import { clamp, isString } from 'lodash-es';
import { ImagesService } from 'services/api';
import { getHeaders } from 'services/util/getHeaders';
const imagesLog = log.child({ namespace: 'image' });
type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0];
/**
@@ -14,6 +17,9 @@ export const imageReceived = createAppAsyncThunk(
'api/imageReceived',
async (arg: ImageReceivedArg, _thunkApi) => {
const response = await ImagesService.getImage(arg);
imagesLog.info({ arg, response }, 'Received image');
return response;
}
);
@@ -29,6 +35,9 @@ export const thumbnailReceived = createAppAsyncThunk(
'api/thumbnailReceived',
async (arg: ThumbnailReceivedArg, _thunkApi) => {
const response = await ImagesService.getThumbnail(arg);
imagesLog.info({ arg, response }, 'Received thumbnail');
return response;
}
);
@@ -43,6 +52,12 @@ export const imageUploaded = createAppAsyncThunk(
async (arg: ImageUploadedArg, _thunkApi) => {
const response = await ImagesService.uploadImage(arg);
const { location } = getHeaders(response);
imagesLog.info(
{ arg: '<Blob>', response, location },
`Image uploaded (${response.image_name})`
);
return { response, location };
}
);
@@ -70,7 +85,7 @@ export const imageDeleted = createAppAsyncThunk(
// Determine which image should replace the deleted image, if the deleted image is the selected image.
// Unfortunately, we have to do this here, because the resultsSlice and uploadsSlice cannot change
// the selected image.
const selectedImageName = getState().gallery.selectedImageName;
const selectedImageName = getState().gallery.selectedImage?.name;
if (selectedImageName === imageName) {
const allIds = getState()[imageType].ids;
@@ -89,13 +104,22 @@ export const imageDeleted = createAppAsyncThunk(
const newSelectedImageId = filteredIds[newSelectedImageIndex];
dispatch(
imageSelected(newSelectedImageId ? newSelectedImageId.toString() : '')
);
if (newSelectedImageId) {
dispatch(
imageSelected({ name: newSelectedImageId as string, type: imageType })
);
} else {
dispatch(imageSelected());
}
}
const response = await ImagesService.deleteImage(arg);
imagesLog.info(
{ arg, response },
`Image deleted (${arg.imageType} - ${arg.imageName})`
);
return response;
}
);

View File

@@ -1,14 +1,18 @@
import { log } from 'app/logging/useLogger';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { Model } from 'features/system/store/modelSlice';
import { reduce } from 'lodash-es';
import { reduce, size } from 'lodash-es';
import { ModelsService } from 'services/api';
const models = log.child({ namespace: 'model' });
export const IMAGES_PER_PAGE = 20;
export const receivedModels = createAppAsyncThunk(
'models/receivedModels',
async (_arg) => {
async (_) => {
const response = await ModelsService.listModels();
const deserializedModels = reduce(
response.models,
(modelsAccumulator, model, modelName) => {
@@ -19,6 +23,8 @@ export const receivedModels = createAppAsyncThunk(
{} as Record<string, Model>
);
models.info({ response }, `Received ${size(response.models)} models`);
return deserializedModels;
}
);

View File

@@ -1,16 +1,19 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { log } from 'app/logging/useLogger';
import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice';
import { OpenAPIV3 } from 'openapi-types';
const schemaLog = log.child({ namespace: 'schema' });
export const receivedOpenAPISchema = createAsyncThunk(
'nodes/receivedOpenAPISchema',
async (_, { dispatch }): Promise<OpenAPIV3.Document> => {
const response = await fetch(`openapi.json`);
const openAPISchema = (await response.json()) as OpenAPIV3.Document;
const openAPISchema = await response.json();
console.debug('OpenAPI schema: ', openAPISchema);
schemaLog.info({ openAPISchema }, 'Received OpenAPI schema');
dispatch(parsedOpenAPISchema(openAPISchema));
dispatch(parsedOpenAPISchema(openAPISchema as OpenAPIV3.Document));
return openAPISchema;
}

View File

@@ -3,26 +3,42 @@ import { SessionsService } from 'services/api';
import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph';
import { isAnyOf, isFulfilled } from '@reduxjs/toolkit';
import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/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 }) => {
const graph = buildGenerateGraph(getState());
dispatch(sessionCreated({ graph }));
return graph;
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 }) => {
const graph = buildNodesGraph(getState());
dispatch(sessionCreated({ graph }));
return graph;
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);
}
}
);
@@ -43,12 +59,12 @@ type SessionCreatedArg = {
export const sessionCreated = createAppAsyncThunk(
'api/sessionCreated',
async (arg: SessionCreatedArg, { dispatch, getState }) => {
console.log('Session created, graph: ', arg.graph);
const response = await SessionsService.createSession({
requestBody: arg.graph,
});
sessionLog.info({ arg, response }, `Session created (${response.id})`);
return response;
}
);
@@ -74,6 +90,8 @@ export const nodeAdded = createAppAsyncThunk(
sessionId: arg.sessionId,
});
sessionLog.info({ arg, response }, `Node added (${response})`);
return response;
}
);
@@ -91,6 +109,8 @@ export const sessionInvoked = createAppAsyncThunk(
all: true,
});
sessionLog.info({ arg, response }, `Session invoked (${sessionId})`);
return response;
}
);
@@ -111,6 +131,8 @@ export const sessionCanceled = createAppAsyncThunk(
sessionId,
});
sessionLog.info({ arg, response }, `Session canceled (${sessionId})`);
return response;
}
);
@@ -127,6 +149,11 @@ export const listedSessions = createAppAsyncThunk(
async (arg: SessionsListedArg, _thunkApi) => {
const response = await SessionsService.listSessions(arg);
sessionLog.info(
{ arg, response },
`Sessions listed (${response.items.length})`
);
return response;
}
);

View File

@@ -1,3 +1,5 @@
import { Image } from 'app/types/invokeai';
import { get, isObject, isString } from 'lodash-es';
import {
GraphExecutionState,
GraphInvocationOutput,
@@ -6,6 +8,8 @@ import {
PromptOutput,
IterateInvocationOutput,
CollectInvocationOutput,
ImageType,
ImageField,
} from 'services/api';
export const isImageOutput = (
@@ -31,3 +35,16 @@ export const isIterateOutput = (
export const isCollectOutput = (
output: GraphExecutionState['results'][string]
): output is CollectInvocationOutput => output.type === 'collect_output';
export const isImageType = (t: unknown): t is ImageType =>
isString(t) && ['results', 'uploads', 'intermediates'].includes(t);
export const isImage = (image: unknown): image is Image =>
isObject(image) &&
isString(get(image, 'name')) &&
isImageType(get(image, 'type'));
export const isImageField = (imageField: unknown): imageField is ImageField =>
isObject(imageField) &&
isString(get(imageField, 'image_name')) &&
isImageType(get(imageField, 'image_type'));

View File

@@ -57,6 +57,13 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.1.2":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/types@^7.21.4", "@babel/types@^7.4":
version "7.21.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4"
@@ -1421,6 +1428,15 @@
redux-thunk "^2.4.2"
reselect "^4.1.8"
"@roarr/browser-log-writer@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@roarr/browser-log-writer/-/browser-log-writer-1.1.5.tgz#755ff62ddaa297bb3488067408a7085db382352b"
integrity sha512-yLn//DRjh1/rUgZpZkwmT/5RqHYfkdOwGXWXnKBR3l/HE04DIhSVeYin3sc8aWHBa7s7WglQpYX/uw/WI6POpw==
dependencies:
boolean "^3.1.4"
globalthis "^1.0.2"
liqe "^3.6.0"
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
@@ -1828,6 +1844,11 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/js-cookie@^2.2.6":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3"
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
@@ -2039,6 +2060,11 @@
dependencies:
"@swc/core" "^1.3.42"
"@xobotyi/scrollbar-width@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
@@ -2077,7 +2103,7 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv@^6.10.0, ajv@^6.12.4, ajv@~6.12.6:
ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@~6.12.6:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2307,6 +2333,11 @@ bl@^4.1.0:
inherits "^2.0.4"
readable-stream "^3.4.0"
boolean@^3.1.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b"
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
boxen@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
@@ -2553,6 +2584,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
code-block-writer@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
@@ -2609,7 +2645,7 @@ commander@^10.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^2.16.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1:
commander@^2.16.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -2671,7 +2707,7 @@ convert-source-map@^1.5.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
copy-to-clipboard@3.3.3:
copy-to-clipboard@3.3.3, copy-to-clipboard@^3.3.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
@@ -2722,7 +2758,22 @@ css-box-model@1.2.1:
dependencies:
tiny-invariant "^1.0.6"
csstype@^3.0.11, csstype@^3.0.2:
css-in-js-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb"
integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==
dependencies:
hyphenate-style-name "^1.0.3"
css-tree@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
csstype@^3.0.11, csstype@^3.0.2, csstype@^3.0.6:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
@@ -2828,6 +2879,11 @@ deepmerge@^2.1.1:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
deepmerge@^4.2.2:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
defaults@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
@@ -3031,6 +3087,11 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
discontinuous-range@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -3118,6 +3179,13 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.6:
version "2.1.4"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286"
integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==
dependencies:
stackframe "^1.3.4"
es-abstract@^1.19.0, es-abstract@^1.20.4:
version "1.21.2"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff"
@@ -3442,11 +3510,48 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-json-stringify@^2.7.10:
version "2.7.13"
resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz#277aa86c2acba4d9851bd6108ed657aa327ed8c0"
integrity sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==
dependencies:
ajv "^6.11.0"
deepmerge "^4.2.2"
rfdc "^1.2.0"
string-similarity "^4.0.1"
fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-loops@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.3.tgz#ce96adb86d07e7bf9b4822ab9c6fac9964981f75"
integrity sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==
fast-memoize@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
fast-printf@^1.6.9:
version "1.6.9"
resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676"
integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==
dependencies:
boolean "^3.1.4"
fast-shallow-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fastest-stable-stringify@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76"
integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==
fastq@^1.6.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
@@ -3768,7 +3873,7 @@ globals@^13.19.0:
dependencies:
type-fest "^0.20.2"
globalthis@^1.0.3:
globalthis@^1.0.2, globalthis@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
@@ -3925,6 +4030,11 @@ husky@^8.0.3:
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
i18next-browser-languagedetector@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz#ead34592edc96c6c3a618a51cb57ad027c5b5d87"
@@ -4017,6 +4127,14 @@ ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
inline-style-prefixer@^6.0.0:
version "6.0.4"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz#4290ed453ab0e4441583284ad86e41ad88384f44"
integrity sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==
dependencies:
css-in-js-utils "^3.1.0"
fast-loops "^1.1.3"
internal-slot@^1.0.3, internal-slot@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986"
@@ -4309,6 +4427,11 @@ jju@~1.4.0:
resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
js-sdsl@^4.1.4:
version "4.4.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
@@ -4465,6 +4588,14 @@ lint-staged@^13.2.2:
string-argv "^0.3.1"
yaml "^2.2.2"
liqe@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/liqe/-/liqe-3.6.0.tgz#2d05376e93ff9f4bfdb3e76481f6456d165b499f"
integrity sha512-CYVQr0bk5CCTkX3wW2MdyEWdr9FHLpiE/1cQXQ36Sdjn5gv7JIpm9jnkovFwiVzumw7f6JDFXpljwUY+fAcFYQ==
dependencies:
nearley "^2.20.1"
ts-error "^1.0.6"
listr2@^5.0.7:
version "5.0.8"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.8.tgz#a9379ffeb4bd83a68931a65fb223a11510d6ba23"
@@ -4618,6 +4749,11 @@ make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -4714,11 +4850,30 @@ module-lookup-amd@^7.0.1:
requirejs "^2.3.5"
requirejs-config-file "^4.0.0"
moo@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nano-css@^5.3.1:
version "5.3.5"
resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.5.tgz#3075ea29ffdeb0c7cb6d25edb21d8f7fa8e8fe8e"
integrity sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==
dependencies:
css-tree "^1.1.2"
csstype "^3.0.6"
fastest-stable-stringify "^2.0.2"
inline-style-prefixer "^6.0.0"
rtl-css-js "^1.14.0"
sourcemap-codec "^1.4.8"
stacktrace-js "^2.0.2"
stylis "^4.0.6"
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
@@ -4734,6 +4889,16 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
nearley@^2.20.1:
version "2.20.1"
resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==
dependencies:
commander "^2.19.0"
moo "^0.5.0"
railroad-diagrams "^1.0.0"
randexp "0.4.6"
neo-async@^2.6.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
@@ -5215,6 +5380,19 @@ quote-unquote@^1.0.0:
resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b"
integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==
railroad-diagrams@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
dependencies:
discontinuous-range "1.0.0"
ret "~0.1.10"
rc@1.2.8, rc@^1.2.7, rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -5225,6 +5403,13 @@ rc@1.2.8, rc@^1.2.7, rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
re-resizable@6.9.6:
version "6.9.6"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.6.tgz#b95d37e3821481b56ddfb1e12862940a791e827d"
integrity sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==
dependencies:
fast-memoize "^2.5.1"
re-resizable@^6.9.9:
version "6.9.9"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216"
@@ -5250,6 +5435,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-draggable@4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"
react-dropzone@^14.2.3:
version "14.2.3"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"
@@ -5366,6 +5559,15 @@ react-remove-scroll@^2.5.5:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-rnd@^10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.4.1.tgz#9e1c3f244895d7862ef03be98b2a620848c3fba1"
integrity sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q==
dependencies:
re-resizable "6.9.6"
react-draggable "4.4.5"
tslib "2.3.1"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@@ -5385,6 +5587,31 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
react-use@^17.4.0:
version "17.4.0"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.4.0.tgz#cefef258b0a6c534a5c8021c2528ac6e1a4cdc6d"
integrity sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==
dependencies:
"@types/js-cookie" "^2.2.6"
"@xobotyi/scrollbar-width" "^1.9.5"
copy-to-clipboard "^3.3.1"
fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.3.1"
react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.1.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^3.0.1"
ts-easing "^0.2.0"
tslib "^2.1.0"
react-zoom-pan-pinch@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.0.7.tgz#def52f6886bc11e1b160dedf4250aae95470b94d"
@@ -5503,6 +5730,11 @@ reselect@^4.1.8:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524"
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-dependency-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736"
@@ -5554,12 +5786,17 @@ restore-cursor@^3.1.0:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rfdc@^1.3.0:
rfdc@^1.2.0, rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
@@ -5578,6 +5815,18 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
roarr@^7.15.0:
version "7.15.0"
resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.0.tgz#09b792f0cd31b4a7f91030bb1c47550ceec98ee4"
integrity sha512-CV9WefQfUXTX6wr8CrEMhfNef3sjIt9wNhE/5PNu4tNWsaoDNDXqq+OGn/RW9A1UPb0qc7FQlswXRaJJJsqn8A==
dependencies:
boolean "^3.1.4"
fast-json-stringify "^2.7.10"
fast-printf "^1.6.9"
globalthis "^1.0.2"
safe-stable-stringify "^2.4.1"
semver-compare "^1.0.0"
rollup-plugin-visualizer@^5.9.0:
version "5.9.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz#013ac54fb6a9d7c9019e7eb77eced673399e5a0b"
@@ -5602,6 +5851,13 @@ rollup@^3.21.0:
optionalDependencies:
fsevents "~2.3.2"
rtl-css-js@^1.14.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80"
integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==
dependencies:
"@babel/runtime" "^7.1.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -5630,6 +5886,11 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3"
is-regex "^1.1.4"
safe-stable-stringify@^2.4.1:
version "2.4.3"
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
sass-lookup@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-3.0.0.tgz#3b395fa40569738ce857bc258e04df2617c48cac"
@@ -5644,6 +5905,16 @@ scheduler@^0.23.0:
dependencies:
loose-envify "^1.1.0"
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
@@ -5675,6 +5946,18 @@ semver@~7.3.0:
dependencies:
lru-cache "^6.0.0"
serialize-error@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-11.0.0.tgz#0129f2b07b19b09bc7a5f2d850ffe9cd2d561582"
integrity sha512-YKrURWDqcT3VGX/s/pCwaWtpfJEEaEw5Y4gAnQDku92b/HjVj4r4UhA5QrMVMFotymK2wIWs5xthny5SMFu7Vw==
dependencies:
type-fest "^2.12.2"
set-harmonic-interval@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249"
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -5773,6 +6056,11 @@ source-map-support@~0.5.20:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
source-map@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -5788,6 +6076,11 @@ source-map@^0.7.4:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spawn-command@0.0.2-1:
version "0.0.2-1"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
@@ -5798,6 +6091,35 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
stack-generator@^2.0.5:
version "2.0.10"
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==
dependencies:
stackframe "^1.3.4"
stackframe@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
stacktrace-gps@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz#0c40b24a9b119b20da4525c398795338966a2fb0"
integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==
dependencies:
source-map "0.5.6"
stackframe "^1.3.4"
stacktrace-js@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b"
integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==
dependencies:
error-stack-parser "^2.0.6"
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
stream-to-array@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
@@ -5810,6 +6132,11 @@ string-argv@^0.3.1, string-argv@~0.3.1:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
string-similarity@^4.0.1:
version "4.0.4"
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -5919,7 +6246,7 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
stylis@4.1.4:
stylis@4.1.4, stylis@^4.0.6:
version "4.1.4"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.4.tgz#9cb60e7153d8ac6d02d773552bf51c7a0344535b"
integrity sha512-USf5pszRYwuE6hg9by0OkKChkQYEXfkeTtm0xKw+jqQhwyjCVLdYyMBK7R+n7dhzsblAWJnGxju4vxq5eH20GQ==
@@ -5978,6 +6305,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -6032,6 +6364,16 @@ tree-kill@^1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
ts-error@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/ts-error/-/ts-error-1.0.6.tgz#277496f2a28de6c184cfce8dfd5cdd03a4e6b0fc"
integrity sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==
ts-graphviz@^1.5.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.6.1.tgz#f44525c048cb8c8c188b7324d2a91015fd31ceaf"
@@ -6093,6 +6435,11 @@ tsconfig-paths@^4.0.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tslib@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
@@ -6139,6 +6486,11 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-fest@^2.12.2:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
typed-array-length@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"