diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 6ee6a9c8f48..d9e4f0af5ce 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -88,13 +88,16 @@ "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" diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d3ccbcb3957..876cd96b39f 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -527,6 +527,7 @@ "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.", @@ -645,5 +646,9 @@ "betaDarkenOutside": "Darken Outside", "betaLimitToBox": "Limit To Box", "betaPreserveMasked": "Preserve Masked" + }, + "ui": { + "showProgressImages": "Show Progress Images", + "hideProgressImages": "Hide Progress Images" } } diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 37d0c7ba721..3aebfa40975 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -27,6 +27,7 @@ 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 = {}; @@ -64,7 +65,7 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => { }, []); return ( - + {isLightboxEnabled && } @@ -120,6 +121,7 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => { + ); }; diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index a66f2ef2a3d..97a8be6fc1c 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -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 ( diff --git a/invokeai/frontend/web/src/app/constants.ts b/invokeai/frontend/web/src/app/constants.ts index 083f57f26f6..534ca9e29a7 100644 --- a/invokeai/frontend/web/src/app/constants.ts +++ b/invokeai/frontend/web/src/app/constants.ts @@ -1,7 +1,5 @@ // TODO: use Enums? -import { InProgressImageType } from 'features/system/store/systemSlice'; - export const DIFFUSERS_SCHEDULERS: Array = [ '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; diff --git a/invokeai/frontend/web/src/app/socketio/actions.ts b/invokeai/frontend/web/src/app/socketio/actions.ts index 923c1f59b0e..bb2a0dd0cbb 100644 --- a/invokeai/frontend/web/src/app/socketio/actions.ts +++ b/invokeai/frontend/web/src/app/socketio/actions.ts @@ -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( - 'socketio/generateImage' -); -export const runESRGAN = createAction('socketio/runESRGAN'); -export const runFacetool = createAction( - 'socketio/runFacetool' -); -export const deleteImage = createAction( - 'socketio/deleteImage' -); -export const requestImages = createAction( - 'socketio/requestImages' -); -export const requestNewImages = createAction( - 'socketio/requestNewImages' -); -export const cancelProcessing = createAction( - 'socketio/cancelProcessing' -); +// export const generateImage = createAction( +// 'socketio/generateImage' +// ); +// export const runESRGAN = createAction('socketio/runESRGAN'); +// export const runFacetool = createAction( +// 'socketio/runFacetool' +// ); +// export const deleteImage = createAction( +// 'socketio/deleteImage' +// ); +// export const requestImages = createAction( +// 'socketio/requestImages' +// ); +// export const requestNewImages = createAction( +// 'socketio/requestNewImages' +// ); +// export const cancelProcessing = createAction( +// 'socketio/cancelProcessing' +// ); -export const requestSystemConfig = createAction( - 'socketio/requestSystemConfig' -); +// export const requestSystemConfig = createAction( +// 'socketio/requestSystemConfig' +// ); -export const searchForModels = createAction('socketio/searchForModels'); +// export const searchForModels = createAction('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('socketio/deleteModel'); +// export const deleteModel = createAction('socketio/deleteModel'); -export const convertToDiffusers = - createAction( - 'socketio/convertToDiffusers' - ); +// export const convertToDiffusers = +// createAction( +// 'socketio/convertToDiffusers' +// ); -export const mergeDiffusersModels = - createAction( - 'socketio/mergeDiffusersModels' - ); +// export const mergeDiffusersModels = +// createAction( +// 'socketio/mergeDiffusersModels' +// ); -export const requestModelChange = createAction( - 'socketio/requestModelChange' -); +// export const requestModelChange = createAction( +// 'socketio/requestModelChange' +// ); -export const saveStagingAreaImageToGallery = createAction( - 'socketio/saveStagingAreaImageToGallery' -); +// export const saveStagingAreaImageToGallery = createAction( +// 'socketio/saveStagingAreaImageToGallery' +// ); -export const emptyTempFolder = createAction( - 'socketio/requestEmptyTempFolder' -); +// export const emptyTempFolder = createAction( +// 'socketio/requestEmptyTempFolder' +// ); + +export default {}; diff --git a/invokeai/frontend/web/src/app/socketio/emitters.ts b/invokeai/frontend/web/src/app/socketio/emitters.ts index 610f05b8263..ad7979503f0 100644 --- a/invokeai/frontend/web/src/app/socketio/emitters.ts +++ b/invokeai/frontend/web/src/app/socketio/emitters.ts @@ -1,207 +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 { - 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, 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)); - - const state: RootState = getState(); - - const { - generation: generationState, - postprocessing: postprocessingState, - system: systemState, - canvas: canvasState, - } = state; - - const frontendToBackendParametersConfig: FrontendToBackendParametersConfig = - { - generationMode, - generationState, - postprocessingState, - canvasState, - systemState, - }; - - dispatch(generationRequested()); - - const { generationParameters, esrganParameters, facetoolParameters } = - frontendToBackendParameters(frontendToBackendParametersConfig); - - 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('...'); - } - - 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 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 facetoolParameters: Record = { - facetool_strength: facetoolStrength, - }; - - 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'); - }, - }; -}; - -export default makeSocketIOEmitters; +// 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, 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)); + +// const state: RootState = getState(); + +// const { +// generation: generationState, +// postprocessing: postprocessingState, +// system: systemState, +// canvas: canvasState, +// } = state; + +// const frontendToBackendParametersConfig: FrontendToBackendParametersConfig = +// { +// generationMode, +// generationState, +// postprocessingState, +// canvasState, +// systemState, +// }; + +// dispatch(generationRequested()); + +// const { generationParameters, esrganParameters, facetoolParameters } = +// frontendToBackendParameters(frontendToBackendParametersConfig); + +// 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('...'); +// } + +// 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 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 facetoolParameters: Record = { +// facetool_strength: facetoolStrength, +// }; + +// 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'); +// }, +// }; +// }; + +// export default makeSocketIOEmitters; + +export default {}; diff --git a/invokeai/frontend/web/src/app/socketio/listeners.ts b/invokeai/frontend/web/src/app/socketio/listeners.ts index de2f86fd4c6..cb6db260fc1 100644 --- a/invokeai/frontend/web/src/app/socketio/listeners.ts +++ b/invokeai/frontend/web/src/app/socketio/listeners.ts @@ -1,500 +1,502 @@ -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 { - 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 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, 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; - - 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'))); - - 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, - }; - - 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 (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; - // } - // } - // } - - 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: `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 - } - - 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. - */ - - // Generate a UUID for each image - const preparedImages = images.map((image): InvokeAI._Image => { - return { - uuid: uuidv4(), - ...image, - }; - }); - - 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()); - - 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()); - } - - 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 references to image in options - const { - generation: { initialImage, maskPath }, - } = getState(); - - if ( - initialImage === url || - (initialImage as InvokeAI._Image)?.url === url - ) { - dispatch(clearInitialImage()); - } - - 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, - }) - ); - }, - }; -}; - -export default makeSocketIOListeners; +// 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 { +// 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 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, 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; + +// 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'))); + +// 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, +// }; + +// 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 (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; +// // } +// // } +// // } + +// 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: `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 +// } + +// 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. +// */ + +// // Generate a UUID for each image +// const preparedImages = images.map((image): InvokeAI._Image => { +// return { +// uuid: uuidv4(), +// ...image, +// }; +// }); + +// 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()); + +// 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()); +// } + +// 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 references to image in options +// const { +// generation: { initialImage, maskPath }, +// } = getState(); + +// if ( +// initialImage === url || +// (initialImage as InvokeAI._Image)?.url === url +// ) { +// dispatch(clearInitialImage()); +// } + +// 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, +// }) +// ); +// }, +// }; +// }; + +// export default makeSocketIOListeners; + +export default {}; diff --git a/invokeai/frontend/web/src/app/socketio/middleware.ts b/invokeai/frontend/web/src/app/socketio/middleware.ts index 74752dc980f..88013ea2228 100644 --- a/invokeai/frontend/web/src/app/socketio/middleware.ts +++ b/invokeai/frontend/web/src/app/socketio/middleware.ts @@ -1,246 +1,248 @@ -import { Middleware } from '@reduxjs/toolkit'; -import { io } from 'socket.io-client'; - -import makeSocketIOEmitters from './emitters'; -import makeSocketIOListeners from './listeners'; - -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); - - const socketio = io(origin, { - timeout: 60000, - path: `${window.location.pathname}socket.io`, - }); - - socketio.disconnect(); - - 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 { - 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()); - - socketio.on('disconnect', () => onDisconnect()); - - socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data)); - - socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) => - onGenerationResult(data) - ); - - socketio.on( - 'postprocessingResult', - (data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data) - ); - - socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) => - onIntermediateResult(data) - ); - - socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) => - onProgressUpdate(data) - ); - - socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) => - onGalleryImages(data) - ); - - socketio.on('processingCanceled', () => { - onProcessingCanceled(); - }); - - socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => { - onImageDeleted(data); - }); - - socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => { - onSystemConfig(data); - }); - - socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => { - onFoundModels(data); - }); - - socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => { - onNewModelAdded(data); - }); - - socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => { - onModelDeleted(data); - }); - - socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => { - onModelConverted(data); - }); - - socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => { - onModelsMerged(data); - }); - - socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => { - onModelChanged(data); - }); - - socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => { - onModelChangeFailed(data); - }); - - socketio.on('tempFolderEmptied', () => { - onTempFolderEmptied(); - }); - - areListenersSet = true; - } - - /** - * 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/runFacetool': { - emitRunFacetool(action.payload); - break; - } - - case 'socketio/deleteImage': { - emitDeleteImage(action.payload); - break; - } - - case 'socketio/requestImages': { - emitRequestImages(action.payload); - break; - } - - case 'socketio/requestNewImages': { - emitRequestNewImages(action.payload); - break; - } - - case 'socketio/cancelProcessing': { - emitCancelProcessing(); - break; - } - - case 'socketio/requestSystemConfig': { - emitRequestSystemConfig(); - break; - } - - case 'socketio/searchForModels': { - emitSearchForModels(action.payload); - break; - } - - case 'socketio/addNewModel': { - emitAddNewModel(action.payload); - break; - } - - case 'socketio/deleteModel': { - emitDeleteModel(action.payload); - break; - } - - case 'socketio/convertToDiffusers': { - emitConvertToDiffusers(action.payload); - break; - } - - case 'socketio/mergeDiffusersModels': { - emitMergeDiffusersModels(action.payload); - break; - } - - case 'socketio/requestModelChange': { - emitRequestModelChange(action.payload); - break; - } - - case 'socketio/saveStagingAreaImageToGallery': { - emitSaveStagingAreaImageToGallery(action.payload); - break; - } - - case 'socketio/requestEmptyTempFolder': { - emitRequestEmptyTempFolder(); - break; - } - } - - next(action); - }; - - return middleware; -}; +// import { Middleware } from '@reduxjs/toolkit'; +// import { io } from 'socket.io-client'; + +// import makeSocketIOEmitters from './emitters'; +// import makeSocketIOListeners from './listeners'; + +// 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); + +// const socketio = io(origin, { +// timeout: 60000, +// path: `${window.location.pathname}socket.io`, +// }); + +// socketio.disconnect(); + +// 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 { +// 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()); + +// socketio.on('disconnect', () => onDisconnect()); + +// socketio.on('error', (data: InvokeAI.ErrorResponse) => onError(data)); + +// socketio.on('generationResult', (data: InvokeAI.ImageResultResponse) => +// onGenerationResult(data) +// ); + +// socketio.on( +// 'postprocessingResult', +// (data: InvokeAI.ImageResultResponse) => onPostprocessingResult(data) +// ); + +// socketio.on('intermediateResult', (data: InvokeAI.ImageResultResponse) => +// onIntermediateResult(data) +// ); + +// socketio.on('progressUpdate', (data: InvokeAI.SystemStatus) => +// onProgressUpdate(data) +// ); + +// socketio.on('galleryImages', (data: InvokeAI.GalleryImagesResponse) => +// onGalleryImages(data) +// ); + +// socketio.on('processingCanceled', () => { +// onProcessingCanceled(); +// }); + +// socketio.on('imageDeleted', (data: InvokeAI.ImageDeletedResponse) => { +// onImageDeleted(data); +// }); + +// socketio.on('systemConfig', (data: InvokeAI.SystemConfig) => { +// onSystemConfig(data); +// }); + +// socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => { +// onFoundModels(data); +// }); + +// socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => { +// onNewModelAdded(data); +// }); + +// socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => { +// onModelDeleted(data); +// }); + +// socketio.on('modelConverted', (data: InvokeAI.ModelConvertedResponse) => { +// onModelConverted(data); +// }); + +// socketio.on('modelsMerged', (data: InvokeAI.ModelsMergedResponse) => { +// onModelsMerged(data); +// }); + +// socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => { +// onModelChanged(data); +// }); + +// socketio.on('modelChangeFailed', (data: InvokeAI.ModelChangeResponse) => { +// onModelChangeFailed(data); +// }); + +// socketio.on('tempFolderEmptied', () => { +// onTempFolderEmptied(); +// }); + +// areListenersSet = true; +// } + +// /** +// * 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/runFacetool': { +// emitRunFacetool(action.payload); +// break; +// } + +// case 'socketio/deleteImage': { +// emitDeleteImage(action.payload); +// break; +// } + +// case 'socketio/requestImages': { +// emitRequestImages(action.payload); +// break; +// } + +// case 'socketio/requestNewImages': { +// emitRequestNewImages(action.payload); +// break; +// } + +// case 'socketio/cancelProcessing': { +// emitCancelProcessing(); +// break; +// } + +// case 'socketio/requestSystemConfig': { +// emitRequestSystemConfig(); +// break; +// } + +// case 'socketio/searchForModels': { +// emitSearchForModels(action.payload); +// break; +// } + +// case 'socketio/addNewModel': { +// emitAddNewModel(action.payload); +// break; +// } + +// case 'socketio/deleteModel': { +// emitDeleteModel(action.payload); +// break; +// } + +// case 'socketio/convertToDiffusers': { +// emitConvertToDiffusers(action.payload); +// break; +// } + +// case 'socketio/mergeDiffusersModels': { +// emitMergeDiffusersModels(action.payload); +// break; +// } + +// case 'socketio/requestModelChange': { +// emitRequestModelChange(action.payload); +// break; +// } + +// case 'socketio/saveStagingAreaImageToGallery': { +// emitSaveStagingAreaImageToGallery(action.payload); +// break; +// } + +// case 'socketio/requestEmptyTempFolder': { +// emitRequestEmptyTempFolder(); +// break; +// } +// } + +// next(action); +// }; + +// return middleware; +// }; + +export default {}; diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 627c4f0063e..e9e104dac27 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -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'; @@ -30,6 +28,8 @@ import { nodesDenylist } from 'features/nodes/store/nodesPersistDenylist'; import { postprocessingDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; import { systemDenylist } from 'features/system/store/systemPersistDenylist'; import { uiDenylist } from 'features/ui/store/uiPersistDenylist'; +import { 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,12 +73,10 @@ const rootPersistConfig = getPersistConfig({ ...modelsDenylist, ...nodesDenylist, ...postprocessingDenylist, - // ...resultsDenylist, - 'results', + ...resultsDenylist, ...systemDenylist, ...uiDenylist, - // ...uploadsDenylist, - 'uploads', + ...uploadsDenylist, 'hotkeys', 'config', ], @@ -87,13 +85,13 @@ const rootPersistConfig = getPersistConfig({ 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, diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 27ca9dc4a6a..05e6e088d6f 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -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>; - 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; diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index 44f039c433d..48080e89704 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -233,7 +233,7 @@ const IAISlider = (props: IAIFullSliderProps) => { hidden={hideTooltip} {...sliderTooltipProps} > - + diff --git a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx index a78ced06ea2..45a45e37d38 100644 --- a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx @@ -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 ( { - const isImageToImageEnabled = useAppSelector( - (state: RootState) => state.generation.isImageToImageEnabled - ); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx index cbcd86d8d68..eeb51d955b9 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts index d9feba63d35..e14871e11b3 100644 --- a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts +++ b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts @@ -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, diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 3cfe932b756..4fef811d46a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -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) => { > } > {t('parameters.sendToImg2Img')} @@ -570,16 +501,16 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} 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} /> } 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} /> { 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 && ( - ) : !imageToDisplay.isProgressImage ? ( - - ) : undefined - } + src={shouldHidePreview ? undefined : getUrl(image.url)} + width={image.metadata.width} + height={image.metadata.height} + fallback={shouldHidePreview ? : undefined} sx={{ objectFit: 'contain', maxWidth: '100%', maxHeight: '100%', height: 'auto', position: 'absolute', - imageRendering: imageToDisplay.isProgressImage - ? 'pixelated' - : 'initial', borderRadius: 'base', }} /> )} + {shouldShowImageDetails && image && 'metadata' in image && ( + + + + )} {!shouldShowImageDetails && } - {shouldShowImageDetails && - imageToDisplay && - 'metadata' in imageToDisplay.image && ( - - - - )} ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 6ad8e876427..d0ff9aee400 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -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) => { + 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) => { - 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) => { menuProps={{ size: 'sm', isLazy: true }} renderMenu={() => ( - + } onClickCapture={handleOpenInNewTab} > {t('common.openInNewTab')} - {!disabledFeatures.includes('lightbox') && ( + {isLightboxEnabled && ( } onClickCapture={handleLightBox}> {t('parameters.openInViewer')} )} } - onClickCapture={handleUsePrompt} + onClickCapture={handleRecallPrompt} isDisabled={image?.metadata?.invokeai?.node?.prompt === undefined} > {t('parameters.usePrompt')} @@ -276,14 +221,14 @@ const HoverableImage = memo((props: HoverableImageProps) => { } - onClickCapture={handleUseSeed} + onClickCapture={handleRecallSeed} isDisabled={image?.metadata?.invokeai?.node?.seed === undefined} > {t('parameters.useSeed')} } - onClickCapture={handleUseInitialImage} + onClickCapture={handleRecallInitialImage} isDisabled={image?.metadata?.invokeai?.node?.type !== 'img2img'} > {t('parameters.useInitImg')} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 1b57dbea784..6524452e90f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -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 = () => { } - onClick={() => dispatch(setCurrentCategory('result'))} + onClick={() => dispatch(setCurrentCategory('results'))} /> } - onClick={() => dispatch(setCurrentCategory('user'))} + onClick={() => dispatch(setCurrentCategory('uploads'))} /> ) : ( <> dispatch(setCurrentCategory('result'))} + isChecked={currentCategory === 'results'} + onClick={() => dispatch(setCurrentCategory('results'))} flexGrow={1} > {t('gallery.generations')} 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 ( { [shouldPinGallery] ); - useHotkeys( - 'left', - () => { - dispatch(selectPrevImage()); - }, - { - enabled: !isStaging || activeTabName !== 'unifiedCanvas', - }, - [isStaging, activeTabName] - ); - - useHotkeys( - 'right', - () => { - dispatch(selectNextImage()); - }, - { - enabled: !isStaging || activeTabName !== 'unifiedCanvas', - }, - [isStaging, activeTabName] - ); - useHotkeys( 'shift+g', () => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index 073521344d9..eedbf630815 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -159,6 +159,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { _dark: { bg: 'blackAlpha.600', }, + overflow: 'scroll', }} > diff --git a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx index 7c878a94854..d0d25f8bc67 100644 --- a/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/NextPrevImageButtons.tsx @@ -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(false); - const handleCurrentImagePreviewMouseOver = () => { + const handleCurrentImagePreviewMouseOver = useCallback(() => { setShouldShowNextPrevButtons(true); - }; + }, []); - const handleCurrentImagePreviewMouseOut = () => { + const handleCurrentImagePreviewMouseOut = useCallback(() => { setShouldShowNextPrevButtons(false); - }; + }, []); + + const handlePrevImage = useCallback(() => { + dispatch(imageSelected(prevImage)); + }, [dispatch, prevImage]); - const handleClickPrevButton = () => { - dispatch(selectPrevImage()); - }; + const handleNextImage = useCallback(() => { + dispatch(imageSelected(nextImage)); + }, [dispatch, nextImage]); - const handleClickNextButton = () => { - dispatch(selectNextImage()); - }; + useHotkeys( + 'left', + () => { + handlePrevImage(); + }, + [prevImage] + ); + + useHotkeys( + 'right', + () => { + handleNextImage(); + }, + [nextImage] + ); return ( { aria-label={t('accessibility.previousImage')} icon={} variant="unstyled" - onClick={handleClickPrevButton} + onClick={handlePrevImage} boxSize={16} sx={nextPrevButtonStyles} /> @@ -119,7 +154,7 @@ const NextPrevImageButtons = () => { aria-label={t('accessibility.nextImage')} icon={} variant="unstyled" - onClick={handleClickNextButton} + onClick={handleNextImage} boxSize={16} sx={nextPrevButtonStyles} /> diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index ebb27e12d9e..0fc8d300e93 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -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); } } ); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 4d752a151b8..418d95734db 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -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; - 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 - */ - selectedImageName: string; - /** - * The currently selected image - * @deprecated See `state.gallery.selectedImageName` + * The selected image */ - 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) => { - state.selectedImageName = action.payload; - }, - setCurrentImage: (state, action: PayloadAction) => { - state.currentImage = action.payload; - state.currentImageUuid = action.payload.uuid; - }, - removeImage: ( - state, - action: PayloadAction - ) => { - 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: ( + imageSelected: ( state, - action: PayloadAction<{ - image: InvokeAI._Image; - category: GalleryCategory; - }> + action: PayloadAction ) => { - 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) => { - 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) => { state.galleryImageMinimumWidth = action.payload; @@ -267,7 +53,10 @@ export const gallerySlice = createSlice({ setShouldAutoSwitchToNewImages: (state, action: PayloadAction) => { state.shouldAutoSwitchToNewImages = action.payload; }, - setCurrentCategory: (state, action: PayloadAction) => { + setCurrentCategory: ( + state, + action: PayloadAction<'results' | 'uploads'> + ) => { state.currentCategory = action.payload; }, setGalleryWidth: (state, action: PayloadAction) => { @@ -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; diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts index ef21f4b7b20..b62a199b338 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts @@ -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}` diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index 73da68c031b..26af366e039 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -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); } }); diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts index ec4248e99c7..6e2ac1c3aa7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts @@ -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}` diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index 04d321d7d91..bb77844f42c 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -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); }); /** diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx index 2082c2a0159..9682d2eb0b2 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx @@ -5,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 = () => { } /> - {isLoaded && ( - - )} + {isLoaded && } )} {!initialImage?.url && } diff --git a/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx b/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx index a15759cd1ff..da9262ac0f0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AnimatedImageToImagePanel.tsx @@ -15,9 +15,9 @@ const AnimatedImageToImagePanel = () => { {isImageToImageEnabled && ( diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx index a71e8a36381..4f6c2ecc1c1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx @@ -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] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index e028fe4f8dd..d2002eb04f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -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 { @@ -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={} 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" diff --git a/invokeai/frontend/web/src/features/parameters/components/ProgressImage.tsx b/invokeai/frontend/web/src/features/parameters/components/ProgressImage.tsx new file mode 100644 index 00000000000..869f4dbc632 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/ProgressImage.tsx @@ -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 ? ( + + + + ) : ( + + + + ); +}; + +export default memo(ProgressImage); diff --git a/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx new file mode 100644 index 00000000000..9a54e37463d --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx @@ -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 ( + <> + {' '} + + dispatch(setShouldShowProgressImages(!showProgressWindow)) + } + tooltip={t('ui.showProgressImages')} + isChecked={showProgressWindow} + sx={{ + position: 'absolute', + bottom: 4, + insetInlineStart: 4, + }} + aria-label={t('ui.showProgressImages')} + icon={} + /> + {showProgressWindow && ( + { + dispatch(floatingProgressImageMoved({ x: d.x, y: d.y })); + }} + onResizeStop={(e, direction, ref, delta, position) => { + const newRect: Partial = {}; + + 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)); + }} + > + + + + + Progress Images + + + dispatch(setShouldShowProgressImages(false))} + aria-label={t('ui.hideProgressImages')} + size="xs" + icon={} + variant="ghost" + /> + + + + + )} + + ); +}; + +export default memo(ProgressImagePreview); diff --git a/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx b/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx index 13095ffefad..b54106733c5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx @@ -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(null); @@ -58,7 +59,8 @@ const PromptInput = () => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && e.shiftKey === false && isReady) { e.preventDefault(); - dispatch(generateImage(activeTabName)); + dispatch(clampSymmetrySteps()); + dispatch(generateGraphBuilt()); } }; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts new file mode 100644 index 00000000000..7c45f159b28 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts @@ -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 }; +}; diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts index ce3c9c4e1ed..dbf5eec7910 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts @@ -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); + } } ); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index f303491b2b6..9d9d689cb04 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -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) => { state.verticalSymmetrySteps = action.payload; }, - initialImageSelected: (state, action: PayloadAction) => { + initialImageSelected: (state, action: PayloadAction) => { state.initialImage = action.payload; state.isImageToImageEnabled = true; }, diff --git a/invokeai/frontend/web/src/features/system/components/ClearTempFolderButtonModal.tsx b/invokeai/frontend/web/src/features/system/components/ClearTempFolderButtonModal.tsx index 353eddc323e..a220c93b3ff 100644 --- a/invokeai/frontend/web/src/features/system/components/ClearTempFolderButtonModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/ClearTempFolderButtonModal.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx index 71d2b68a86d..bb5db0302da 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/AddCheckpointModel.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/AddDiffusersModel.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/AddDiffusersModel.tsx index 5a22472fc42..cb3af5f176c 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/AddDiffusersModel.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/AddDiffusersModel.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/CheckpointModelEdit.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/CheckpointModelEdit.tsx index 3523e6fab7d..b860a0848cd 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/CheckpointModelEdit.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/CheckpointModelEdit.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/DiffusersModelEdit.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/DiffusersModelEdit.tsx index f996d5a5d67..81998e4976c 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/DiffusersModelEdit.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/DiffusersModelEdit.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx index 47e9277a599..6ba148cac41 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/MergeModels.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/ModelConvert.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/ModelConvert.tsx index 3a5aa1264b3..820ad546b3a 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/ModelConvert.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/ModelConvert.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/ModelListItem.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/ModelListItem.tsx index 47d139cc8fc..aa9f87816ca 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/ModelListItem.tsx @@ -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'; diff --git a/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx b/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx index a7867efd5b4..3a99997ac85 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelManager/SearchModels.tsx @@ -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, diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index a806ef262b2..0ca0b496fc4 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -1,7 +1,6 @@ import { ChakraProps, Flex, - Grid, Heading, Modal, ModalBody, @@ -14,64 +13,57 @@ 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 { consoleLogLevelChanged, - InProgressImageType, 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, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { InvokeLogLevel, VALID_LOG_LEVELS } from 'app/logging/useLogger'; +import { VALID_LOG_LEVELS } from 'app/logging/useLogger'; import { LogLevelName } from 'roarr'; -import { F } from 'ts-toolbelt'; 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, }; @@ -104,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, @@ -119,13 +109,12 @@ const SettingsModal = ({ children }: SettingsModalProps) => { } = useDisclosure(); const { - shouldDisplayInProgressType, shouldConfirmOnDelete, shouldDisplayGuides, - saveIntermediatesInterval, enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, + shouldAutoShowProgressImages, consoleLogLevel, shouldLogToConsole, } = useAppSelector(selector); @@ -134,18 +123,12 @@ const SettingsModal = ({ children }: SettingsModalProps) => { * Resets localstorage, then opens a secondary modal informing user to * refresh their browser. * */ - const handleClickResetWebUI = () => { + const handleClickResetWebUI = useCallback(() => { persistor.purge().then(() => { onSettingsModalClose(); onRefreshModalOpen(); }); - }; - - const handleChangeIntermediateSteps = (value: number) => { - if (value > steps) value = steps; - if (value < 1) value = 1; - dispatch(setSaveIntermediatesInterval(value)); - }; + }, [onSettingsModalClose, onRefreshModalOpen]); const handleLogLevelChanged = useCallback( (e: ChangeEvent) => { @@ -182,32 +165,6 @@ const SettingsModal = ({ children }: SettingsModalProps) => { {t('settings.general')} - ) => - dispatch( - setShouldDisplayInProgressType( - e.target.value as InProgressImageType - ) - ) - } - /> - {shouldDisplayInProgressType === 'full-res' && ( - - )} { dispatch(setShouldUseSliders(e.target.checked)) } /> + ) => + dispatch(setShouldAutoShowProgressImages(e.target.checked)) + } + /> diff --git a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx index 03d8934f450..48418c9f198 100644 --- a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx @@ -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; + if (isConnected) { + return 'ok'; + } - const statusIndicatorCursor = - hasError && !wasErrorSeen ? 'pointer' : 'initial'; + return 'error'; + }, [isProcessing, isConnected]); - const handleClickStatusIndicator = () => { - if (hasError || !wasErrorSeen) { - dispatch(errorSeen()); + const iterationsText = useMemo(() => { + if (!(currentIteration && totalIterations)) { + return; } - }; + + return ` (${currentIteration}/${totalIterations})`; + }, [currentIteration, totalIterations]); + + const isHovered = useHoverDirty(ref); return ( - - - {t(statusMessage as keyof typeof t)} - - + + + {isHovered && ( + + + {t(statusTranslationKey as ResourceKey)} + {iterationsText} + + + )} + + + ); }; diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 84176bd096e..a74e6dca7e9 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -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,7 +14,6 @@ import { socketUnsubscribed, } from 'services/events/actions'; -import i18n from 'i18n'; import { ProgressImage } from 'services/events/types'; import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { makeToast } from '../hooks/useToastWatcher'; @@ -22,53 +22,29 @@ 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; - 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 */ @@ -102,47 +78,28 @@ export interface SystemState */ consoleLogLevel: InvokeLogLevel; shouldLogToConsole: boolean; + statusTranslationKey: TFuncKey; + canceledSession: string; } const initialSystemState: SystemState = { isConnected: false, isProcessing: false, - 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', @@ -152,29 +109,21 @@ const initialSystemState: SystemState = { wasSchemaParsed: false, consoleLogLevel: 'error', shouldLogToConsole: true, + statusTranslationKey: 'common.statusDisconnected', + canceledSession: '', }; export const systemSlice = createSlice({ name: 'system', initialState: initialSystemState, reducers: { - setShouldDisplayInProgressType: ( - state, - action: PayloadAction - ) => { - state.shouldDisplayInProgressType = action.payload; - }, setIsProcessing: (state, action: PayloadAction) => { state.isProcessing = action.payload; }, - setCurrentStatus: (state, action: PayloadAction) => { - state.currentStatus = action.payload; - }, - setSystemStatus: (state, action: PayloadAction) => { - return { ...state, ...action.payload }; + setCurrentStatus: (state, action: PayloadAction) => { + state.statusTranslationKey = action.payload; }, errorOccurred: (state) => { - state.hasError = true; state.isProcessing = false; state.isCancelable = true; state.currentStep = 0; @@ -182,18 +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'); - }, - setShouldShowLogViewer: (state, action: PayloadAction) => { - state.shouldShowLogViewer = action.payload; + state.statusTranslationKey = 'common.statusError'; }, setIsConnected: (state, action: PayloadAction) => { state.isConnected = action.payload; @@ -204,23 +142,10 @@ export const systemSlice = createSlice({ state.currentIteration = 0; state.totalIterations = 0; state.currentStatusHasSteps = false; - state.hasError = false; - }, - setSocketId: (state, action: PayloadAction) => { - state.socketId = action.payload; }, setShouldConfirmOnDelete: (state, action: PayloadAction) => { state.shouldConfirmOnDelete = action.payload; }, - setOpenAccordions: (state, action: PayloadAction) => { - state.openAccordions = action.payload; - }, - setSystemConfig: (state, action: PayloadAction) => { - return { - ...state, - ...action.payload, - }; - }, setShouldDisplayGuides: (state, action: PayloadAction) => { state.shouldDisplayGuides = action.payload; }, @@ -232,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; @@ -242,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> - ) => { - state.model_list = action.payload; + state.statusTranslationKey = 'common.statusPreparing'; }, setIsCancelable: (state, action: PayloadAction) => { 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) => { - state.saveIntermediatesInterval = action.payload; - }, setEnableImageDebugging: (state, action: PayloadAction) => { state.enableImageDebugging = action.payload; }, @@ -283,9 +199,12 @@ export const systemSlice = createSlice({ clearToastQueue: (state) => { state.toastQueue = []; }, - setProcessingIndeterminateTask: (state, action: PayloadAction) => { + setProcessingIndeterminateTask: ( + state, + action: PayloadAction + ) => { state.isProcessing = true; - state.currentStatus = action.payload; + state.statusTranslationKey = action.payload; state.currentStatusHasSteps = false; }, setSearchFolder: (state, action: PayloadAction) => { @@ -300,12 +219,6 @@ export const systemSlice = createSlice({ setOpenModel: (state, action: PayloadAction) => { state.openModel = action.payload; }, - setCancelType: (state, action: PayloadAction) => { - state.cancelOptions.cancelType = action.payload; - }, - setCancelAfter: (state, action: PayloadAction) => { - state.cancelOptions.cancelAfter = action.payload; - }, /** * A cancel was scheduled */ @@ -321,7 +234,7 @@ export const systemSlice = createSlice({ /** * The cancel type was changed */ - cancelTypeChanged: (state, action: PayloadAction) => { + cancelTypeChanged: (state, action: PayloadAction) => { state.cancelType = action.payload; }, /** @@ -343,6 +256,7 @@ export const systemSlice = createSlice({ */ builder.addCase(socketSubscribed, (state, action) => { state.sessionId = action.payload.sessionId; + state.canceledSession = ''; }); /** @@ -357,9 +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.isCancelable = true; + state.isProcessing = false; + state.currentStatusHasSteps = false; + state.currentStep = 0; + state.totalSteps = 0; + state.currentIteration = 0; + state.totalIterations = 0; + state.statusTranslationKey = 'common.statusConnected'; }); /** @@ -369,17 +289,28 @@ export const systemSlice = createSlice({ const { timestamp } = action.payload; state.isConnected = false; - state.currentStatus = i18n.t('common.statusDisconnected'); + 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'; }); /** @@ -395,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'; }); /** @@ -407,11 +343,17 @@ 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'; + + if (state.canceledSession === data.graph_execution_state_id) { + state.isProcessing = false; + state.isCancelable = true; + } }); /** @@ -420,12 +362,17 @@ export const systemSlice = createSlice({ builder.addCase(invocationError, (state, action) => { const { data, timestamp } = action.payload; - 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' }) ); }); @@ -434,7 +381,7 @@ export const systemSlice = createSlice({ */ builder.addCase(sessionInvoked.pending, (state) => { - state.currentStatus = i18n.t('common.statusPreparing'); + state.statusTranslationKey = 'common.statusPreparing'; }); /** @@ -443,23 +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' }) ); }); + /** + * 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'))); }); /** @@ -479,26 +441,17 @@ export const systemSlice = createSlice({ }); export const { - setShouldDisplayInProgressType, setIsProcessing, - setShouldShowLogViewer, setIsConnected, - setSocketId, setShouldConfirmOnDelete, - setOpenAccordions, - setSystemStatus, setCurrentStatus, - setSystemConfig, setShouldDisplayGuides, processingCanceled, errorOccurred, - errorSeen, - setModelList, setIsCancelable, modelChangeRequested, modelConvertRequested, modelMergingRequested, - setSaveIntermediatesInterval, setEnableImageDebugging, generationRequested, addToast, @@ -507,8 +460,6 @@ export const { setSearchFolder, setFoundModels, setOpenModel, - setCancelType, - setCancelAfter, cancelScheduled, scheduledCancelAborted, cancelTypeChanged, diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx index 53fdcb4a499..de7b7389565 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx @@ -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 ( `ui.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 965fa21eb07..11abf6a20d1 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -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) => { + state.floatingProgressImageRect = { + ...state.floatingProgressImageRect, + ...action.payload, + }; + }, + floatingProgressImageResized: ( + state, + action: PayloadAction> + ) => { + state.floatingProgressImageRect = { + ...state.floatingProgressImageRect, + ...action.payload, + }; + }, + setShouldShowProgressImages: (state, action: PayloadAction) => { + state.shouldShowProgressImages = action.payload; + }, + setShouldAutoShowProgressImages: ( + state, + action: PayloadAction + ) => { + state.shouldAutoShowProgressImages = action.payload; + }, }, }); @@ -128,6 +155,10 @@ export const { toggleParametersPanel, toggleGalleryPanel, openAccordionItemsChanged, + floatingProgressImageMoved, + floatingProgressImageResized, + setShouldShowProgressImages, + setShouldAutoShowProgressImages, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 66e85ce71bc..bdcf0a3c301 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -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; } diff --git a/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts b/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts index b923479feae..a0f92e01b7d 100644 --- a/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts +++ b/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts @@ -22,15 +22,13 @@ export interface OnCancel { } export class CancelablePromise implements Promise { - readonly [Symbol.toStringTag]!: string; - - private _isResolved: boolean; - private _isRejected: boolean; - private _isCancelled: boolean; - private readonly _cancelHandlers: (() => void)[]; - private readonly _promise: Promise; - private _resolve?: (value: T | PromiseLike) => void; - private _reject?: (reason?: any) => void; + #isResolved: boolean; + #isRejected: boolean; + #isCancelled: boolean; + readonly #cancelHandlers: (() => void)[]; + readonly #promise: Promise; + #resolve?: (value: T | PromiseLike) => void; + #reject?: (reason?: any) => void; constructor( executor: ( @@ -39,78 +37,82 @@ export class CancelablePromise implements Promise { onCancel: OnCancel ) => void ) { - this._isResolved = false; - this._isRejected = false; - this._isCancelled = false; - this._cancelHandlers = []; - this._promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; + this.#isResolved = false; + this.#isRejected = false; + this.#isCancelled = false; + this.#cancelHandlers = []; + this.#promise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; const onResolve = (value: T | PromiseLike): 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( onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, onRejected?: ((reason: any) => TResult2 | PromiseLike) | null ): Promise { - return this._promise.then(onFulfilled, onRejected); + return this.#promise.then(onFulfilled, onRejected); } public catch( onRejected?: ((reason: any) => TResult | PromiseLike) | null ): Promise { - return this._promise.catch(onRejected); + return this.#promise.catch(onRejected); } public finally(onFinally?: (() => void) | null): Promise { - 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 implements Promise { 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; } } diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index f1b84f84657..2a348377154 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -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'; diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts index 1e590e4ba90..57a9178290e 100644 --- a/invokeai/frontend/web/src/services/api/models/Graph.ts +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -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; + nodes?: Record; /** * The connections between nodes and their fields in this graph */ diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts index 8210f01bb6f..d04885bf857 100644 --- a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts @@ -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 */ diff --git a/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts new file mode 100644 index 00000000000..c0fabb49846 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ResizeLatentsInvocation.ts @@ -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; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts new file mode 100644 index 00000000000..f398eaf4085 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ScaleLatentsInvocation.ts @@ -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; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts index 63754db163c..217b917f18c 100644 --- a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts @@ -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 */ diff --git a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts index b431011ba6e..6fd8117db8b 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts @@ -33,6 +33,10 @@ export const $Graph = { type: 'TextToLatentsInvocation', }, { type: 'LatentsToImageInvocation', + }, { + type: 'ResizeLatentsInvocation', + }, { + type: 'ScaleLatentsInvocation', }, { type: 'AddInvocation', }, { diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts index d27fdc7c1fd..b20ee88a52e 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts @@ -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`, diff --git a/invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts new file mode 100644 index 00000000000..2609b1a6817 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ResizeLatentsInvocation.ts @@ -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; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts new file mode 100644 index 00000000000..8d4d15e2e81 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ScaleLatentsInvocation.ts @@ -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; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts index 7b6dd155ca0..06376824c62 100644 --- a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts @@ -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`, diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts index 269092c6d96..dad455fc805 100644 --- a/invokeai/frontend/web/src/services/api/services/SessionsService.ts +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -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 { 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 { return __request(OpenAPI, { method: 'PUT', diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index e57851e19c8..dab2e756f38 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -7,15 +7,9 @@ import { } from 'services/events/types'; import { invocationComplete, - socketConnected, - socketDisconnected, 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 { @@ -23,14 +17,12 @@ import { isFulfilledSessionCreatedAction, } 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 moduleLog = log.child({ namespace: 'socketio' }); +const socketioLog = log.child({ namespace: 'socketio' }); export const socketMiddleware = () => { let areListenersSet = false; @@ -65,106 +57,27 @@ export const socketMiddleware = () => { (store: MiddlewareAPI) => (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', () => { - moduleLog.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) { - const sessionLog = moduleLog.child({ sessionId: system.sessionId }); - - sessionLog.debug( - `Subscribed to existing session (${system.sessionId})` - ); - - socket.emit('subscribe', { session: system.sessionId }); - dispatch( - socketSubscribed({ - sessionId: system.sessionId, - timestamp: getTimestamp(), - }) - ); - setEventListeners({ socket, store, sessionLog }); - } - }); - - socket.on('disconnect', () => { - moduleLog.debug('Disconnected'); - 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 = moduleLog.child({ sessionId }); + 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) { sessionLog.debug( { oldSessionId }, `Unsubscribed from old session (${oldSessionId})` ); - // Unsubscribe when invocations complete + socket.emit('unsubscribe', { session: oldSessionId, }); @@ -175,28 +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); - }); } sessionLog.debug(`Subscribe to new session (${sessionId})`); + socket.emit('subscribe', { session: sessionId }); + dispatch( socketSubscribed({ sessionId: sessionId, timestamp: getTimestamp(), }) ); - setEventListeners({ socket, store, sessionLog }); // Finally we actually invoke the session, starting processing dispatch(sessionInvoked({ sessionId })); @@ -222,7 +125,6 @@ export const socketMiddleware = () => { } } - // Always pass the action on so other middleware and reducers can handle it next(action); }; diff --git a/invokeai/frontend/web/src/services/events/types.ts b/invokeai/frontend/web/src/services/events/types.ts index 573dc0ac3a0..2577b7fe929 100644 --- a/invokeai/frontend/web/src/services/events/types.ts +++ b/invokeai/frontend/web/src/services/events/types.ts @@ -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[string]; export type AnyResult = GraphExecutionState['results'][string]; +export type BaseNode = { + id: string; + type: string; + [key: string]: NonNullable[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; }; diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index 90a285d2382..89bbc717a33 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -9,38 +9,140 @@ import { 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; store: MiddlewareAPI; - sessionLog: Logger; + log: Logger; }; export const setEventListeners = (arg: SetEventListenersArg) => { - const { socket, store, sessionLog } = 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) => { - sessionLog.child({ data }).info(`Invocation started (${data.node.type})`); + 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) => { - sessionLog.child({ data }).trace(`Generator progress (${data.node.type})`); + 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) => { - sessionLog.child({ data }).error(`Invocation error (${data.node.type})`); + 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) => { - sessionLog.child({ data }).info(`Invocation complete (${data.node.type})`); + 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; @@ -60,12 +162,14 @@ export const setEventListeners = (arg: SetEventListenersArg) => { ); }); + /** + * Graph complete + */ socket.on('graph_execution_state_complete', (data) => { - sessionLog - .child({ data }) - .info( - `Graph execution state complete (${data.graph_execution_state_id})` - ); + log.info( + { data, sessionId: data.graph_execution_state_id }, + `Graph execution state complete (${data.graph_execution_state_id})` + ); dispatch(graphExecutionStateComplete({ data, timestamp: getTimestamp() })); }); }; diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index c4da9d9f16d..a0d8f504b79 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -2,7 +2,7 @@ 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'; @@ -85,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; @@ -104,9 +104,13 @@ 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); diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index 0641437d527..af03045967d 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -4,28 +4,41 @@ import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/line 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); + } } ); diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts index 5a9d891395c..72cf1108fbb 100644 --- a/invokeai/frontend/web/src/services/types/guards.ts +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -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')); diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index f117d4f2de6..b60071ab657 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -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" @@ -1837,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" @@ -2048,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" @@ -2567,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" @@ -2685,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== @@ -2736,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== @@ -3142,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" @@ -3481,6 +3525,16 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~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" @@ -3488,6 +3542,16 @@ fast-printf@^1.6.9: 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" @@ -3966,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" @@ -4058,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" @@ -4350,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" @@ -4667,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" @@ -4773,6 +4860,20 @@ ms@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" @@ -5302,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" @@ -5327,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" @@ -5443,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" @@ -5462,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" @@ -5580,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" @@ -5696,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" @@ -5743,6 +5905,11 @@ 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" @@ -5779,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" @@ -5877,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" @@ -5892,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" @@ -5902,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" @@ -6028,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== @@ -6087,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" @@ -6141,6 +6364,11 @@ 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" @@ -6207,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" @@ -6253,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"