Skip to content

Commit 4aca264

Browse files
feat(ui): handle node versions
- Node versions are now added to node templates - Node data (including in workflows) include the version of the node - On loading a workflow, we check to see if the node and template versions match exactly. If not, a warning is logged to console. - The node info icon (top-right corner of node, which you may click to open the notes editor) now shows the version and mentions any issues. - Some workflow validation logic has been shifted around and is now executed in a redux listener.
1 parent d9148fb commit 4aca264

File tree

17 files changed

+322
-85
lines changed

17 files changed

+322
-85
lines changed

invokeai/frontend/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@reduxjs/toolkit": "^1.9.5",
7676
"@roarr/browser-log-writer": "^1.1.5",
7777
"@stevebel/png": "^1.5.1",
78+
"compare-versions": "^6.1.0",
7879
"dateformat": "^5.0.3",
7980
"formik": "^2.4.3",
8081
"framer-motion": "^10.16.1",

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas';
8484
import { addUserInvokedImageToImageListener } from './listeners/userInvokedImageToImage';
8585
import { addUserInvokedNodesListener } from './listeners/userInvokedNodes';
8686
import { addUserInvokedTextToImageListener } from './listeners/userInvokedTextToImage';
87+
import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
8788

8889
export const listenerMiddleware = createListenerMiddleware();
8990

@@ -202,6 +203,9 @@ addBoardIdSelectedListener();
202203
// Node schemas
203204
addReceivedOpenAPISchemaListener();
204205

206+
// Workflows
207+
addWorkflowLoadedListener();
208+
205209
// DND
206210
addImageDroppedListener();
207211

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { logger } from 'app/logging/logger';
2+
import { workflowLoadRequested } from 'features/nodes/store/actions';
3+
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
4+
import { $flow } from 'features/nodes/store/reactFlowInstance';
5+
import { validateWorkflow } from 'features/nodes/util/validateWorkflow';
6+
import { addToast } from 'features/system/store/systemSlice';
7+
import { makeToast } from 'features/system/util/makeToast';
8+
import { setActiveTab } from 'features/ui/store/uiSlice';
9+
import { startAppListening } from '..';
10+
11+
export const addWorkflowLoadedListener = () => {
12+
startAppListening({
13+
actionCreator: workflowLoadRequested,
14+
effect: (action, { dispatch, getState }) => {
15+
const log = logger('nodes');
16+
const workflow = action.payload;
17+
const nodeTemplates = getState().nodes.nodeTemplates;
18+
19+
const { workflow: validatedWorkflow, errors } = validateWorkflow(
20+
workflow,
21+
nodeTemplates
22+
);
23+
24+
dispatch(workflowLoaded(validatedWorkflow));
25+
26+
if (!errors.length) {
27+
dispatch(
28+
addToast(
29+
makeToast({
30+
title: 'Workflow Loaded',
31+
status: 'success',
32+
})
33+
)
34+
);
35+
} else {
36+
dispatch(
37+
addToast(
38+
makeToast({
39+
title: 'Workflow Loaded with Warnings',
40+
status: 'warning',
41+
})
42+
)
43+
);
44+
errors.forEach(({ message, ...rest }) => {
45+
log.warn(rest, message);
46+
});
47+
}
48+
49+
dispatch(setActiveTab('nodes'));
50+
requestAnimationFrame(() => {
51+
$flow.get()?.fitView();
52+
});
53+
},
54+
});
55+
};

invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
1717
import IAIIconButton from 'common/components/IAIIconButton';
1818
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
1919
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
20-
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
20+
import { workflowLoadRequested } from 'features/nodes/store/actions';
2121
import ParamUpscalePopover from 'features/parameters/components/Parameters/Upscale/ParamUpscaleSettings';
2222
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
2323
import { initialImageSelected } from 'features/parameters/store/actions';
2424
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
25-
import { addToast } from 'features/system/store/systemSlice';
26-
import { makeToast } from 'features/system/util/makeToast';
2725
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
2826
import {
29-
setActiveTab,
3027
setShouldShowImageDetails,
3128
setShouldShowProgressInViewer,
3229
} from 'features/ui/store/uiSlice';
@@ -124,16 +121,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
124121
if (!workflow) {
125122
return;
126123
}
127-
dispatch(workflowLoaded(workflow));
128-
dispatch(setActiveTab('nodes'));
129-
dispatch(
130-
addToast(
131-
makeToast({
132-
title: 'Workflow Loaded',
133-
status: 'success',
134-
})
135-
)
136-
);
124+
dispatch(workflowLoadRequested(workflow));
137125
}, [dispatch, workflow]);
138126

139127
const handleClickUseAllParameters = useCallback(() => {

invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@ import {
77
isModalOpenChanged,
88
} from 'features/changeBoardModal/store/slice';
99
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
10-
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
1110
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
1211
import { initialImageSelected } from 'features/parameters/store/actions';
1312
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
14-
import { addToast } from 'features/system/store/systemSlice';
15-
import { makeToast } from 'features/system/util/makeToast';
1613
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
1714
import { setActiveTab } from 'features/ui/store/uiSlice';
1815
import { memo, useCallback } from 'react';
@@ -36,6 +33,7 @@ import {
3633
} from 'services/api/endpoints/images';
3734
import { ImageDTO } from 'services/api/types';
3835
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
36+
import { workflowLoadRequested } from 'features/nodes/store/actions';
3937

4038
type SingleSelectionMenuItemsProps = {
4139
imageDTO: ImageDTO;
@@ -102,16 +100,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
102100
if (!workflow) {
103101
return;
104102
}
105-
dispatch(workflowLoaded(workflow));
106-
dispatch(setActiveTab('nodes'));
107-
dispatch(
108-
addToast(
109-
makeToast({
110-
title: 'Workflow Loaded',
111-
status: 'success',
112-
})
113-
)
114-
);
103+
dispatch(workflowLoadRequested(workflow));
115104
}, [dispatch, workflow]);
116105

117106
const handleSendToImageToImage = useCallback(() => {

invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
33
import { stateSelector } from 'app/store/store';
44
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
55
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
6+
import { $flow } from 'features/nodes/store/reactFlowInstance';
67
import { contextMenusClosed } from 'features/ui/store/uiSlice';
78
import { useCallback } from 'react';
89
import { useHotkeys } from 'react-hotkeys-hook';
@@ -13,6 +14,7 @@ import {
1314
OnConnectStart,
1415
OnEdgesChange,
1516
OnEdgesDelete,
17+
OnInit,
1618
OnMoveEnd,
1719
OnNodesChange,
1820
OnNodesDelete,
@@ -147,6 +149,11 @@ export const Flow = () => {
147149
dispatch(contextMenusClosed());
148150
}, [dispatch]);
149151

152+
const onInit: OnInit = useCallback((flow) => {
153+
$flow.set(flow);
154+
flow.fitView();
155+
}, []);
156+
150157
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
151158
e.preventDefault();
152159
dispatch(selectionCopied());
@@ -170,6 +177,7 @@ export const Flow = () => {
170177
edgeTypes={edgeTypes}
171178
nodes={nodes}
172179
edges={edges}
180+
onInit={onInit}
173181
onNodesChange={onNodesChange}
174182
onEdgesChange={onEdgesChange}
175183
onEdgesDelete={onEdgesDelete}

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotes.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Tooltip,
1313
useDisclosure,
1414
} from '@chakra-ui/react';
15+
import { compare } from 'compare-versions';
1516
import { useNodeData } from 'features/nodes/hooks/useNodeData';
1617
import { useNodeLabel } from 'features/nodes/hooks/useNodeLabel';
1718
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
@@ -20,6 +21,7 @@ import { isInvocationNodeData } from 'features/nodes/types/types';
2021
import { memo, useMemo } from 'react';
2122
import { FaInfoCircle } from 'react-icons/fa';
2223
import NotesTextarea from './NotesTextarea';
24+
import { useDoNodeVersionsMatch } from 'features/nodes/hooks/useDoNodeVersionsMatch';
2325

2426
interface Props {
2527
nodeId: string;
@@ -29,6 +31,7 @@ const InvocationNodeNotes = ({ nodeId }: Props) => {
2931
const { isOpen, onOpen, onClose } = useDisclosure();
3032
const label = useNodeLabel(nodeId);
3133
const title = useNodeTemplateTitle(nodeId);
34+
const doVersionsMatch = useDoNodeVersionsMatch(nodeId);
3235

3336
return (
3437
<>
@@ -50,7 +53,11 @@ const InvocationNodeNotes = ({ nodeId }: Props) => {
5053
>
5154
<Icon
5255
as={FaInfoCircle}
53-
sx={{ boxSize: 4, w: 8, color: 'base.400' }}
56+
sx={{
57+
boxSize: 4,
58+
w: 8,
59+
color: doVersionsMatch ? 'base.400' : 'error.400',
60+
}}
5461
/>
5562
</Flex>
5663
</Tooltip>
@@ -92,16 +99,59 @@ const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
9299
return 'Unknown Node';
93100
}, [data, nodeTemplate]);
94101

102+
const versionComponent = useMemo(() => {
103+
if (!isInvocationNodeData(data) || !nodeTemplate) {
104+
return null;
105+
}
106+
107+
if (!data.version) {
108+
return (
109+
<Text as="span" sx={{ color: 'error.500' }}>
110+
Version unknown
111+
</Text>
112+
);
113+
}
114+
115+
if (!nodeTemplate.version) {
116+
return (
117+
<Text as="span" sx={{ color: 'error.500' }}>
118+
Version {data.version} (unknown template)
119+
</Text>
120+
);
121+
}
122+
123+
if (compare(data.version, nodeTemplate.version, '<')) {
124+
return (
125+
<Text as="span" sx={{ color: 'error.500' }}>
126+
Version {data.version} (update node)
127+
</Text>
128+
);
129+
}
130+
131+
if (compare(data.version, nodeTemplate.version, '>')) {
132+
return (
133+
<Text as="span" sx={{ color: 'error.500' }}>
134+
Version {data.version} (update app)
135+
</Text>
136+
);
137+
}
138+
139+
return <Text as="span">Version {data.version}</Text>;
140+
}, [data, nodeTemplate]);
141+
95142
if (!isInvocationNodeData(data)) {
96143
return <Text sx={{ fontWeight: 600 }}>Unknown Node</Text>;
97144
}
98145

99146
return (
100147
<Flex sx={{ flexDir: 'column' }}>
101-
<Text sx={{ fontWeight: 600 }}>{title}</Text>
148+
<Text as="span" sx={{ fontWeight: 600 }}>
149+
{title}
150+
</Text>
102151
<Text sx={{ opacity: 0.7, fontStyle: 'oblique 5deg' }}>
103152
{nodeTemplate?.description}
104153
</Text>
154+
{versionComponent}
105155
{data?.notes && <Text>{data.notes}</Text>}
106156
</Flex>
107157
);

invokeai/frontend/web/src/features/nodes/hooks/useBuildNodeData.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,14 @@ export const useBuildNodeData = () => {
138138
data: {
139139
id: nodeId,
140140
type,
141-
inputs,
142-
outputs,
143-
isOpen: true,
141+
version: template.version,
144142
label: '',
145143
notes: '',
144+
isOpen: true,
146145
embedWorkflow: false,
147146
isIntermediate: true,
147+
inputs,
148+
outputs,
148149
},
149150
};
150151

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createSelector } from '@reduxjs/toolkit';
2+
import { stateSelector } from 'app/store/store';
3+
import { useAppSelector } from 'app/store/storeHooks';
4+
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
5+
import { compareVersions } from 'compare-versions';
6+
import { useMemo } from 'react';
7+
import { isInvocationNode } from '../types/types';
8+
9+
export const useDoNodeVersionsMatch = (nodeId: string) => {
10+
const selector = useMemo(
11+
() =>
12+
createSelector(
13+
stateSelector,
14+
({ nodes }) => {
15+
const node = nodes.nodes.find((node) => node.id === nodeId);
16+
if (!isInvocationNode(node)) {
17+
return false;
18+
}
19+
const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
20+
if (!nodeTemplate?.version || !node.data?.version) {
21+
return false;
22+
}
23+
return compareVersions(nodeTemplate.version, node.data.version) === 0;
24+
},
25+
defaultSelectorOptions
26+
),
27+
[nodeId]
28+
);
29+
30+
const nodeTemplate = useAppSelector(selector);
31+
32+
return nodeTemplate;
33+
};

0 commit comments

Comments
 (0)