From ce7172d78c3e3b836182ab1ed116b60590c82730 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:14:11 +1000 Subject: [PATCH] feat(ui): add workflow saving/loading (wip) Adds loading workflows with exhaustive validation via `zod`. There is a load button but no dedicated save/load UI yet. Also need to add versioning to the workflow format itself. --- invokeai/frontend/web/package.json | 3 +- .../frontend/web/src/app/components/App.tsx | 4 +- .../frontend/web/src/app/logging/logger.ts | 2 +- .../frontend/web/src/app/logging/useLogger.ts | 17 +- .../listeners/socketio/socketConnected.ts | 3 +- .../web/src/common/util/colorTokenToCssVar.ts | 2 + .../ImageMetadataViewer/DataViewer.tsx | 44 +- .../connectionLines/CustomConnectionLine.tsx | 5 +- .../flow/edges/util/makeEdgeSelector.ts | 3 +- .../nodes/Invocation/fields/FieldHandle.tsx | 10 +- .../TopCenterPanel/LoadWorkflowButton.tsx | 28 + .../panels/TopCenterPanel/TopCenterPanel.tsx | 2 + .../sidePanel/workflow/WorkflowJSONTab.tsx | 2 +- .../nodes/hooks/useLoadWorkflowFromFile.tsx | 107 +++ .../nodes/store/nodesPersistDenylist.ts | 1 - .../src/features/nodes/store/nodesSlice.ts | 45 +- .../web/src/features/nodes/store/types.ts | 2 - .../web/src/features/nodes/types/constants.ts | 3 - .../web/src/features/nodes/types/types.ts | 629 +++++++++++------- .../src/features/nodes/util/buildWorkflow.ts | 25 +- .../parameters/types/parameterSchemas.ts | 2 +- .../system/hooks/useIsApplicationReady.ts | 38 -- .../system/store/systemPersistDenylist.ts | 1 - .../src/features/system/store/systemSlice.ts | 13 - .../frontend/web/src/services/api/schema.d.ts | 12 +- invokeai/frontend/web/yarn.lock | 5 + 26 files changed, 611 insertions(+), 397 deletions(-) create mode 100644 invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx delete mode 100644 invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index c1d1254d58..5faaef3236 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -114,7 +114,8 @@ "use-debounce": "^9.0.4", "use-image": "^1.1.1", "uuid": "^9.0.0", - "zod": "^3.22.2" + "zod": "^3.22.2", + "zod-validation-error": "^1.5.0" }, "peerDependencies": { "@chakra-ui/cli": "^2.4.0", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 8c7ce65ece..44f5c8dbd6 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -32,7 +32,7 @@ interface Props { const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { const language = useAppSelector(languageSelector); - const logger = useLogger(); + const logger = useLogger('system'); const dispatch = useAppDispatch(); const handleReset = useCallback(() => { localStorage.clear(); @@ -46,7 +46,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => { useEffect(() => { if (size(config)) { - logger.info({ namespace: 'App', config }, 'Received config'); + logger.info({ config }, 'Received config'); dispatch(configChanged(config)); } }, [dispatch, config, logger]); diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts index 7797b8dc92..2d7b8a7744 100644 --- a/invokeai/frontend/web/src/app/logging/logger.ts +++ b/invokeai/frontend/web/src/app/logging/logger.ts @@ -9,7 +9,7 @@ export const log = Roarr.child(BASE_CONTEXT); export const $logger = atom(Roarr.child(BASE_CONTEXT)); -type LoggerNamespace = +export type LoggerNamespace = | 'images' | 'models' | 'config' diff --git a/invokeai/frontend/web/src/app/logging/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts index 6c60bd4fd0..d31bcc2660 100644 --- a/invokeai/frontend/web/src/app/logging/useLogger.ts +++ b/invokeai/frontend/web/src/app/logging/useLogger.ts @@ -1,12 +1,17 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { createLogWriter } from '@roarr/browser-log-writer'; import { useAppSelector } from 'app/store/storeHooks'; import { systemSelector } from 'features/system/store/systemSelectors'; import { isEqual } from 'lodash-es'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { ROARR, Roarr } from 'roarr'; -import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP } from './logger'; +import { + $logger, + BASE_CONTEXT, + LOG_LEVEL_MAP, + LoggerNamespace, + logger, +} from './logger'; const selector = createSelector( systemSelector, @@ -25,7 +30,7 @@ const selector = createSelector( } ); -export const useLogger = () => { +export const useLogger = (namespace: LoggerNamespace) => { const { consoleLogLevel, shouldLogToConsole } = useAppSelector(selector); // The provided Roarr browser log writer uses localStorage to config logging to console @@ -57,7 +62,7 @@ export const useLogger = () => { $logger.set(Roarr.child(newContext)); }, []); - const logger = useStore($logger); + const log = useMemo(() => logger(namespace), [namespace]); - return logger; + return log; }; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts index 32a6cce203..739bbd7110 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts @@ -5,6 +5,7 @@ import { modelsApi } from 'services/api/endpoints/models'; import { receivedOpenAPISchema } from 'services/api/thunks/schema'; import { appSocketConnected, socketConnected } from 'services/events/actions'; import { startAppListening } from '../..'; +import { size } from 'lodash-es'; export const addSocketConnectedEventListener = () => { startAppListening({ @@ -18,7 +19,7 @@ export const addSocketConnectedEventListener = () => { const { disabledTabs } = config; - if (!nodes.schema && !disabledTabs.includes('nodes')) { + if (!size(nodes.nodeTemplates) && !disabledTabs.includes('nodes')) { dispatch(receivedOpenAPISchema()); } diff --git a/invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts b/invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts new file mode 100644 index 0000000000..e29005186f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/colorTokenToCssVar.ts @@ -0,0 +1,2 @@ +export const colorTokenToCssVar = (colorToken: string) => + `var(--invokeai-colors-${colorToken.split('.').join('-')}`; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx index a98124bb89..87c0957354 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx @@ -8,10 +8,12 @@ type Props = { label: string; data: object | string; fileName?: string; + withDownload?: boolean; + withCopy?: boolean; }; const DataViewer = (props: Props) => { - const { label, data, fileName } = props; + const { label, data, fileName, withDownload = true, withCopy = true } = props; const dataString = useMemo( () => (isString(data) ? data : JSON.stringify(data, null, 2)), [data] @@ -70,24 +72,28 @@ const DataViewer = (props: Props) => { - - } - variant="ghost" - opacity={0.7} - onClick={handleSave} - /> - - - } - variant="ghost" - opacity={0.7} - onClick={handleCopy} - /> - + {withDownload && ( + + } + variant="ghost" + opacity={0.7} + onClick={handleSave} + /> + + )} + {withCopy && ( + + } + variant="ghost" + opacity={0.7} + onClick={handleCopy} + /> + + )} ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx index ad8ba3dc62..a379be7ee2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx @@ -1,9 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; -import { ConnectionLineComponentProps, getBezierPath } from 'reactflow'; -import { FIELDS, colorTokenToCssVar } from '../../../types/constants'; +import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; +import { FIELDS } from 'features/nodes/types/constants'; import { memo } from 'react'; +import { ConnectionLineComponentProps, getBezierPath } from 'reactflow'; const selector = createSelector(stateSelector, ({ nodes }) => { const { shouldAnimateEdges, currentConnectionFieldType, shouldColorEdges } = diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index ed692042c6..b5dc484eae 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,7 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { FIELDS, colorTokenToCssVar } from 'features/nodes/types/constants'; +import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; +import { FIELDS } from 'features/nodes/types/constants'; import { isInvocationNode } from 'features/nodes/types/types'; export const makeEdgeSelector = ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx index 16e06471f3..14924a16fe 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldHandle.tsx @@ -1,15 +1,15 @@ import { Tooltip } from '@chakra-ui/react'; -import { CSSProperties, memo, useMemo } from 'react'; -import { Handle, HandleType, Position } from 'reactflow'; +import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { FIELDS, HANDLE_TOOLTIP_OPEN_DELAY, - colorTokenToCssVar, -} from '../../../../../types/constants'; +} from 'features/nodes/types/constants'; import { InputFieldTemplate, OutputFieldTemplate, -} from '../../../../../types/types'; +} from 'features/nodes/types/types'; +import { CSSProperties, memo, useMemo } from 'react'; +import { Handle, HandleType, Position } from 'reactflow'; export const handleBaseStyles: CSSProperties = { position: 'absolute', diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx new file mode 100644 index 0000000000..6a413cbf04 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/LoadWorkflowButton.tsx @@ -0,0 +1,28 @@ +import { FileButton } from '@mantine/core'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile'; +import { memo, useRef } from 'react'; +import { FaUpload } from 'react-icons/fa'; + +const LoadWorkflowButton = () => { + const resetRef = useRef<() => void>(null); + const loadWorkflowFromFile = useLoadWorkflowFromFile(); + return ( + + {(props) => ( + } + tooltip="Load Workflow" + aria-label="Load Workflow" + {...props} + /> + )} + + ); +}; + +export default memo(LoadWorkflowButton); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx index 675a69325a..61a396498f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopCenterPanel/TopCenterPanel.tsx @@ -6,6 +6,7 @@ import NodeEditorSettings from './NodeEditorSettings'; import ClearGraphButton from './ClearGraphButton'; import NodeInvokeButton from './NodeInvokeButton'; import ReloadSchemaButton from './ReloadSchemaButton'; +import LoadWorkflowButton from './LoadWorkflowButton'; const TopCenterPanel = () => { return ( @@ -16,6 +17,7 @@ const TopCenterPanel = () => { + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx index 21c75a42da..ed5386938f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowJSONTab.tsx @@ -31,7 +31,7 @@ const WorkflowJSONTab = () => { h: 'full', }} > - + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx new file mode 100644 index 0000000000..7c29e77e12 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx @@ -0,0 +1,107 @@ +import { ListItem, Text, UnorderedList } from '@chakra-ui/react'; +import { useLogger } from 'app/logging/useLogger'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { parseify } from 'common/util/serialize'; +import { workflowLoaded } from 'features/nodes/store/nodesSlice'; +import { zWorkflow } from 'features/nodes/types/types'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { memo, useCallback } from 'react'; +import { flushSync } from 'react-dom'; +import { useReactFlow } from 'reactflow'; +import { ZodError } from 'zod'; +import { fromZodError, fromZodIssue } from 'zod-validation-error'; + +export const useLoadWorkflowFromFile = () => { + const dispatch = useAppDispatch(); + const { fitView } = useReactFlow(); + const logger = useLogger('nodes'); + const loadWorkflowFromFile = useCallback( + (file: File | null) => { + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = async () => { + const rawJSON = reader.result; + + try { + const parsedJSON = JSON.parse(String(rawJSON)); + const result = zWorkflow.safeParse(parsedJSON); + + if (!result.success) { + const message = fromZodError(result.error, { + prefix: 'Workflow Validation Error', + }).toString(); + logger.error({ error: parseify(result.error) }, message); + + dispatch( + addToast( + makeToast({ + title: 'Unable to Validate Workflow', + description: ( + + ), + status: 'error', + duration: 5000, + }) + ) + ); + return; + } + + dispatch(workflowLoaded(result.data)); + flushSync(fitView); + + dispatch( + addToast( + makeToast({ + title: 'Workflow Loaded', + status: 'success', + }) + ) + ); + reader.abort(); + } catch (error) { + // file reader error + if (error) { + dispatch( + addToast( + makeToast({ + title: 'Unable to Load Workflow', + status: 'error', + }) + ) + ); + } + } + }; + + reader.readAsText(file); + }, + [dispatch, fitView, logger] + ); + + return loadWorkflowFromFile; +}; + +const WorkflowValidationErrorContent = memo((props: { error: ZodError }) => { + if (props.error.issues[0]) { + return ( + + {fromZodIssue(props.error.issues[0], { prefix: null }).toString()} + + ); + } + return ( + + {props.error.issues.map((issue, i) => ( + + {fromZodIssue(issue, { prefix: null }).toString()} + + ))} + + ); +}); + +WorkflowValidationErrorContent.displayName = 'WorkflowValidationErrorContent'; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts index fe52b63bb2..acf9918a89 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts @@ -4,7 +4,6 @@ import { NodesState } from './types'; * Nodes slice persist denylist */ export const nodesPersistDenylist: (keyof NodesState)[] = [ - 'schema', 'nodeTemplates', 'connectionStartParams', 'currentConnectionFieldType', diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 816a326ae0..de9b7cd06a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -56,6 +56,8 @@ import { import { NodesState } from './types'; import { findUnoccupiedPosition } from './util/findUnoccupiedPosition'; +export const WORKFLOW_FORMAT_VERSION = '1.0.0'; + const initialNodeExecutionState: Omit = { status: NodeStatus.PENDING, error: null, @@ -64,10 +66,23 @@ const initialNodeExecutionState: Omit = { outputs: [], }; +export const initialWorkflow = { + meta: { + version: WORKFLOW_FORMAT_VERSION, + }, + name: '', + author: '', + description: '', + notes: '', + tags: '', + contact: '', + version: '', + exposedFields: [], +}; + export const initialNodesState: NodesState = { nodes: [], edges: [], - schema: null, nodeTemplates: {}, isReady: false, connectionStartParams: null, @@ -82,16 +97,7 @@ export const initialNodesState: NodesState = { nodeOpacity: 1, selectedNodes: [], selectedEdges: [], - workflow: { - name: '', - author: '', - description: '', - notes: '', - tags: '', - contact: '', - version: '', - exposedFields: [], - }, + workflow: initialWorkflow, nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, mouseOverField: null, @@ -570,15 +576,6 @@ const nodesSlice = createSlice({ nodeOpacityChanged: (state, action: PayloadAction) => { state.nodeOpacity = action.payload; }, - loadFileNodes: ( - state, - action: PayloadAction[]> - ) => { - state.nodes = action.payload; - }, - loadFileEdges: (state, action: PayloadAction) => { - state.edges = action.payload; - }, workflowNameChanged: (state, action: PayloadAction) => { state.workflow.name = action.payload; }, @@ -616,6 +613,9 @@ const nodesSlice = createSlice({ [] ); }, + workflowReset: (state) => { + state.workflow = cloneDeep(initialWorkflow); + }, viewportChanged: (state, action: PayloadAction) => { state.viewport = action.payload; }, @@ -726,9 +726,6 @@ const nodesSlice = createSlice({ builder.addCase(receivedOpenAPISchema.pending, (state) => { state.isReady = false; }); - builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { - state.schema = action.payload; - }); builder.addCase(appSocketInvocationStarted, (state, action) => { const { source_node_id } = action.payload.data; const node = state.nodeExecutionStates[source_node_id]; @@ -792,8 +789,6 @@ export const { nodeTemplatesBuilt, nodeEditorReset, imageCollectionFieldValueChanged, - loadFileNodes, - loadFileEdges, fieldStringValueChanged, fieldNumberValueChanged, fieldBooleanValueChanged, diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index bcc878d69e..54ad235a31 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -1,4 +1,3 @@ -import { OpenAPIV3 } from 'openapi-types'; import { Edge, Node, OnConnectStartParams, Viewport } from 'reactflow'; import { FieldIdentifier, @@ -13,7 +12,6 @@ import { export type NodesState = { nodes: Node[]; edges: Edge[]; - schema: OpenAPIV3.Document | null; nodeTemplates: Record; connectionStartParams: OnConnectStartParams | null; currentConnectionFieldType: FieldType | null; diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 4fbb32fe7f..4e61e46d8a 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -23,9 +23,6 @@ export const COLLECTION_TYPES: FieldType[] = [ 'ImageCollection', ]; -export const colorTokenToCssVar = (colorToken: string) => - `var(--invokeai-colors-${colorToken.split('.').join('-')}`; - export const FIELDS: Record = { integer: { title: 'Integer', diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 35be598176..33ab1991b8 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -1,23 +1,13 @@ import { - ControlNetModelParam, - LoRAModelParam, - MainModelParam, - OnnxModelParam, SchedulerParam, - VaeModelParam, + zBaseModel, + zMainOrOnnxModel, + zScheduler, } from 'features/parameters/types/parameterSchemas'; import { OpenAPIV3 } from 'openapi-types'; import { RgbaColor } from 'react-colorful'; -import { Edge, Node } from 'reactflow'; -import { components } from 'services/api/schema'; -import { - Graph, - GraphExecutionState, - ImageDTO, - ImageField, - _InputField, - _OutputField, -} from 'services/api/types'; +import { Node } from 'reactflow'; +import { Graph, ImageDTO, _InputField, _OutputField } from 'services/api/types'; import { AnyInvocationType, AnyResult, @@ -118,40 +108,6 @@ export type FieldType = z.infer; export const isFieldType = (value: unknown): value is FieldType => zFieldType.safeParse(value).success; -/** - * An input field is persisted across reloads as part of the user's local state. - * - * An input field has three properties: - * - `id` a unique identifier - * - `name` the name of the field, which comes from the python dataclass - * - `value` the field's value - */ -export type InputFieldValue = - | IntegerInputFieldValue - | SeedInputFieldValue - | FloatInputFieldValue - | StringInputFieldValue - | BooleanInputFieldValue - | ImageInputFieldValue - | LatentsInputFieldValue - | ConditioningInputFieldValue - | UNetInputFieldValue - | ClipInputFieldValue - | VaeInputFieldValue - | ControlInputFieldValue - | EnumInputFieldValue - | MainModelInputFieldValue - | SDXLMainModelInputFieldValue - | SDXLRefinerModelInputFieldValue - | VaeModelInputFieldValue - | LoRAModelInputFieldValue - | ControlNetModelInputFieldValue - | CollectionInputFieldValue - | CollectionItemInputFieldValue - | ColorInputFieldValue - | ImageCollectionInputFieldValue - | SchedulerInputFieldValue; - /** * An input field template is generated on each page load from the OpenAPI schema. * @@ -183,6 +139,19 @@ export type InputFieldTemplate = | ImageCollectionInputFieldTemplate | SchedulerInputFieldTemplate; +/** + * Indicates the kind of input(s) this field may have. + */ +export const zInputKind = z.enum(['connection', 'direct', 'any']); +export type InputKind = z.infer; + +export const zFieldValueBase = z.object({ + id: z.string().trim().min(1), + name: z.string().trim().min(1), + type: zFieldType, +}); +export type FieldValueBase = z.infer; + /** * An output field is persisted across as part of the user's local state. * @@ -190,7 +159,11 @@ export type InputFieldTemplate = * - `id` a unique identifier * - `name` the name of the field, which comes from the python dataclass */ -export type OutputFieldValue = FieldValueBase & { fieldKind: 'output' }; + +export const zOutputFieldValue = zFieldValueBase.extend({ + fieldKind: z.literal('output'), +}); +export type OutputFieldValue = z.infer; /** * An output field template is generated on each page load from the OpenAPI schema. @@ -205,141 +178,307 @@ export type OutputFieldTemplate = { description: string; } & _OutputField; -/** - * Indicates the kind of input(s) this field may have. - */ -export type InputKind = 'connection' | 'direct' | 'any'; +export const zInputFieldValueBase = zFieldValueBase.extend({ + fieldKind: z.literal('input'), + label: z.string(), +}); +export type InputFieldValueBase = z.infer; -export type FieldValueBase = { - id: string; - name: string; - type: FieldType; -}; +export const zModelIdentifier = z.object({ + model_name: z.string().trim().min(1), + base_model: zBaseModel, +}); -export type InputFieldValueBase = FieldValueBase & { - fieldKind: 'input'; - label: string; -}; +export const zImageField = z.object({ + image_name: z.string().trim().min(1), +}); +export type ImageField = z.infer; -export type IntegerInputFieldValue = InputFieldValueBase & { - type: 'integer'; - value?: number; -}; +export const zLatentsField = z.object({ + latents_name: z.string().trim().min(1), + seed: z.number().int().optional(), +}); +export type LatentsField = z.infer; -export type FloatInputFieldValue = InputFieldValueBase & { - type: 'float'; - value?: number; -}; +export const zConditioningField = z.object({ + conditioning_name: z.string().trim().min(1), +}); +export type ConditioningField = z.infer; -export type SeedInputFieldValue = InputFieldValueBase & { - type: 'Seed'; - value?: number; -}; +export const zIntegerInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('integer'), + value: z.number().optional(), +}); +export type IntegerInputFieldValue = z.infer; -export type StringInputFieldValue = InputFieldValueBase & { - type: 'string'; - value?: string; -}; +export const zFloatInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('float'), + value: z.number().optional(), +}); +export type FloatInputFieldValue = z.infer; -export type BooleanInputFieldValue = InputFieldValueBase & { - type: 'boolean'; - value?: boolean; -}; +export const zStringInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('string'), + value: z.string().optional(), +}); +export type StringInputFieldValue = z.infer; -export type EnumInputFieldValue = InputFieldValueBase & { - type: 'enum'; - value?: number | string; -}; +export const zBooleanInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('boolean'), + value: z.boolean().optional(), +}); +export type BooleanInputFieldValue = z.infer; -export type LatentsInputFieldValue = InputFieldValueBase & { - type: 'LatentsField'; - value?: undefined; -}; +export const zEnumInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('enum'), + value: z.union([z.string(), z.number()]).optional(), +}); +export type EnumInputFieldValue = z.infer; -export type ConditioningInputFieldValue = InputFieldValueBase & { - type: 'ConditioningField'; - value?: string; -}; +export const zLatentsInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('LatentsField'), + value: zLatentsField.optional(), +}); +export type LatentsInputFieldValue = z.infer; -export type ControlInputFieldValue = InputFieldValueBase & { - type: 'ControlField'; - value?: undefined; -}; +export const zConditioningInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('ConditioningField'), + value: zConditioningField.optional(), +}); +export type ConditioningInputFieldValue = z.infer< + typeof zConditioningInputFieldValue +>; -export type UNetInputFieldValue = InputFieldValueBase & { - type: 'UNetField'; - value?: undefined; -}; +export const zControlNetModel = zModelIdentifier; +export type ControlNetModel = z.infer; -export type ClipInputFieldValue = InputFieldValueBase & { - type: 'ClipField'; - value?: undefined; -}; +export const zControlField = zInputFieldValueBase.extend({ + image: zImageField, + control_model: zControlNetModel, + control_weight: z.union([z.number(), z.array(z.number())]).optional(), + begin_step_percent: z.number().optional(), + end_step_percent: z.number().optional(), + control_mode: z + .enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']) + .optional(), + resize_mode: z + .enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple']) + .optional(), +}); +export type ControlField = z.infer; -export type VaeInputFieldValue = InputFieldValueBase & { - type: 'VaeField'; - value?: undefined; -}; +export const zControlInputFieldTemplate = zInputFieldValueBase.extend({ + type: z.literal('ControlField'), + value: zControlField.optional(), +}); +export type ControlInputFieldValue = z.infer; -export type ImageInputFieldValue = InputFieldValueBase & { - type: 'ImageField'; - value?: ImageField; -}; +export const zModelType = z.enum([ + 'onnx', + 'main', + 'vae', + 'lora', + 'controlnet', + 'embedding', +]); +export type ModelType = z.infer; -export type ImageCollectionInputFieldValue = InputFieldValueBase & { - type: 'ImageCollection'; - value?: ImageField[]; -}; +export const zSubModelType = z.enum([ + 'unet', + 'text_encoder', + 'text_encoder_2', + 'tokenizer', + 'tokenizer_2', + 'vae', + 'vae_decoder', + 'vae_encoder', + 'scheduler', + 'safety_checker', +]); +export type SubModelType = z.infer; -export type MainModelInputFieldValue = InputFieldValueBase & { - type: 'MainModelField'; - value?: MainModelParam | OnnxModelParam; -}; +export const zModelInfo = zModelIdentifier.extend({ + model_type: zModelType, + submodel: zSubModelType.optional(), +}); +export type ModelInfo = z.infer; -export type SDXLMainModelInputFieldValue = InputFieldValueBase & { - type: 'SDXLMainModelField'; - value?: MainModelParam | OnnxModelParam; -}; +export const zLoraInfo = zModelInfo.extend({ + weight: z.number().optional(), +}); +export type LoraInfo = z.infer; -export type SDXLRefinerModelInputFieldValue = InputFieldValueBase & { - type: 'SDXLRefinerModelField'; - value?: MainModelParam | OnnxModelParam; -}; +export const zUNetField = z.object({ + unet: zModelInfo, + scheduler: zModelInfo, + loras: z.array(zLoraInfo), +}); +export type UNetField = z.infer; -export type VaeModelInputFieldValue = InputFieldValueBase & { - type: 'VaeModelField'; - value?: VaeModelParam; -}; +export const zUNetInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('UNetField'), + value: zUNetField.optional(), +}); +export type UNetInputFieldValue = z.infer; -export type LoRAModelInputFieldValue = InputFieldValueBase & { - type: 'LoRAModelField'; - value?: LoRAModelParam; -}; +export const zClipField = z.object({ + tokenizer: zModelInfo, + text_encoder: zModelInfo, + skipped_layers: z.number(), + loras: z.array(zLoraInfo), +}); +export type ClipField = z.infer; -export type ControlNetModelInputFieldValue = InputFieldValueBase & { - type: 'ControlNetModelField'; - value?: ControlNetModelParam; -}; +export const zClipInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('ClipField'), + value: zClipField.optional(), +}); +export type ClipInputFieldValue = z.infer; -export type CollectionInputFieldValue = InputFieldValueBase & { - type: 'Collection'; - value?: (string | number)[]; -}; +export const zVaeField = z.object({ + vae: zModelInfo, +}); +export type VaeField = z.infer; -export type CollectionItemInputFieldValue = InputFieldValueBase & { - type: 'CollectionItem'; - value?: undefined; -}; +export const zVaeInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('VaeField'), + value: zVaeField.optional(), +}); +export type VaeInputFieldValue = z.infer; -export type ColorInputFieldValue = InputFieldValueBase & { - type: 'ColorField'; - value?: RgbaColor; -}; +export const zImageInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('ImageField'), + value: zImageField.optional(), +}); +export type ImageInputFieldValue = z.infer; -export type SchedulerInputFieldValue = InputFieldValueBase & { - type: 'Scheduler'; - value?: SchedulerParam; -}; +export const zImageCollectionInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('ImageCollection'), + value: z.array(zImageField).optional(), +}); +export type ImageCollectionInputFieldValue = z.infer< + typeof zImageCollectionInputFieldValue +>; + +export const zMainModelInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('MainModelField'), + value: zMainOrOnnxModel.optional(), +}); +export type MainModelInputFieldValue = z.infer< + typeof zMainModelInputFieldValue +>; + +export const zSDXLMainModelInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('SDXLMainModelField'), + value: zMainOrOnnxModel.optional(), +}); +export type SDXLMainModelInputFieldValue = z.infer< + typeof zSDXLMainModelInputFieldValue +>; + +export const zSDXLRefinerModelInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('SDXLRefinerModelField'), + value: zMainOrOnnxModel.optional(), // TODO: should narrow this down to a refiner model +}); +export type SDXLRefinerModelInputFieldValue = z.infer< + typeof zSDXLRefinerModelInputFieldValue +>; + +export const zVaeModelField = zModelIdentifier; + +export const zVaeModelInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('VaeModelField'), + value: zVaeModelField.optional(), +}); +export type VaeModelInputFieldValue = z.infer; + +export const zLoRAModelField = zModelIdentifier; +export type LoRAModelField = z.infer; + +export const zLoRAModelInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('LoRAModelField'), + value: zLoRAModelField.optional(), +}); +export type LoRAModelInputFieldValue = z.infer< + typeof zLoRAModelInputFieldValue +>; + +export const zControlNetModelField = zModelIdentifier; +export type ControlNetModelField = z.infer; + +export const zControlNetModelInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('ControlNetModelField'), + value: zControlNetModelField.optional(), +}); +export type ControlNetModelInputFieldValue = z.infer< + typeof zControlNetModelInputFieldValue +>; + +export const zCollectionInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('Collection'), + value: z.array(z.any()).optional(), // TODO: should this field ever have a value? +}); +export type CollectionInputFieldValue = z.infer< + typeof zCollectionInputFieldValue +>; + +export const zCollectionItemInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('CollectionItem'), + value: z.any().optional(), // TODO: should this field ever have a value? +}); +export type CollectionItemInputFieldValue = z.infer< + typeof zCollectionItemInputFieldValue +>; + +export const zColorField = z.object({ + r: z.number().int().min(0).max(255), + g: z.number().int().min(0).max(255), + b: z.number().int().min(0).max(255), + a: z.number().int().min(0).max(255), +}); +export type ColorField = z.infer; + +export const zColorInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('ColorField'), + value: zColorField.optional(), +}); +export type ColorInputFieldValue = z.infer; + +export const zSchedulerInputFieldValue = zInputFieldValueBase.extend({ + type: z.literal('Scheduler'), + value: zScheduler.optional(), +}); +export type SchedulerInputFieldValue = z.infer< + typeof zSchedulerInputFieldValue +>; + +export const zInputFieldValue = z.discriminatedUnion('type', [ + zIntegerInputFieldValue, + zFloatInputFieldValue, + zStringInputFieldValue, + zBooleanInputFieldValue, + zImageInputFieldValue, + zLatentsInputFieldValue, + zConditioningInputFieldValue, + zUNetInputFieldValue, + zClipInputFieldValue, + zVaeInputFieldValue, + zControlInputFieldTemplate, + zEnumInputFieldValue, + zMainModelInputFieldValue, + zSDXLMainModelInputFieldValue, + zSDXLRefinerModelInputFieldValue, + zVaeModelInputFieldValue, + zLoRAModelInputFieldValue, + zControlNetModelInputFieldValue, + zCollectionInputFieldValue, + zCollectionItemInputFieldValue, + zColorInputFieldValue, + zImageCollectionInputFieldValue, + zSchedulerInputFieldValue, +]); + +export type InputFieldValue = z.infer; export type InputFieldTemplateBase = { name: string; @@ -576,24 +715,26 @@ export const isInvocationFieldSchema = ( export type InvocationEdgeExtra = { type: 'default' | 'collapsed' }; -export const zInputFieldValue = z.object({ - id: z.string().trim().min(1), - name: z.string().trim().min(1), - type: zFieldType, - label: z.string(), - isExposed: z.boolean(), -}); - export const zInvocationNodeData = z.object({ id: z.string().trim().min(1), + // no easy way to build this dynamically, and we don't want to anyways, because this will be used + // to validate incoming workflows, and we want to allow community nodes. type: z.string().trim().min(1), - inputs: z.record(z.any()), - outputs: z.record(z.any()), + inputs: z.record(zInputFieldValue), + outputs: z.record(zOutputFieldValue), label: z.string(), isOpen: z.boolean(), notes: z.string(), }); +// Massage this to get better type safety while developing +export type InvocationNodeData = Omit< + z.infer, + 'type' +> & { + type: AnyInvocationType; +}; + export const zNotesNodeData = z.object({ id: z.string().trim().min(1), type: z.literal('notes'), @@ -602,6 +743,69 @@ export const zNotesNodeData = z.object({ notes: z.string(), }); +export type NotesNodeData = z.infer; + +export const zWorkflowInvocationNode = z.object({ + id: z.string().trim().min(1), + type: z.literal('invocation'), + data: zInvocationNodeData, + width: z.number().gt(0), + height: z.number().gt(0), + position: z.object({ + x: z.number(), + y: z.number(), + }), +}); + +export const zWorkflowNotesNode = z.object({ + id: z.string().trim().min(1), + type: z.literal('notes'), + data: zNotesNodeData, + width: z.number().gt(0), + height: z.number().gt(0), + position: z.object({ + x: z.number(), + y: z.number(), + }), +}); + +export const zWorkflowNode = z.discriminatedUnion('type', [ + zWorkflowInvocationNode, + zWorkflowNotesNode, +]); + +export type WorkflowNode = z.infer; + +export const zWorkflowEdge = z.object({ + source: z.string().trim().min(1), + sourceHandle: z.string().trim().min(1), + target: z.string().trim().min(1), + targetHandle: z.string().trim().min(1), + id: z.string().trim().min(1), + type: z.enum(['default', 'collapsed']), +}); + +export const zFieldIdentifier = z.object({ + nodeId: z.string().trim().min(1), + fieldName: z.string().trim().min(1), +}); + +export type FieldIdentifier = z.infer; + +export const zSemVer = z.string().refine((val) => { + const [major, minor, patch] = val.split('.'); + return ( + major !== undefined && + minor !== undefined && + patch !== undefined && + Number.isInteger(Number(major)) && + Number.isInteger(Number(minor)) && + Number.isInteger(Number(patch)) + ); +}); + +export type SemVer = z.infer; + export const zWorkflow = z.object({ name: z.string().trim().min(1), author: z.string(), @@ -610,67 +814,12 @@ export const zWorkflow = z.object({ contact: z.string(), tags: z.string(), notes: z.string(), - nodes: z.array( - z.object({ - id: z.string().trim().min(1), - type: z.string().trim().min(1), - data: z.union([zInvocationNodeData, zNotesNodeData]), - width: z.number().gt(0), - height: z.number().gt(0), - position: z.object({ - x: z.number(), - y: z.number(), - }), - }) - ), - edges: z.array( - z.object({ - source: z.string().trim().min(1), - sourceHandle: z.string().trim().min(1), - target: z.string().trim().min(1), - targetHandle: z.string().trim().min(1), - id: z.string().trim().min(1), - type: z.string().trim().min(1), - }) - ), + nodes: z.array(zWorkflowNode), + edges: z.array(zWorkflowEdge), + exposedFields: z.array(zFieldIdentifier), }); -export type Workflow = { - name: string; - author: string; - description: string; - version: string; - contact: string; - tags: string; - notes: string; - nodes: Pick< - Node, - 'id' | 'type' | 'data' | 'width' | 'height' | 'position' - >[]; - edges: Pick< - Edge, - 'source' | 'sourceHandle' | 'target' | 'targetHandle' | 'id' | 'type' - >[]; - exposedFields: FieldIdentifier[]; -}; - -export type InvocationNodeData = { - id: string; - type: AnyInvocationType; - inputs: Record; - outputs: Record; - label: string; - isOpen: boolean; - notes: string; -}; - -export type NotesNodeData = { - id: string; - type: 'notes'; - label: string; - notes: string; - isOpen: boolean; -}; +export type Workflow = z.infer; export type CurrentImageNodeData = { id: string; @@ -710,25 +859,6 @@ export enum NodeStatus { FAILED, } -type SavedOutput = - | components['schemas']['StringOutput'] - | components['schemas']['IntegerOutput'] - | components['schemas']['FloatOutput'] - | components['schemas']['ImageOutput']; - -export const isSavedOutput = ( - output: GraphExecutionState['results'][string] -): output is SavedOutput => - Boolean( - output && - [ - 'string_output', - 'integer_output', - 'float_output', - 'image_output', - ].includes(output?.type) - ); - export type NodeExecutionState = { nodeId: string; status: NodeStatus; @@ -738,11 +868,6 @@ export type NodeExecutionState = { outputs: AnyResult[]; }; -export type FieldIdentifier = { - nodeId: string; - fieldName: string; -}; - export type FieldComponentProps< V extends InputFieldValue, T extends InputFieldTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts index da3aff7e1c..cb0d0b942b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/buildWorkflow.ts @@ -1,8 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; -import { pick } from 'lodash-es'; import { NodesState } from '../store/types'; -import { Workflow, isInvocationNode, isNotesNode } from '../types/types'; +import { Workflow, zWorkflowEdge, zWorkflowNode } from '../types/types'; export const buildWorkflow = (nodesState: NodesState): Workflow => { const { workflow: workflowMeta, nodes, edges } = nodesState; @@ -13,25 +12,19 @@ export const buildWorkflow = (nodesState: NodesState): Workflow => { }; nodes.forEach((node) => { - if (!isInvocationNode(node) && !isNotesNode(node)) { + const result = zWorkflowNode.safeParse(node); + if (!result.success) { return; } - workflow.nodes.push( - pick(node, ['id', 'type', 'position', 'width', 'height', 'data']) - ); + workflow.nodes.push(result.data); }); edges.forEach((edge) => { - workflow.edges.push( - pick(edge, [ - 'source', - 'sourceHandle', - 'target', - 'targetHandle', - 'id', - 'type', - ]) - ); + const result = zWorkflowEdge.safeParse(edge); + if (!result.success) { + return; + } + workflow.edges.push(result.data); }); return workflow; diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index 25905e1e14..e210efc414 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -210,7 +210,7 @@ export type HeightParam = z.infer; export const isValidHeight = (val: unknown): val is HeightParam => zHeight.safeParse(val).success; -const zBaseModel = z.enum(['sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); +export const zBaseModel = z.enum(['sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); export type BaseModelParam = z.infer; diff --git a/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts b/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts deleted file mode 100644 index 8ba5731a5b..0000000000 --- a/invokeai/frontend/web/src/features/system/hooks/useIsApplicationReady.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useMemo } from 'react'; -import { configSelector } from '../store/configSelectors'; -import { systemSelector } from '../store/systemSelectors'; - -const isApplicationReadySelector = createSelector( - [systemSelector, configSelector], - (system, config) => { - const { wasSchemaParsed } = system; - - const { disabledTabs } = config; - - return { - disabledTabs, - wasSchemaParsed, - }; - } -); - -/** - * Checks if the application is ready to be used, i.e. if the initial startup process is finished. - */ -export const useIsApplicationReady = () => { - const { disabledTabs, wasSchemaParsed } = useAppSelector( - isApplicationReadySelector - ); - - const isApplicationReady = useMemo(() => { - if (!disabledTabs.includes('nodes') && !wasSchemaParsed) { - return false; - } - - return true; - }, [disabledTabs, wasSchemaParsed]); - - return isApplicationReady; -}; diff --git a/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts index bba279c4bc..b5376afc4f 100644 --- a/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts @@ -16,7 +16,6 @@ export const systemPersistDenylist: (keyof SystemState)[] = [ 'isCancelScheduled', 'progressImage', 'wereModelsReceived', - 'wasSchemaParsed', 'isPersisted', 'isUploading', ]; diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index d789df49a4..88f7f74476 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -2,7 +2,6 @@ import { UseToastOptions } from '@chakra-ui/react'; import { PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { InvokeLogLevel } from 'app/logging/logger'; import { userInvoked } from 'app/store/actions'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; import { t } from 'i18next'; import { startCase, upperFirst } from 'lodash-es'; import { LogLevelName } from 'roarr'; @@ -68,10 +67,6 @@ export interface SystemState { * Whether or not the available models were received */ wereModelsReceived: boolean; - /** - * Whether or not the OpenAPI schema was received and parsed - */ - wasSchemaParsed: boolean; /** * The console output logging level */ @@ -112,7 +107,6 @@ export const initialSystemState: SystemState = { isCancelScheduled: false, subscribedNodeIds: [], wereModelsReceived: false, - wasSchemaParsed: false, consoleLogLevel: 'debug', shouldLogToConsole: true, statusTranslationKey: 'common.statusDisconnected', @@ -339,13 +333,6 @@ export const systemSlice = createSlice({ ); }); - /** - * OpenAPI schema was parsed - */ - builder.addCase(nodeTemplatesBuilt, (state) => { - state.wasSchemaParsed = true; - }); - // *** Matchers - must be after all cases *** /** diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index 1e03f92190..d6790f079f 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -6238,12 +6238,6 @@ export type components = { * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. @@ -6256,6 +6250,12 @@ export type components = { * @enum {string} */ StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 42336f4f7a..b1588c615f 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -7063,6 +7063,11 @@ z-schema@~5.0.2: optionalDependencies: commander "^10.0.0" +zod-validation-error@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-1.5.0.tgz#2b355007a1c3b7fb04fa476bfad4e7b3fd5491e3" + integrity sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw== + zod@^3.22.2: version "3.22.2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b"