diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index cc1e17cf51..9a45dd89a5 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -75,6 +75,7 @@ "@reduxjs/toolkit": "^1.9.5", "@roarr/browser-log-writer": "^1.1.5", "@stevebel/png": "^1.5.1", + "compare-versions": "^6.1.0", "dateformat": "^5.0.3", "formik": "^2.4.3", "framer-motion": "^10.16.1", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 4afe023fbb..261edba0af 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -84,6 +84,7 @@ import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas'; import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage'; import { addUserInvokedNodesListener } from './listeners/userInvokedNodes'; import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage'; +import { addWorkflowLoadedListener } from './listeners/workflowLoaded'; export const listenerMiddleware = createListenerMiddleware(); @@ -202,6 +203,9 @@ addBoardIdSelectedListener(); // Node schemas addReceivedOpenAPISchemaListener(); +// Workflows +addWorkflowLoadedListener(); + // DND addImageDroppedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts new file mode 100644 index 0000000000..c447720941 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoaded.ts @@ -0,0 +1,55 @@ +import { logger } from 'app/logging/logger'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; +import { workflowLoaded } from 'features/nodes/store/nodesSlice'; +import { $flow } from 'features/nodes/store/reactFlowInstance'; +import { validateWorkflow } from 'features/nodes/util/validateWorkflow'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { setActiveTab } from 'features/ui/store/uiSlice'; +import { startAppListening } from '..'; + +export const addWorkflowLoadedListener = () => { + startAppListening({ + actionCreator: workflowLoadRequested, + effect: (action, { dispatch, getState }) => { + const log = logger('nodes'); + const workflow = action.payload; + const nodeTemplates = getState().nodes.nodeTemplates; + + const { workflow: validatedWorkflow, errors } = validateWorkflow( + workflow, + nodeTemplates + ); + + dispatch(workflowLoaded(validatedWorkflow)); + + if (!errors.length) { + dispatch( + addToast( + makeToast({ + title: 'Workflow Loaded', + status: 'success', + }) + ) + ); + } else { + dispatch( + addToast( + makeToast({ + title: 'Workflow Loaded with Warnings', + status: 'warning', + }) + ) + ); + errors.forEach(({ message, ...rest }) => { + log.warn(rest, message); + }); + } + + dispatch(setActiveTab('nodes')); + requestAnimationFrame(() => { + $flow.get()?.fitView(); + }); + }, + }); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 3559679fc4..846cf5a6f0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -17,16 +17,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; -import { workflowLoaded } from 'features/nodes/store/nodesSlice'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { - setActiveTab, setShouldShowImageDetails, setShouldShowProgressInViewer, } from 'features/ui/store/uiSlice'; @@ -124,16 +121,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { if (!workflow) { return; } - dispatch(workflowLoaded(workflow)); - dispatch(setActiveTab('nodes')); - dispatch( - addToast( - makeToast({ - title: 'Workflow Loaded', - status: 'success', - }) - ) - ); + dispatch(workflowLoadRequested(workflow)); }, [dispatch, workflow]); const handleClickUseAllParameters = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index e75a7745bb..90272a3a86 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -7,12 +7,9 @@ import { isModalOpenChanged, } from 'features/changeBoardModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; -import { workflowLoaded } from 'features/nodes/store/nodesSlice'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; @@ -36,6 +33,7 @@ import { } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; +import { workflowLoadRequested } from 'features/nodes/store/actions'; type SingleSelectionMenuItemsProps = { imageDTO: ImageDTO; @@ -102,16 +100,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { if (!workflow) { return; } - dispatch(workflowLoaded(workflow)); - dispatch(setActiveTab('nodes')); - dispatch( - addToast( - makeToast({ - title: 'Workflow Loaded', - status: 'success', - }) - ) - ); + dispatch(workflowLoadRequested(workflow)); }, [dispatch, workflow]); const handleSendToImageToImage = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index e8fb66d074..16af1fe12c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -3,6 +3,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { $flow } from 'features/nodes/store/reactFlowInstance'; import { contextMenusClosed } from 'features/ui/store/uiSlice'; import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -13,6 +14,7 @@ import { OnConnectStart, OnEdgesChange, OnEdgesDelete, + OnInit, OnMoveEnd, OnNodesChange, OnNodesDelete, @@ -147,6 +149,11 @@ export const Flow = () => { dispatch(contextMenusClosed()); }, [dispatch]); + const onInit: OnInit = useCallback((flow) => { + $flow.set(flow); + flow.fitView(); + }, []); + useHotkeys(['Ctrl+c', 'Meta+c'], (e) => { e.preventDefault(); dispatch(selectionCopied()); @@ -170,6 +177,7 @@ export const Flow = () => { edgeTypes={edgeTypes} nodes={nodes} edges={edges} + onInit={onInit} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onEdgesDelete={onEdgesDelete} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx index eae05688b5..143785ecfe 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx @@ -12,6 +12,7 @@ import { Tooltip, useDisclosure, } from '@chakra-ui/react'; +import { compare } from 'compare-versions'; import { useNodeData } from 'features/nodes/hooks/useNodeData'; import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel'; import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; @@ -20,6 +21,7 @@ import { isInvocationNodeData } from 'features/nodes/types/types'; import { memo, useMemo } from 'react'; import { FaInfoCircle } from 'react-icons/fa'; import NotesTextarea from './NotesTextarea'; +import { useDoNodeVersionsMatch } from 'features/nodes/hooks/useDoNodeVersionsMatch'; interface Props { nodeId: string; @@ -29,6 +31,7 @@ const InvocationNodeNotes = ({ nodeId }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); const label = useNodeLabel(nodeId); const title = useNodeTemplateTitle(nodeId); + const doVersionsMatch = useDoNodeVersionsMatch(nodeId); return ( <> @@ -50,7 +53,11 @@ const InvocationNodeNotes = ({ nodeId }: Props) => { > @@ -92,16 +99,59 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { return 'Unknown Node'; }, [data, nodeTemplate]); + const versionComponent = useMemo(() => { + if (!isInvocationNodeData(data) || !nodeTemplate) { + return null; + } + + if (!data.version) { + return ( + + Version unknown + + ); + } + + if (!nodeTemplate.version) { + return ( + + Version {data.version} (unknown template) + + ); + } + + if (compare(data.version, nodeTemplate.version, '<')) { + return ( + + Version {data.version} (update node) + + ); + } + + if (compare(data.version, nodeTemplate.version, '>')) { + return ( + + Version {data.version} (update app) + + ); + } + + return Version {data.version}; + }, [data, nodeTemplate]); + if (!isInvocationNodeData(data)) { return Unknown Node; } return ( - {title} + + {title} + {nodeTemplate?.description} + {versionComponent} {data?.notes && {data.notes}} ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNodeData.ts index a88a82e1fc..24982f591e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNodeData.ts @@ -138,13 +138,14 @@ export const useBuildNodeData = () => { data: { id: nodeId, type, - inputs, - outputs, - isOpen: true, + version: template.version, label: '', notes: '', + isOpen: true, embedWorkflow: false, isIntermediate: true, + inputs, + outputs, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts new file mode 100644 index 0000000000..926c56ac1e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts @@ -0,0 +1,33 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { compareVersions } from 'compare-versions'; +import { useMemo } from 'react'; +import { isInvocationNode } from '../types/types'; + +export const useDoNodeVersionsMatch = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + if (!nodeTemplate?.version || !node.data?.version) { + return false; + } + return compareVersions(nodeTemplate.version, node.data.version) === 0; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const nodeTemplate = useAppSelector(selector); + + return nodeTemplate; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx index 97f2cea77b..7f015ac5eb 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/nodes/hooks/useLoadWorkflowFromFile.tsx @@ -2,13 +2,13 @@ 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 { zValidatedWorkflow } from 'features/nodes/types/types'; +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 { ZodError } from 'zod'; import { fromZodError, fromZodIssue } from 'zod-validation-error'; +import { workflowLoadRequested } from '../store/actions'; export const useLoadWorkflowFromFile = () => { const dispatch = useAppDispatch(); @@ -24,7 +24,7 @@ export const useLoadWorkflowFromFile = () => { try { const parsedJSON = JSON.parse(String(rawJSON)); - const result = zValidatedWorkflow.safeParse(parsedJSON); + const result = zWorkflow.safeParse(parsedJSON); if (!result.success) { const { message } = fromZodError(result.error, { @@ -45,32 +45,8 @@ export const useLoadWorkflowFromFile = () => { reader.abort(); return; } - dispatch(workflowLoaded(result.data.workflow)); - if (!result.data.warnings.length) { - dispatch( - addToast( - makeToast({ - title: 'Workflow Loaded', - status: 'success', - }) - ) - ); - reader.abort(); - return; - } - - dispatch( - addToast( - makeToast({ - title: 'Workflow Loaded with Warnings', - status: 'warning', - }) - ) - ); - result.data.warnings.forEach(({ message, ...rest }) => { - logger.warn(rest, message); - }); + dispatch(workflowLoadRequested(result.data)); reader.abort(); } catch { diff --git a/invokeai/frontend/web/src/features/nodes/store/actions.ts b/invokeai/frontend/web/src/features/nodes/store/actions.ts index 2463a1e945..cf7ccf8238 100644 --- a/invokeai/frontend/web/src/features/nodes/store/actions.ts +++ b/invokeai/frontend/web/src/features/nodes/store/actions.ts @@ -1,5 +1,6 @@ import { createAction, isAnyOf } from '@reduxjs/toolkit'; import { Graph } from 'services/api/types'; +import { Workflow } from '../types/types'; export const textToImageGraphBuilt = createAction( 'nodes/textToImageGraphBuilt' @@ -16,3 +17,7 @@ export const isAnyGraphBuilt = isAnyOf( canvasGraphBuilt, nodesGraphBuilt ); + +export const workflowLoadRequested = createAction( + 'nodes/workflowLoadRequested' +); diff --git a/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts b/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts new file mode 100644 index 0000000000..e9094a9310 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/reactFlowInstance.ts @@ -0,0 +1,4 @@ +import { atom } from 'nanostores'; +import { ReactFlowInstance } from 'reactflow'; + +export const $flow = atom(null); diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index f7986a5028..402ef4ac7a 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -52,6 +52,10 @@ export type InvocationTemplate = { * The type of this node's output */ outputType: string; // TODO: generate a union of output types + /** + * The invocation's version. + */ + version?: string; }; export type FieldUIConfig = { @@ -962,6 +966,7 @@ export type InvocationSchemaExtra = { title: string; category?: string; tags?: string[]; + version?: string; properties: Omit< NonNullable & (_InputField | _OutputField), @@ -1095,6 +1100,29 @@ export const zCoreMetadata = z export type CoreMetadata = z.infer; +export const zSemVer = z.string().refine((val) => { + const [major, minor, patch] = val.split('.'); + return ( + major !== undefined && + Number.isInteger(Number(major)) && + minor !== undefined && + Number.isInteger(Number(minor)) && + patch !== undefined && + Number.isInteger(Number(patch)) + ); +}); + +export const zParsedSemver = zSemVer.transform((val) => { + const [major, minor, patch] = val.split('.'); + return { + major: Number(major), + minor: Number(minor), + patch: Number(patch), + }; +}); + +export type SemVer = z.infer; + 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 @@ -1107,6 +1135,7 @@ export const zInvocationNodeData = z.object({ notes: z.string(), embedWorkflow: z.boolean(), isIntermediate: z.boolean(), + version: zSemVer.optional(), }); // Massage this to get better type safety while developing @@ -1195,20 +1224,6 @@ export const zFieldIdentifier = z.object({ 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 type WorkflowWarning = { message: string; issues: string[]; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index 78e7495481..d8bb189abc 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -73,6 +73,7 @@ export const parseSchema = ( const title = schema.title.replace('Invocation', ''); const tags = schema.tags ?? []; const description = schema.description ?? ''; + const version = schema.version ?? ''; const inputs = reduce( schema.properties, @@ -225,11 +226,12 @@ export const parseSchema = ( const invocation: InvocationTemplate = { title, type, + version, tags, description, + outputType, inputs, outputs, - outputType, }; Object.assign(invocationsAccumulator, { [type]: invocation }); diff --git a/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts new file mode 100644 index 0000000000..a3085d516b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/validateWorkflow.ts @@ -0,0 +1,96 @@ +import { compareVersions } from 'compare-versions'; +import { cloneDeep, keyBy } from 'lodash-es'; +import { + InvocationTemplate, + Workflow, + WorkflowWarning, + isWorkflowInvocationNode, +} from '../types/types'; +import { parseify } from 'common/util/serialize'; + +export const validateWorkflow = ( + workflow: Workflow, + nodeTemplates: Record +) => { + const clone = cloneDeep(workflow); + const { nodes, edges } = clone; + const errors: WorkflowWarning[] = []; + const invocationNodes = nodes.filter(isWorkflowInvocationNode); + const keyedNodes = keyBy(invocationNodes, 'id'); + nodes.forEach((node) => { + if (!isWorkflowInvocationNode(node)) { + return; + } + + const nodeTemplate = nodeTemplates[node.data.type]; + if (!nodeTemplate) { + errors.push({ + message: `Node "${node.data.type}" skipped`, + issues: [`Node type "${node.data.type}" does not exist`], + data: node, + }); + return; + } + + if ( + nodeTemplate.version && + node.data.version && + compareVersions(nodeTemplate.version, node.data.version) !== 0 + ) { + errors.push({ + message: `Node "${node.data.type}" has mismatched version`, + issues: [ + `Node "${node.data.type}" v${node.data.version} may be incompatible with installed v${nodeTemplate.version}`, + ], + data: { node, nodeTemplate: parseify(nodeTemplate) }, + }); + return; + } + }); + edges.forEach((edge, i) => { + const sourceNode = keyedNodes[edge.source]; + const targetNode = keyedNodes[edge.target]; + const issues: string[] = []; + if (!sourceNode) { + issues.push(`Output node ${edge.source} does not exist`); + } else if ( + edge.type === 'default' && + !(edge.sourceHandle in sourceNode.data.outputs) + ) { + issues.push( + `Output field "${edge.source}.${edge.sourceHandle}" does not exist` + ); + } + if (!targetNode) { + issues.push(`Input node ${edge.target} does not exist`); + } else if ( + edge.type === 'default' && + !(edge.targetHandle in targetNode.data.inputs) + ) { + issues.push( + `Input field "${edge.target}.${edge.targetHandle}" does not exist` + ); + } + if (!nodeTemplates[sourceNode?.data.type ?? '__UNKNOWN_NODE_TYPE__']) { + issues.push( + `Source node "${edge.source}" missing template "${sourceNode?.data.type}"` + ); + } + if (!nodeTemplates[targetNode?.data.type ?? '__UNKNOWN_NODE_TYPE__']) { + issues.push( + `Source node "${edge.target}" missing template "${targetNode?.data.type}"` + ); + } + if (issues.length) { + delete edges[i]; + const src = edge.type === 'default' ? edge.sourceHandle : edge.source; + const tgt = edge.type === 'default' ? edge.targetHandle : edge.target; + errors.push({ + message: `Edge "${src} -> ${tgt}" skipped`, + issues, + data: edge, + }); + } + }); + return { workflow: clone, errors }; +}; diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index f48892113d..6e00d1b38b 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -6981,6 +6981,11 @@ export type components = { * @description The node's category */ category?: string; + /** + * Version + * @description The node's version. Should be a valid semver string e.g. "1.0.0" or "3.8.13". + */ + version?: string; }; /** * Input @@ -7036,24 +7041,12 @@ export type components = { /** Ui Order */ ui_order?: number; }; - /** - * StableDiffusionOnnxModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** * StableDiffusion1ModelFormat * @description An enumeration. * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. @@ -7066,6 +7059,18 @@ export type components = { * @enum {string} */ StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusionOnnxModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionOnnxModelFormat: "olive" | "onnx"; }; responses: never; parameters: never; diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 2c3c9ae88f..787c81a756 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -2970,6 +2970,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +compare-versions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" + integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== + compute-scroll-into-view@1.0.20: version "1.0.20" resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43"