From 6962536b4abc45b61368224d0341402b3a3a83ae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:52:51 +1000 Subject: [PATCH] refactor(ui): use zod for all redux state (wip) needed for confidence w/ state rehydration logic --- invokeai/frontend/web/src/app/store/types.ts | 13 +- .../frontend/web/src/app/types/invokeai.ts | 390 +++++++++++++----- .../changeBoardModal/store/initialState.ts | 6 - .../features/changeBoardModal/store/slice.ts | 13 +- .../features/changeBoardModal/store/types.ts | 4 - .../store/canvasSettingsSlice.ts | 68 +-- .../controlLayers/store/canvasSlice.ts | 9 +- .../store/canvasStagingAreaSlice.ts | 45 +- .../features/controlLayers/store/filters.ts | 2 +- .../controlLayers/store/paramsSlice.ts | 11 +- .../controlLayers/store/refImagesSlice.ts | 3 +- .../src/features/controlLayers/store/types.ts | 215 ++++++---- .../store/dynamicPromptsSlice.ts | 37 +- .../src/features/system/store/configSlice.ts | 188 +-------- .../web/src/features/ui/store/uiSlice.ts | 41 +- .../web/src/features/ui/store/uiTypes.ts | 37 +- 16 files changed, 577 insertions(+), 505 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts delete mode 100644 invokeai/frontend/web/src/features/changeBoardModal/store/types.ts diff --git a/invokeai/frontend/web/src/app/store/types.ts b/invokeai/frontend/web/src/app/store/types.ts index 8ba22916b1..8787c93e36 100644 --- a/invokeai/frontend/web/src/app/store/types.ts +++ b/invokeai/frontend/web/src/app/store/types.ts @@ -1,10 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Slice } from '@reduxjs/toolkit'; import type { UndoableOptions } from 'redux-undo'; +import type { ZodType } from 'zod'; type StateFromSlice = T extends Slice ? U : never; -export type SliceConfig = { +export type SliceConfig = { + /** + * The redux slice (return of createSlice). + */ slice: T; + /** + * The zod schema for the slice. + */ + zSchema: ZodType>; /** * A function that returns the initial state of the slice. */ @@ -18,7 +27,7 @@ export type SliceConfig = { * @param state The rehydrated state. * @returns A correctly-shaped state. */ - migrate: (state: unknown) => StateFromSlice; + migrate: (state: any) => StateFromSlice; /** * Keys to omit from the persisted state. */ diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index afa3a402aa..02be210de7 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -1,130 +1,300 @@ -import type { FilterType } from 'features/controlLayers/store/filters'; -import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas'; -import type { TabName } from 'features/ui/store/uiTypes'; +import { zFilterType } from 'features/controlLayers/store/filters'; +import { zParameterPrecision, zParameterScheduler } from 'features/parameters/types/parameterSchemas'; +import { zTabName } from 'features/ui/store/uiTypes'; import type { PartialDeep } from 'type-fest'; +import z from 'zod'; -/** - * A disable-able application feature - */ -export type AppFeature = - | 'faceRestore' - | 'upscaling' - | 'lightbox' - | 'modelManager' - | 'githubLink' - | 'discordLink' - | 'bugLink' - | 'aboutModal' - | 'localization' - | 'consoleLogging' - | 'dynamicPrompting' - | 'batches' - | 'syncModels' - | 'multiselect' - | 'pauseQueue' - | 'resumeQueue' - | 'invocationCache' - | 'modelCache' - | 'bulkDownload' - | 'starterModels' - | 'hfToken' - | 'retryQueueItem' - | 'cancelAndClearAll' - | 'chatGPT4oHigh' - | 'modelRelationships'; -/** - * A disable-able Stable Diffusion feature - */ -export type SDFeature = - | 'controlNet' - | 'noise' - | 'perlinNoise' - | 'noiseThreshold' - | 'variation' - | 'symmetry' - | 'seamless' - | 'hires' - | 'lora' - | 'embedding' - | 'vae' - | 'hrf'; +const zAppFeature = z.enum([ + 'faceRestore', + 'upscaling', + 'lightbox', + 'modelManager', + 'githubLink', + 'discordLink', + 'bugLink', + 'aboutModal', + 'localization', + 'consoleLogging', + 'dynamicPrompting', + 'batches', + 'syncModels', + 'multiselect', + 'pauseQueue', + 'resumeQueue', + 'invocationCache', + 'modelCache', + 'bulkDownload', + 'starterModels', + 'hfToken', + 'retryQueueItem', + 'cancelAndClearAll', + 'chatGPT4oHigh', + 'modelRelationships', +]); +export type AppFeature = z.infer; -export type NumericalParameterConfig = { - initial: number; - sliderMin: number; - sliderMax: number; - numberInputMin: number; - numberInputMax: number; - fineStep: number; - coarseStep: number; -}; +const zSDFeature = z.enum([ + 'controlNet', + 'noise', + 'perlinNoise', + 'noiseThreshold', + 'variation', + 'symmetry', + 'seamless', + 'hires', + 'lora', + 'embedding', + 'vae', + 'hrf', +]); +export type SDFeature = z.infer; + +const zNumericalParameterConfig = z.object({ + initial: z.number().default(512), + sliderMin: z.number().default(64), + sliderMax: z.number().default(1536), + numberInputMin: z.number().default(64), + numberInputMax: z.number().default(4096), + fineStep: z.number().default(8), + coarseStep: z.number().default(64), +}); +export type NumericalParameterConfig = z.infer; /** * Configuration options for the InvokeAI UI. * Distinct from system settings which may be changed inside the app. */ -export type AppConfig = { +export const zAppConfig = z.object({ /** * Whether or not we should update image urls when image loading errors */ - shouldUpdateImagesOnConnect: boolean; - shouldFetchMetadataFromApi: boolean; + shouldUpdateImagesOnConnect: z.boolean(), + shouldFetchMetadataFromApi: z.boolean(), /** * Sets a size limit for outputs on the upscaling tab. This is a maximum dimension, so the actual max number of pixels * will be the square of this value. */ - maxUpscaleDimension?: number; - allowPrivateBoards: boolean; - allowPrivateStylePresets: boolean; - allowClientSideUpload: boolean; - allowPublishWorkflows: boolean; - allowPromptExpansion: boolean; - disabledTabs: TabName[]; - disabledFeatures: AppFeature[]; - disabledSDFeatures: SDFeature[]; - nodesAllowlist: string[] | undefined; - nodesDenylist: string[] | undefined; - metadataFetchDebounce?: number; - workflowFetchDebounce?: number; - isLocal?: boolean; - shouldShowCredits: boolean; - sd: { - defaultModel?: string; - disabledControlNetModels: string[]; - disabledControlNetProcessors: FilterType[]; + maxUpscaleDimension: z.number().optional(), + allowPrivateBoards: z.boolean(), + allowPrivateStylePresets: z.boolean(), + allowClientSideUpload: z.boolean(), + allowPublishWorkflows: z.boolean(), + allowPromptExpansion: z.boolean(), + disabledTabs: z.array(zTabName), + disabledFeatures: z.array(zAppFeature), + disabledSDFeatures: z.array(zSDFeature), + nodesAllowlist: z.array(z.string()).optional(), + nodesDenylist: z.array(z.string()).optional(), + metadataFetchDebounce: z.number().int().optional(), + workflowFetchDebounce: z.number().int().optional(), + isLocal: z.boolean().optional(), + shouldShowCredits: z.boolean().optional(), + sd: z.object({ + defaultModel: z.string().optional(), + disabledControlNetModels: z.array(z.string()), + disabledControlNetProcessors: z.array(zFilterType), // Core parameters - iterations: NumericalParameterConfig; - width: NumericalParameterConfig; // initial value comes from model - height: NumericalParameterConfig; // initial value comes from model - steps: NumericalParameterConfig; - guidance: NumericalParameterConfig; - cfgRescaleMultiplier: NumericalParameterConfig; - img2imgStrength: NumericalParameterConfig; - scheduler?: ParameterScheduler; - vaePrecision?: ParameterPrecision; + iterations: zNumericalParameterConfig, + width: zNumericalParameterConfig, + height: zNumericalParameterConfig, + steps: zNumericalParameterConfig, + guidance: zNumericalParameterConfig, + cfgRescaleMultiplier: zNumericalParameterConfig, + img2imgStrength: zNumericalParameterConfig, + scheduler: zParameterScheduler.optional(), + vaePrecision: zParameterPrecision.optional(), // Canvas - boundingBoxHeight: NumericalParameterConfig; // initial value comes from model - boundingBoxWidth: NumericalParameterConfig; // initial value comes from model - scaledBoundingBoxHeight: NumericalParameterConfig; // initial value comes from model - scaledBoundingBoxWidth: NumericalParameterConfig; // initial value comes from model - canvasCoherenceStrength: NumericalParameterConfig; - canvasCoherenceEdgeSize: NumericalParameterConfig; - infillTileSize: NumericalParameterConfig; - infillPatchmatchDownscaleSize: NumericalParameterConfig; + boundingBoxHeight: zNumericalParameterConfig, + boundingBoxWidth: zNumericalParameterConfig, + scaledBoundingBoxHeight: zNumericalParameterConfig, + scaledBoundingBoxWidth: zNumericalParameterConfig, + canvasCoherenceStrength: zNumericalParameterConfig, + canvasCoherenceEdgeSize: zNumericalParameterConfig, + infillTileSize: zNumericalParameterConfig, + infillPatchmatchDownscaleSize: zNumericalParameterConfig, // Misc advanced - clipSkip: NumericalParameterConfig; // slider and input max are ignored for this, because the values depend on the model - maskBlur: NumericalParameterConfig; - hrfStrength: NumericalParameterConfig; - dynamicPrompts: { - maxPrompts: NumericalParameterConfig; - }; - ca: { - weight: NumericalParameterConfig; - }; - }; - flux: { - guidance: NumericalParameterConfig; - }; -}; + clipSkip: zNumericalParameterConfig, // slider and input max are ignored for this, because the values depend on the model + maskBlur: zNumericalParameterConfig, + hrfStrength: zNumericalParameterConfig, + dynamicPrompts: z.object({ + maxPrompts: zNumericalParameterConfig, + }), + ca: z.object({ + weight: zNumericalParameterConfig, + }), + }), + flux: z.object({ + guidance: zNumericalParameterConfig, + }), +}); +export type AppConfig = z.infer; export type PartialAppConfig = PartialDeep; + +export const getDefaultAppConfig = (): AppConfig => ({ + isLocal: true, + shouldUpdateImagesOnConnect: false, + shouldFetchMetadataFromApi: false, + allowPrivateBoards: false, + allowPrivateStylePresets: false, + allowClientSideUpload: false, + allowPublishWorkflows: false, + allowPromptExpansion: false, + shouldShowCredits: false, + disabledTabs: [], + disabledFeatures: ['lightbox', 'faceRestore', 'batches'] satisfies AppFeature[], + disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'] satisfies SDFeature[], + sd: { + disabledControlNetModels: [], + disabledControlNetProcessors: [], + iterations: { + initial: 1, + sliderMin: 1, + sliderMax: 1000, + numberInputMin: 1, + numberInputMax: 10000, + fineStep: 1, + coarseStep: 1, + }, + width: zNumericalParameterConfig.parse({}), // initial value comes from model + height: zNumericalParameterConfig.parse({}), // initial value comes from model + boundingBoxWidth: zNumericalParameterConfig.parse({}), // initial value comes from model + boundingBoxHeight: zNumericalParameterConfig.parse({}), // initial value comes from model + scaledBoundingBoxWidth: zNumericalParameterConfig.parse({}), // initial value comes from model + scaledBoundingBoxHeight: zNumericalParameterConfig.parse({}), // initial value comes from model + scheduler: 'dpmpp_3m_k' as const, + vaePrecision: 'fp32' as const, + steps: { + initial: 30, + sliderMin: 1, + sliderMax: 100, + numberInputMin: 1, + numberInputMax: 500, + fineStep: 1, + coarseStep: 1, + }, + guidance: { + initial: 7, + sliderMin: 1, + sliderMax: 20, + numberInputMin: 1, + numberInputMax: 200, + fineStep: 0.1, + coarseStep: 0.5, + }, + img2imgStrength: { + initial: 0.7, + sliderMin: 0, + sliderMax: 1, + numberInputMin: 0, + numberInputMax: 1, + fineStep: 0.01, + coarseStep: 0.05, + }, + canvasCoherenceStrength: { + initial: 0.3, + sliderMin: 0, + sliderMax: 1, + numberInputMin: 0, + numberInputMax: 1, + fineStep: 0.01, + coarseStep: 0.05, + }, + hrfStrength: { + initial: 0.45, + sliderMin: 0, + sliderMax: 1, + numberInputMin: 0, + numberInputMax: 1, + fineStep: 0.01, + coarseStep: 0.05, + }, + canvasCoherenceEdgeSize: { + initial: 16, + sliderMin: 0, + sliderMax: 128, + numberInputMin: 0, + numberInputMax: 1024, + fineStep: 8, + coarseStep: 16, + }, + cfgRescaleMultiplier: { + initial: 0, + sliderMin: 0, + sliderMax: 0.99, + numberInputMin: 0, + numberInputMax: 0.99, + fineStep: 0.05, + coarseStep: 0.1, + }, + clipSkip: { + initial: 0, + sliderMin: 0, + sliderMax: 12, // determined by model selection, unused in practice + numberInputMin: 0, + numberInputMax: 12, // determined by model selection, unused in practice + fineStep: 1, + coarseStep: 1, + }, + infillPatchmatchDownscaleSize: { + initial: 1, + sliderMin: 1, + sliderMax: 10, + numberInputMin: 1, + numberInputMax: 10, + fineStep: 1, + coarseStep: 1, + }, + infillTileSize: { + initial: 32, + sliderMin: 16, + sliderMax: 64, + numberInputMin: 16, + numberInputMax: 256, + fineStep: 1, + coarseStep: 1, + }, + maskBlur: { + initial: 16, + sliderMin: 0, + sliderMax: 128, + numberInputMin: 0, + numberInputMax: 512, + fineStep: 1, + coarseStep: 1, + }, + ca: { + weight: { + initial: 1, + sliderMin: 0, + sliderMax: 2, + numberInputMin: -1, + numberInputMax: 2, + fineStep: 0.01, + coarseStep: 0.05, + }, + }, + dynamicPrompts: { + maxPrompts: { + initial: 100, + sliderMin: 1, + sliderMax: 1000, + numberInputMin: 1, + numberInputMax: 10000, + fineStep: 1, + coarseStep: 10, + }, + }, + }, + flux: { + guidance: { + initial: 4, + sliderMin: 2, + sliderMax: 6, + numberInputMin: 1, + numberInputMax: 20, + fineStep: 0.1, + coarseStep: 0.5, + }, + }, +}); diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts deleted file mode 100644 index 0d7b2c3ec0..0000000000 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/initialState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ChangeBoardModalState } from './types'; - -export const initialState: ChangeBoardModalState = { - isModalOpen: false, - image_names: [], -}; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts index 5688d77857..fc652ec44f 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts +++ b/invokeai/frontend/web/src/features/changeBoardModal/store/slice.ts @@ -2,15 +2,19 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; -import { deepClone } from 'common/util/deepClone'; +import z from 'zod'; -import { initialState } from './initialState'; +const zChangeBoardModalState = z.object({ + isModalOpen: z.boolean().default(false), + image_names: z.array(z.string()).default(() => []), +}); +type ChangeBoardModalState = z.infer; -const getInitialState = () => deepClone(initialState); +const getInitialState = (): ChangeBoardModalState => zChangeBoardModalState.parse({}); const slice = createSlice({ name: 'changeBoardModal', - initialState, + initialState: getInitialState(), reducers: { isModalOpenChanged: (state, action: PayloadAction) => { state.isModalOpen = action.payload; @@ -31,5 +35,6 @@ export const selectChangeBoardModalSlice = (state: RootState) => state.changeBoa export const changeBoardModalSliceConfig: SliceConfig = { slice, + zSchema: zChangeBoardModalState, getInitialState, }; diff --git a/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts b/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts deleted file mode 100644 index c46a7aa7fa..0000000000 --- a/invokeai/frontend/web/src/features/changeBoardModal/store/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type ChangeBoardModalState = { - isModalOpen: boolean; - image_names: string[]; -}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts index 8af5852513..3587d07c1c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSettingsSlice.ts @@ -12,32 +12,32 @@ const zCanvasSettingsState = z.object({ /** * Whether to show HUD (Heads-Up Display) on the canvas. */ - showHUD: z.boolean().default(true), + showHUD: z.boolean(), /** * Whether to clip lines and shapes to the generation bounding box. If disabled, lines and shapes will be clipped to * the canvas bounds. */ - clipToBbox: z.boolean().default(false), + clipToBbox: z.boolean(), /** * Whether to show a dynamic grid on the canvas. If disabled, a checkerboard pattern will be shown instead. */ - dynamicGrid: z.boolean().default(false), + dynamicGrid: z.boolean(), /** * Whether to invert the scroll direction when adjusting the brush or eraser width with the scroll wheel. */ - invertScrollForToolWidth: z.boolean().default(false), + invertScrollForToolWidth: z.boolean(), /** * The width of the brush tool. */ - brushWidth: z.int().gt(0).default(50), + brushWidth: z.int().gt(0), /** * The width of the eraser tool. */ - eraserWidth: z.int().gt(0).default(50), + eraserWidth: z.int().gt(0), /** * The color to use when drawing lines or filling shapes. */ - color: zRgbaColor.default({ r: 31, g: 160, b: 224, a: 1 }), // invokeBlue.500 + color: zRgbaColor, /** * Whether to composite inpainted/outpainted regions back onto the source image when saving canvas generations. * @@ -45,55 +45,75 @@ const zCanvasSettingsState = z.object({ * * When `sendToCanvas` is disabled, this setting is ignored, masked regions will always be composited. */ - outputOnlyMaskedRegions: z.boolean().default(true), + outputOnlyMaskedRegions: z.boolean(), /** * Whether to automatically process the operations like filtering and auto-masking. */ - autoProcess: z.boolean().default(true), + autoProcess: z.boolean(), /** * The snap-to-grid setting for the canvas. */ - snapToGrid: z.boolean().default(true), + snapToGrid: z.boolean(), /** * Whether to show progress on the canvas when generating images. */ - showProgressOnCanvas: z.boolean().default(true), + showProgressOnCanvas: z.boolean(), /** * Whether to show the bounding box overlay on the canvas. */ - bboxOverlay: z.boolean().default(false), + bboxOverlay: z.boolean(), /** * Whether to preserve the masked region instead of inpainting it. */ - preserveMask: z.boolean().default(false), + preserveMask: z.boolean(), /** * Whether to show only raster layers while staging. */ - isolatedStagingPreview: z.boolean().default(true), + isolatedStagingPreview: z.boolean(), /** * Whether to show only the selected layer while filtering, transforming, or doing other operations. */ - isolatedLayerPreview: z.boolean().default(true), + isolatedLayerPreview: z.boolean(), /** * Whether to use pressure sensitivity for the brush and eraser tool when a pen device is used. */ - pressureSensitivity: z.boolean().default(true), + pressureSensitivity: z.boolean(), /** * Whether to show the rule of thirds composition guide overlay on the canvas. */ - ruleOfThirds: z.boolean().default(false), + ruleOfThirds: z.boolean(), /** * Whether to save all staging images to the gallery instead of keeping them as intermediate images. */ - saveAllImagesToGallery: z.boolean().default(false), + saveAllImagesToGallery: z.boolean(), /** * The auto-switch mode for the canvas staging area. */ - stagingAreaAutoSwitch: zAutoSwitchMode.default('switch_on_start'), + stagingAreaAutoSwitch: zAutoSwitchMode, }); type CanvasSettingsState = z.infer; -const getInitialState = () => zCanvasSettingsState.parse({}); +const getInitialState = (): CanvasSettingsState => ({ + showHUD: true, + clipToBbox: false, + dynamicGrid: false, + invertScrollForToolWidth: false, + brushWidth: 50, + eraserWidth: 50, + color: { r: 31, g: 160, b: 224, a: 1 }, // invokeBlue.500 + outputOnlyMaskedRegions: true, + autoProcess: true, + snapToGrid: true, + showProgressOnCanvas: true, + bboxOverlay: false, + preserveMask: false, + isolatedStagingPreview: true, + isolatedLayerPreview: true, + pressureSensitivity: true, + ruleOfThirds: false, + saveAllImagesToGallery: false, + stagingAreaAutoSwitch: 'switch_on_start' as const, +}); const slice = createSlice({ name: 'canvasSettings', @@ -187,16 +207,12 @@ export const { settingsStagingAreaAutoSwitchChanged, } = slice.actions; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - export const canvasSettingsSliceConfig: SliceConfig = { slice, + zSchema: zCanvasSettingsState, getInitialState, persistConfig: { - migrate, + migrate: (state) => zCanvasSettingsState.parse(state), }, }; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts index 1ad3541328..668ae31b47 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts @@ -80,6 +80,7 @@ import { isFLUXReduxConfig, isImagenAspectRatioID, isIPAdapterConfig, + zCanvasState, } from './types'; import { converters, @@ -1677,11 +1678,6 @@ export const { // inpaintMaskRecalled, } = slice.actions; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - const syncScaledSize = (state: CanvasState) => { if (API_BASE_MODELS.includes(state.bbox.modelBase)) { // Imagen3 has fixed sizes. Scaled bbox is not supported. @@ -1724,8 +1720,9 @@ const canvasUndoableConfig: UndoableOptions = { export const canvasSliceConfig: SliceConfig = { slice, getInitialState: getInitialCanvasState, + zSchema: zCanvasState, persistConfig: { - migrate, + migrate: (state) => zCanvasState.parse(state), }, undoableConfig: { reduxUndoOptions: canvasUndoableConfig, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts index e1356d2fef..e98fc75b87 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts @@ -3,24 +3,19 @@ import { EMPTY_ARRAY } from 'app/store/constants'; import type { RootState } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import type { SliceConfig } from 'app/store/types'; -import { deepClone } from 'common/util/deepClone'; import { getPrefixedId } from 'features/controlLayers/konva/util'; import { useMemo } from 'react'; import { queueApi } from 'services/api/endpoints/queue'; +import z from 'zod'; -type CanvasStagingAreaState = { - _version: 1; - canvasSessionId: string; - canvasDiscardedQueueItems: number[]; -}; +const zCanvasStagingAreaState = z.object({ + _version: z.literal(1).default(1), + canvasSessionId: z.string().default(() => getPrefixedId('canvas')), + canvasDiscardedQueueItems: z.array(z.number().int()).default(() => []), +}); +type CanvasStagingAreaState = z.infer; -const INITIAL_STATE: CanvasStagingAreaState = { - _version: 1, - canvasSessionId: getPrefixedId('canvas'), - canvasDiscardedQueueItems: [], -}; - -const getInitialState = (): CanvasStagingAreaState => deepClone(INITIAL_STATE); +const getInitialState = (): CanvasStagingAreaState => zCanvasStagingAreaState.parse({}); const slice = createSlice({ name: 'canvasSession', @@ -51,20 +46,22 @@ const slice = createSlice({ export const { canvasSessionReset, canvasQueueItemDiscarded } = slice.actions; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas'); - } - - return state; -}; - export const canvasSessionSliceConfig: SliceConfig = { slice, + zSchema: zCanvasStagingAreaState, getInitialState, - persistConfig: { migrate }, + persistConfig: { + migrate: (state) => { + { + if (!('_version' in state)) { + state._version = 1; + state.canvasSessionId = state.canvasSessionId ?? getPrefixedId('canvas'); + } + + return zCanvasStagingAreaState.parse(state); + } + }, + }, }; export const selectCanvasSessionSlice = (s: RootState) => s[slice.name]; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/filters.ts b/invokeai/frontend/web/src/features/controlLayers/store/filters.ts index 9373031e11..676a353f00 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/filters.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/filters.ts @@ -166,7 +166,7 @@ const _zFilterConfig = z.discriminatedUnion('type', [ ]); export type FilterConfig = z.infer; -const zFilterType = z.enum([ +export const zFilterType = z.enum([ 'adjust_image', 'canny_edge_detection', 'color_map', diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index 4d60a28387..893809b009 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -16,6 +16,7 @@ import { isChatGPT4oAspectRatioID, isFluxKontextAspectRatioID, isImagenAspectRatioID, + zParamsState, } from 'features/controlLayers/store/types'; import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; @@ -400,15 +401,13 @@ export const { paramsReset, } = slice.actions; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - return state; -}; - export const paramsSliceConfig: SliceConfig = { slice, + zSchema: zParamsState, getInitialState: getInitialParamsState, - persistConfig: { migrate }, + persistConfig: { + migrate: (state) => zParamsState.parse(state), + }, }; export const selectParamsSlice = (state: RootState) => state.params; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts index ae36aeac3f..f52bc2a980 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/refImagesSlice.ts @@ -19,7 +19,7 @@ import { assert } from 'tsafe'; import type { PartialDeep } from 'type-fest'; import type { CLIPVisionModelV2, IPMethodV2, RefImageState } from './types'; -import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig } from './types'; +import { getInitialRefImagesState, isFLUXReduxConfig, isIPAdapterConfig, zRefImagesState } from './types'; import { getReferenceImageState, imageDTOToImageWithDims, @@ -273,6 +273,7 @@ const migrate = (state: any): any => { export const refImagesSliceConfig: SliceConfig = { slice, + zSchema: zRefImagesState, getInitialState: getInitialRefImagesState, persistConfig: { migrate, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 85dc44e38f..2607c396cb 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -522,62 +522,108 @@ const zDimensionsState = z.object({ aspectRatio: zAspectRatioConfig, }); -const zParamsState = z.object({ - maskBlur: z.number().default(16), - maskBlurMethod: zParameterMaskBlurMethod.default('box'), - canvasCoherenceMode: zParameterCanvasCoherenceMode.default('Gaussian Blur'), - canvasCoherenceMinDenoise: zParameterStrength.default(0), - canvasCoherenceEdgeSize: z.number().default(16), - infillMethod: z.string().default('lama'), - infillTileSize: z.number().default(32), - infillPatchmatchDownscaleSize: z.number().default(1), - infillColorValue: zRgbaColor.default({ r: 0, g: 0, b: 0, a: 1 }), - cfgScale: zParameterCFGScale.default(7.5), - cfgRescaleMultiplier: zParameterCFGRescaleMultiplier.default(0), - guidance: zParameterGuidance.default(4), - img2imgStrength: zParameterStrength.default(0.75), - optimizedDenoisingEnabled: z.boolean().default(true), - iterations: z.number().default(1), - scheduler: zParameterScheduler.default('dpmpp_3m_k'), - upscaleScheduler: zParameterScheduler.default('kdpm_2'), - upscaleCfgScale: zParameterCFGScale.default(2), - seed: zParameterSeed.default(0), - shouldRandomizeSeed: z.boolean().default(true), - steps: zParameterSteps.default(30), - model: zParameterModel.nullable().default(null), - vae: zParameterVAEModel.nullable().default(null), - vaePrecision: zParameterPrecision.default('fp32'), - fluxVAE: zParameterVAEModel.nullable().default(null), - seamlessXAxis: z.boolean().default(false), - seamlessYAxis: z.boolean().default(false), - clipSkip: z.number().default(0), - shouldUseCpuNoise: z.boolean().default(true), - positivePrompt: zParameterPositivePrompt.default(''), - // Negative prompt may be disabled, in which case it will be null - negativePrompt: zParameterNegativePrompt.default(null), - positivePrompt2: zParameterPositiveStylePromptSDXL.default(''), - negativePrompt2: zParameterNegativeStylePromptSDXL.default(''), - shouldConcatPrompts: z.boolean().default(true), - refinerModel: zParameterSDXLRefinerModel.nullable().default(null), - refinerSteps: z.number().default(20), - refinerCFGScale: z.number().default(7.5), - refinerScheduler: zParameterScheduler.default('euler'), - refinerPositiveAestheticScore: z.number().default(6), - refinerNegativeAestheticScore: z.number().default(2.5), - refinerStart: z.number().default(0.8), - t5EncoderModel: zParameterT5EncoderModel.nullable().default(null), - clipEmbedModel: zParameterCLIPEmbedModel.nullable().default(null), - clipLEmbedModel: zParameterCLIPLEmbedModel.nullable().default(null), - clipGEmbedModel: zParameterCLIPGEmbedModel.nullable().default(null), - controlLora: zParameterControlLoRAModel.nullable().default(null), - dimensions: zDimensionsState.default({ - rect: { x: 0, y: 0, width: 512, height: 512 }, - aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG, - }), +export const zParamsState = z.object({ + maskBlur: z.number(), + maskBlurMethod: zParameterMaskBlurMethod, + canvasCoherenceMode: zParameterCanvasCoherenceMode, + canvasCoherenceMinDenoise: zParameterStrength, + canvasCoherenceEdgeSize: z.number(), + infillMethod: z.string(), + infillTileSize: z.number(), + infillPatchmatchDownscaleSize: z.number(), + infillColorValue: zRgbaColor, + cfgScale: zParameterCFGScale, + cfgRescaleMultiplier: zParameterCFGRescaleMultiplier, + guidance: zParameterGuidance, + img2imgStrength: zParameterStrength, + optimizedDenoisingEnabled: z.boolean(), + iterations: z.number(), + scheduler: zParameterScheduler, + upscaleScheduler: zParameterScheduler, + upscaleCfgScale: zParameterCFGScale, + seed: zParameterSeed, + shouldRandomizeSeed: z.boolean(), + steps: zParameterSteps, + model: zParameterModel.nullable(), + vae: zParameterVAEModel.nullable(), + vaePrecision: zParameterPrecision, + fluxVAE: zParameterVAEModel.nullable(), + seamlessXAxis: z.boolean(), + seamlessYAxis: z.boolean(), + clipSkip: z.number(), + shouldUseCpuNoise: z.boolean(), + positivePrompt: zParameterPositivePrompt, + negativePrompt: zParameterNegativePrompt, + positivePrompt2: zParameterPositiveStylePromptSDXL, + negativePrompt2: zParameterNegativeStylePromptSDXL, + shouldConcatPrompts: z.boolean(), + refinerModel: zParameterSDXLRefinerModel.nullable(), + refinerSteps: z.number(), + refinerCFGScale: z.number(), + refinerScheduler: zParameterScheduler, + refinerPositiveAestheticScore: z.number(), + refinerNegativeAestheticScore: z.number(), + refinerStart: z.number(), + t5EncoderModel: zParameterT5EncoderModel.nullable(), + clipEmbedModel: zParameterCLIPEmbedModel.nullable(), + clipLEmbedModel: zParameterCLIPLEmbedModel.nullable(), + clipGEmbedModel: zParameterCLIPGEmbedModel.nullable(), + controlLora: zParameterControlLoRAModel.nullable(), + dimensions: zDimensionsState, }); export type ParamsState = z.infer; -const INITIAL_PARAMS_STATE = zParamsState.parse({}); -export const getInitialParamsState = () => deepClone(INITIAL_PARAMS_STATE); +export const getInitialParamsState = (): ParamsState => ({ + maskBlur: 16, + maskBlurMethod: 'box' as const, + canvasCoherenceMode: 'Gaussian Blur' as const, + canvasCoherenceMinDenoise: 0, + canvasCoherenceEdgeSize: 16, + infillMethod: 'lama' as const, + infillTileSize: 32, + infillPatchmatchDownscaleSize: 1, + infillColorValue: { r: 0, g: 0, b: 0, a: 1 }, + cfgScale: 7.5, + cfgRescaleMultiplier: 0, + guidance: 4, + img2imgStrength: 0.75, + optimizedDenoisingEnabled: true, + iterations: 1, + scheduler: 'dpmpp_3m_k' as const, + upscaleScheduler: 'kdpm_2' as const, + upscaleCfgScale: 2, + seed: 0, + shouldRandomizeSeed: true, + steps: 30, + model: null, + vae: null, + vaePrecision: 'fp32' as const, + fluxVAE: null, + seamlessXAxis: false, + seamlessYAxis: false, + clipSkip: 0, + shouldUseCpuNoise: true, + positivePrompt: '', + negativePrompt: null, + positivePrompt2: '', + negativePrompt2: '', + shouldConcatPrompts: true, + refinerModel: null, + refinerSteps: 20, + refinerCFGScale: 7.5, + refinerScheduler: 'euler' as const, + refinerPositiveAestheticScore: 6, + refinerNegativeAestheticScore: 2.5, + refinerStart: 0.8, + t5EncoderModel: null, + clipEmbedModel: null, + clipLEmbedModel: null, + clipGEmbedModel: null, + controlLora: null, + dimensions: { + rect: { x: 0, y: 0, width: 512, height: 512 }, + aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG), + }, +}); const zInpaintMasks = z.object({ isHidden: z.boolean(), @@ -595,38 +641,45 @@ const zRegionalGuidance = z.object({ isHidden: z.boolean(), entities: z.array(zCanvasRegionalGuidanceState), }); -const zCanvasState = z.object({ - _version: z.literal(3).default(3), - selectedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null), - bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable().default(null), - inpaintMasks: zInpaintMasks.default({ isHidden: false, entities: [] }), - rasterLayers: zRasterLayers.default({ isHidden: false, entities: [] }), - controlLayers: zControlLayers.default({ isHidden: false, entities: [] }), - regionalGuidance: zRegionalGuidance.default({ isHidden: false, entities: [] }), - bbox: zBboxState.default({ - rect: { x: 0, y: 0, width: 512, height: 512 }, - aspectRatio: DEFAULT_ASPECT_RATIO_CONFIG, - scaleMethod: 'auto', - scaledSize: { width: 512, height: 512 }, - modelBase: 'sd-1', - }), +export const zCanvasState = z.object({ + _version: z.literal(3), + selectedEntityIdentifier: zCanvasEntityIdentifer.nullable(), + bookmarkedEntityIdentifier: zCanvasEntityIdentifer.nullable(), + inpaintMasks: zInpaintMasks, + rasterLayers: zRasterLayers, + controlLayers: zControlLayers, + regionalGuidance: zRegionalGuidance, + bbox: zBboxState, }); export type CanvasState = z.infer; +export const getInitialCanvasState = (): CanvasState => ({ + _version: 3 as const, + selectedEntityIdentifier: null, + bookmarkedEntityIdentifier: null, + inpaintMasks: { isHidden: false, entities: [] }, + rasterLayers: { isHidden: false, entities: [] }, + controlLayers: { isHidden: false, entities: [] }, + regionalGuidance: { isHidden: false, entities: [] }, + bbox: { + rect: { x: 0, y: 0, width: 512, height: 512 }, + aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG), + scaleMethod: 'auto' as const, + scaledSize: { width: 512, height: 512 }, + modelBase: 'sd-1' as const, + }, +}); -const zRefImagesState = z.object({ - selectedEntityId: z.string().nullable().default(null), - isPanelOpen: z.boolean().default(false), - entities: z.array(zRefImageState).default(() => []), +export const zRefImagesState = z.object({ + selectedEntityId: z.string().nullable(), + isPanelOpen: z.boolean(), + entities: z.array(zRefImageState), }); export type RefImagesState = z.infer; -const INITIAL_REF_IMAGES_STATE = zRefImagesState.parse({}); -export const getInitialRefImagesState = () => deepClone(INITIAL_REF_IMAGES_STATE); - -/** - * Gets a fresh canvas initial state with no references in memory to existing objects. - */ -const CANVAS_INITIAL_STATE = zCanvasState.parse({}); -export const getInitialCanvasState = () => deepClone(CANVAS_INITIAL_STATE); +export const getInitialRefImagesState = (): RefImagesState => ({ + selectedEntityId: null, + isPanelOpen: false, + entities: [], +}); export const zCanvasReferenceImageState_OLD = zCanvasEntityBase.extend({ type: z.literal('reference_image'), diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts index 23dbb3b082..6490c75a57 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts @@ -9,16 +9,17 @@ const zSeedBehaviour = z.enum(['PER_ITERATION', 'PER_PROMPT']); export const isSeedBehaviour = buildZodTypeGuard(zSeedBehaviour); export type SeedBehaviour = z.infer; -export interface DynamicPromptsState { - _version: 1; - maxPrompts: number; - combinatorial: boolean; - prompts: string[]; - parsingError: string | undefined | null; - isError: boolean; - isLoading: boolean; - seedBehaviour: SeedBehaviour; -} +const zDynamicPromptsState = z.object({ + _version: z.literal(1), + maxPrompts: z.number().int().min(1).max(1000), + combinatorial: z.boolean(), + prompts: z.array(z.string()), + parsingError: z.string().nullish(), + isError: z.boolean(), + isLoading: z.boolean(), + seedBehaviour: zSeedBehaviour, +}); +type DynamicPromptsState = z.infer; const getInitialState = (): DynamicPromptsState => ({ _version: 1, @@ -66,19 +67,17 @@ export const { seedBehaviourChanged, } = slice.actions; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - return state; -}; - export const dynamicPromptsSliceConfig: SliceConfig = { slice, + zSchema: zDynamicPromptsState, getInitialState, persistConfig: { - migrate, + migrate: (state) => { + if (!('_version' in state)) { + state._version = 1; + } + return zDynamicPromptsState.parse(state); + }, persistDenylist: ['prompts', 'parsingError', 'isError', 'isLoading'], }, }; diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 0685e0bd24..e47adcaed4 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -2,190 +2,19 @@ import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; -import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai'; +import { getDefaultAppConfig, type PartialAppConfig, zAppConfig } from 'app/types/invokeai'; import { merge } from 'es-toolkit/compat'; +import z from 'zod'; -const baseDimensionConfig: NumericalParameterConfig = { - initial: 512, // determined by model selection, unused in practice - sliderMin: 64, - sliderMax: 1536, - numberInputMin: 64, - numberInputMax: 4096, - fineStep: 8, - coarseStep: 64, -}; - -type ConfigState = AppConfig & { didLoad: boolean }; +const zConfigState = z.object({ + ...zAppConfig.shape, + didLoad: z.boolean(), +}); +type ConfigState = z.infer; const getInitialState = (): ConfigState => ({ + ...getDefaultAppConfig(), didLoad: false, - isLocal: true, - shouldUpdateImagesOnConnect: false, - shouldFetchMetadataFromApi: false, - allowPrivateBoards: false, - allowPrivateStylePresets: false, - allowClientSideUpload: false, - allowPublishWorkflows: false, - allowPromptExpansion: false, - shouldShowCredits: false, - disabledTabs: [], - disabledFeatures: ['lightbox', 'faceRestore', 'batches'], - disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'], - nodesAllowlist: undefined, - nodesDenylist: undefined, - sd: { - disabledControlNetModels: [], - disabledControlNetProcessors: [], - iterations: { - initial: 1, - sliderMin: 1, - sliderMax: 1000, - numberInputMin: 1, - numberInputMax: 10000, - fineStep: 1, - coarseStep: 1, - }, - width: { ...baseDimensionConfig }, - height: { ...baseDimensionConfig }, - boundingBoxWidth: { ...baseDimensionConfig }, - boundingBoxHeight: { ...baseDimensionConfig }, - scaledBoundingBoxWidth: { ...baseDimensionConfig }, - scaledBoundingBoxHeight: { ...baseDimensionConfig }, - scheduler: 'dpmpp_3m_k', - vaePrecision: 'fp32', - steps: { - initial: 30, - sliderMin: 1, - sliderMax: 100, - numberInputMin: 1, - numberInputMax: 500, - fineStep: 1, - coarseStep: 1, - }, - guidance: { - initial: 7, - sliderMin: 1, - sliderMax: 20, - numberInputMin: 1, - numberInputMax: 200, - fineStep: 0.1, - coarseStep: 0.5, - }, - img2imgStrength: { - initial: 0.7, - sliderMin: 0, - sliderMax: 1, - numberInputMin: 0, - numberInputMax: 1, - fineStep: 0.01, - coarseStep: 0.05, - }, - canvasCoherenceStrength: { - initial: 0.3, - sliderMin: 0, - sliderMax: 1, - numberInputMin: 0, - numberInputMax: 1, - fineStep: 0.01, - coarseStep: 0.05, - }, - hrfStrength: { - initial: 0.45, - sliderMin: 0, - sliderMax: 1, - numberInputMin: 0, - numberInputMax: 1, - fineStep: 0.01, - coarseStep: 0.05, - }, - canvasCoherenceEdgeSize: { - initial: 16, - sliderMin: 0, - sliderMax: 128, - numberInputMin: 0, - numberInputMax: 1024, - fineStep: 8, - coarseStep: 16, - }, - cfgRescaleMultiplier: { - initial: 0, - sliderMin: 0, - sliderMax: 0.99, - numberInputMin: 0, - numberInputMax: 0.99, - fineStep: 0.05, - coarseStep: 0.1, - }, - clipSkip: { - initial: 0, - sliderMin: 0, - sliderMax: 12, // determined by model selection, unused in practice - numberInputMin: 0, - numberInputMax: 12, // determined by model selection, unused in practice - fineStep: 1, - coarseStep: 1, - }, - infillPatchmatchDownscaleSize: { - initial: 1, - sliderMin: 1, - sliderMax: 10, - numberInputMin: 1, - numberInputMax: 10, - fineStep: 1, - coarseStep: 1, - }, - infillTileSize: { - initial: 32, - sliderMin: 16, - sliderMax: 64, - numberInputMin: 16, - numberInputMax: 256, - fineStep: 1, - coarseStep: 1, - }, - maskBlur: { - initial: 16, - sliderMin: 0, - sliderMax: 128, - numberInputMin: 0, - numberInputMax: 512, - fineStep: 1, - coarseStep: 1, - }, - ca: { - weight: { - initial: 1, - sliderMin: 0, - sliderMax: 2, - numberInputMin: -1, - numberInputMax: 2, - fineStep: 0.01, - coarseStep: 0.05, - }, - }, - dynamicPrompts: { - maxPrompts: { - initial: 100, - sliderMin: 1, - sliderMax: 1000, - numberInputMin: 1, - numberInputMax: 10000, - fineStep: 1, - coarseStep: 10, - }, - }, - }, - flux: { - guidance: { - initial: 4, - sliderMin: 2, - sliderMax: 6, - numberInputMin: 1, - numberInputMax: 20, - fineStep: 0.1, - coarseStep: 0.5, - }, - }, }); const slice = createSlice({ @@ -203,6 +32,7 @@ export const { configChanged } = slice.actions; export const configSliceConfig: SliceConfig = { slice, + zSchema: zConfigState, getInitialState, }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 74d9b969ff..35ead003ac 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -2,15 +2,12 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { SliceConfig } from 'app/store/types'; -import { deepClone } from 'common/util/deepClone'; -import { INITIAL_STATE, type UIState } from './uiTypes'; - -const getInitialState = (): UIState => deepClone(INITIAL_STATE); +import { getInitialUIState, type UIState, zUIState } from './uiTypes'; const slice = createSlice({ name: 'ui', - initialState: getInitialState(), + initialState: getInitialUIState(), reducers: { setActiveTab: (state, action: PayloadAction) => { state.activeTab = action.payload; @@ -88,27 +85,25 @@ export const { export const selectUiSlice = (state: RootState) => state.ui; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrate = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - if (state._version === 1) { - state.activeTab = 'generation'; - state._version = 2; - } - if (state._version === 2) { - state.activeTab = 'canvas'; - state._version = 3; - } - return state; -}; - export const uiSliceConfig: SliceConfig = { slice, - getInitialState, + zSchema: zUIState, + getInitialState: getInitialUIState, persistConfig: { - migrate, + migrate: (state) => { + if (!('_version' in state)) { + state._version = 1; + } + if (state._version === 1) { + state.activeTab = 'generation'; + state._version = 2; + } + if (state._version === 2) { + state.activeTab = 'canvas'; + state._version = 3; + } + return zUIState.parse(state); + }, persistDenylist: ['shouldShowImageDetails'], }, }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 044ea4e3ef..e36c0b1cdb 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,7 +1,7 @@ import { isPlainObject } from 'es-toolkit'; import { z } from 'zod'; -const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']); +export const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']); export type TabName = z.infer; const zPartialDimensions = z.object({ @@ -12,17 +12,28 @@ const zPartialDimensions = z.object({ const zSerializable = z.any().refine(isPlainObject); export type Serializable = z.infer; -const zUIState = z.object({ - _version: z.literal(3).default(3), - activeTab: zTabName.default('generate'), - shouldShowImageDetails: z.boolean().default(false), - shouldShowProgressInViewer: z.boolean().default(true), - accordions: z.record(z.string(), z.boolean()).default(() => ({})), - expanders: z.record(z.string(), z.boolean()).default(() => ({})), - textAreaSizes: z.record(z.string(), zPartialDimensions).default({}), - panels: z.record(z.string(), zSerializable).default({}), - shouldShowNotificationV2: z.boolean().default(true), - pickerCompactViewStates: z.record(z.string(), z.boolean()).default(() => ({})), +export const zUIState = z.object({ + _version: z.literal(3), + activeTab: zTabName, + shouldShowImageDetails: z.boolean(), + shouldShowProgressInViewer: z.boolean(), + accordions: z.record(z.string(), z.boolean()), + expanders: z.record(z.string(), z.boolean()), + textAreaSizes: z.record(z.string(), zPartialDimensions), + panels: z.record(z.string(), zSerializable), + shouldShowNotificationV2: z.boolean(), + pickerCompactViewStates: z.record(z.string(), z.boolean()), }); -export const INITIAL_STATE = zUIState.parse({}); export type UIState = z.infer; +export const getInitialUIState = (): UIState => ({ + _version: 3 as const, + activeTab: 'generate' as const, + shouldShowImageDetails: false, + shouldShowProgressInViewer: true, + accordions: {}, + expanders: {}, + textAreaSizes: {}, + panels: {}, + shouldShowNotificationV2: true, + pickerCompactViewStates: {}, +});