From fdc444ed616a27f9cd45a7c1dafd83a2583cd115 Mon Sep 17 00:00:00 2001
From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
Date: Sun, 23 Jul 2023 15:24:04 +1200
Subject: [PATCH 1/3] fix: Fix app crashing when you upload an incorrect JSON
to node editor
---
invokeai/frontend/web/public/locales/en.json | 2 +
.../nodes/components/ui/LoadNodesButton.tsx | 78 ++++++++++++-------
2 files changed, 54 insertions(+), 26 deletions(-)
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 59cf87fbda..ab10276491 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -615,6 +615,8 @@
"initialImageNotSetDesc": "Could not load initial image",
"nodesSaved": "Nodes Saved",
"nodesLoaded": "Nodes Loaded",
+ "nodesNotValidGraph": "Not a valid InvokeAI Node Graph",
+ "nodesNotValidJSON": "Not a valid JSON",
"nodesLoadedFailed": "Failed To Load Nodes",
"nodesCleared": "Nodes Cleared"
},
diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx
index 706fbd8b31..2aa369bc11 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx
@@ -1,14 +1,28 @@
import { FileButton } from '@mantine/core';
-import { makeToast } from 'features/system/util/makeToast';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { loadFileEdges, loadFileNodes } from 'features/nodes/store/nodesSlice';
import { addToast } from 'features/system/store/systemSlice';
+import { makeToast } from 'features/system/util/makeToast';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
import { useReactFlow } from 'reactflow';
+interface JsonFile {
+ [key: string]: unknown;
+}
+
+function validateInvokeAIGraph(jsonFile: JsonFile): boolean {
+ const keys = ['nodes', 'edges', 'viewport'];
+ for (const key of keys) {
+ if (!(key in jsonFile)) {
+ return false;
+ }
+ }
+ return true;
+}
+
const LoadNodesButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
@@ -22,33 +36,45 @@ const LoadNodesButton = () => {
const reader = new FileReader();
reader.onload = async () => {
const json = reader.result;
- const retrievedNodeTree = await JSON.parse(String(json));
- if (!retrievedNodeTree) {
- dispatch(
- addToast(
- makeToast({
- title: t('toast.nodesLoadedFailed'),
- status: 'error',
- })
- )
- );
+ try {
+ const retrievedNodeTree = await JSON.parse(String(json));
+ const isValidNodeTree = validateInvokeAIGraph(retrievedNodeTree);
+
+ if (isValidNodeTree) {
+ dispatch(loadFileNodes(retrievedNodeTree.nodes));
+ dispatch(loadFileEdges(retrievedNodeTree.edges));
+ fitView();
+
+ dispatch(
+ addToast(
+ makeToast({ title: t('toast.nodesLoaded'), status: 'success' })
+ )
+ );
+ } else {
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('toast.nodesNotValidGraph'),
+ status: 'error',
+ })
+ )
+ );
+ }
+ // Cleanup
+ reader.abort();
+ } catch (error) {
+ if (error) {
+ dispatch(
+ addToast(
+ makeToast({
+ title: t('toast.nodesNotValidJSON'),
+ status: 'error',
+ })
+ )
+ );
+ }
}
-
- if (retrievedNodeTree) {
- dispatch(loadFileNodes(retrievedNodeTree.nodes));
- dispatch(loadFileEdges(retrievedNodeTree.edges));
- fitView();
-
- dispatch(
- addToast(
- makeToast({ title: t('toast.nodesLoaded'), status: 'success' })
- )
- );
- }
-
- // Cleanup
- reader.abort();
};
reader.readAsText(v);
From 225f60855647147e6e4a67461df36c6fc437cf3d Mon Sep 17 00:00:00 2001
From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
Date: Sun, 23 Jul 2023 16:49:52 +1200
Subject: [PATCH 2/3] fix: Add more sanity checks & rename buttons to Graphs
---
invokeai/frontend/web/public/locales/en.json | 6 ++---
.../components/panels/TopCenterPanel.tsx | 12 ++++-----
...arNodesButton.tsx => ClearGraphButton.tsx} | 16 ++++++------
...oadNodesButton.tsx => LoadGraphButton.tsx} | 25 +++++++++++++------
...aveNodesButton.tsx => SaveGraphButton.tsx} | 8 +++---
5 files changed, 39 insertions(+), 28 deletions(-)
rename invokeai/frontend/web/src/features/nodes/components/ui/{ClearNodesButton.tsx => ClearGraphButton.tsx} (87%)
rename invokeai/frontend/web/src/features/nodes/components/ui/{LoadNodesButton.tsx => LoadGraphButton.tsx} (82%)
rename invokeai/frontend/web/src/features/nodes/components/ui/{SaveNodesButton.tsx => SaveGraphButton.tsx} (90%)
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index ab10276491..404c2013e4 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -702,9 +702,9 @@
},
"nodes": {
"reloadSchema": "Reload Schema",
- "saveNodes": "Save Nodes",
- "loadNodes": "Load Nodes",
- "clearNodes": "Clear Nodes",
+ "saveGraph": "Save Graph",
+ "loadGraph": "Load Graph (saved from Node Editor) (Do not copy-paste metadata)",
+ "clearGraph": "Clear Graph",
"zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out",
"fitViewportNodes": "Fit View",
diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx
index 90f8039285..21076e16f5 100644
--- a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx
@@ -2,11 +2,11 @@ import { HStack } from '@chakra-ui/react';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import { memo } from 'react';
import { Panel } from 'reactflow';
-import LoadNodesButton from '../ui/LoadNodesButton';
+import ClearGraphButton from '../ui/ClearGraphButton';
+import LoadGraphButton from '../ui/LoadGraphButton';
import NodeInvokeButton from '../ui/NodeInvokeButton';
import ReloadSchemaButton from '../ui/ReloadSchemaButton';
-import SaveNodesButton from '../ui/SaveNodesButton';
-import ClearNodesButton from '../ui/ClearNodesButton';
+import SaveGraphButton from '../ui/SaveGraphButton';
const TopCenterPanel = () => {
return (
@@ -15,9 +15,9 @@ const TopCenterPanel = () => {
-
-
-
+
+
+
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/ClearNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx
similarity index 87%
rename from invokeai/frontend/web/src/features/nodes/components/ui/ClearNodesButton.tsx
rename to invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx
index 86d9d08a84..88fb60ee0f 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ui/ClearNodesButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx
@@ -9,17 +9,17 @@ import {
Text,
useDisclosure,
} from '@chakra-ui/react';
-import { makeToast } from 'features/system/util/makeToast';
import { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
import { addToast } from 'features/system/store/systemSlice';
-import { memo, useRef, useCallback } from 'react';
+import { makeToast } from 'features/system/util/makeToast';
+import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
-const ClearNodesButton = () => {
+const ClearGraphButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -46,8 +46,8 @@ const ClearNodesButton = () => {
<>
}
- tooltip={t('nodes.clearNodes')}
- aria-label={t('nodes.clearNodes')}
+ tooltip={t('nodes.clearGraph')}
+ aria-label={t('nodes.clearGraph')}
onClick={onOpen}
isDisabled={nodes.length === 0}
/>
@@ -62,11 +62,11 @@ const ClearNodesButton = () => {
- {t('nodes.clearNodes')}
+ {t('nodes.clearGraph')}
- {t('common.clearNodes')}
+ {t('common.clearGraph')}
@@ -83,4 +83,4 @@ const ClearNodesButton = () => {
);
};
-export default memo(ClearNodesButton);
+export default memo(ClearGraphButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx
similarity index 82%
rename from invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx
rename to invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx
index 2aa369bc11..437418e18a 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ui/LoadNodesButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx
@@ -13,17 +13,28 @@ interface JsonFile {
[key: string]: unknown;
}
-function validateInvokeAIGraph(jsonFile: JsonFile): boolean {
+function sanityCheckInvokeAIGraph(jsonFile: JsonFile): boolean {
const keys = ['nodes', 'edges', 'viewport'];
for (const key of keys) {
if (!(key in jsonFile)) {
return false;
}
}
+
+ if (!Array.isArray(jsonFile.nodes) || !Array.isArray(jsonFile.edges)) {
+ return false;
+ }
+
+ for (const node of jsonFile.nodes) {
+ if (!('data' in node)) {
+ return false;
+ }
+ }
+
return true;
}
-const LoadNodesButton = () => {
+const LoadGraphButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { fitView } = useReactFlow();
@@ -39,9 +50,9 @@ const LoadNodesButton = () => {
try {
const retrievedNodeTree = await JSON.parse(String(json));
- const isValidNodeTree = validateInvokeAIGraph(retrievedNodeTree);
+ const isSaneNodeTree = sanityCheckInvokeAIGraph(retrievedNodeTree);
- if (isValidNodeTree) {
+ if (isSaneNodeTree) {
dispatch(loadFileNodes(retrievedNodeTree.nodes));
dispatch(loadFileEdges(retrievedNodeTree.edges));
fitView();
@@ -93,8 +104,8 @@ const LoadNodesButton = () => {
{(props) => (
}
- tooltip={t('nodes.loadNodes')}
- aria-label={t('nodes.loadNodes')}
+ tooltip={t('nodes.loadGraph')}
+ aria-label={t('nodes.loadGraph')}
{...props}
/>
)}
@@ -102,4 +113,4 @@ const LoadNodesButton = () => {
);
};
-export default memo(LoadNodesButton);
+export default memo(LoadGraphButton);
diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/SaveNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/SaveGraphButton.tsx
similarity index 90%
rename from invokeai/frontend/web/src/features/nodes/components/ui/SaveNodesButton.tsx
rename to invokeai/frontend/web/src/features/nodes/components/ui/SaveGraphButton.tsx
index 5833182456..42e545258e 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ui/SaveNodesButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ui/SaveGraphButton.tsx
@@ -6,7 +6,7 @@ import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSave } from 'react-icons/fa';
-const SaveNodesButton = () => {
+const SaveGraphButton = () => {
const { t } = useTranslation();
const editorInstance = useAppSelector(
(state: RootState) => state.nodes.editorInstance
@@ -37,12 +37,12 @@ const SaveNodesButton = () => {
}
fontSize={18}
- tooltip={t('nodes.saveNodes')}
- aria-label={t('nodes.saveNodes')}
+ tooltip={t('nodes.saveGraph')}
+ aria-label={t('nodes.saveGraph')}
onClick={saveEditorToJSON}
isDisabled={nodes.length === 0}
/>
);
};
-export default memo(SaveNodesButton);
+export default memo(SaveGraphButton);
From af4579b4d494d37ae1615f94ae61c7631c28bc79 Mon Sep 17 00:00:00 2001
From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
Date: Sun, 23 Jul 2023 18:12:25 +1200
Subject: [PATCH 3/3] feat: Add more sanity checks for graph loading
---
invokeai/frontend/web/public/locales/en.json | 7 +-
.../nodes/components/ui/ClearGraphButton.tsx | 2 +-
.../nodes/components/ui/LoadGraphButton.tsx | 71 +++++++++++++++----
3 files changed, 64 insertions(+), 16 deletions(-)
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 404c2013e4..0640ab9ef0 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -102,8 +102,7 @@
"openInNewTab": "Open in New Tab",
"dontAskMeAgain": "Don't ask me again",
"areYouSure": "Are you sure?",
- "imagePrompt": "Image Prompt",
- "clearNodes": "Are you sure you want to clear all nodes?"
+ "imagePrompt": "Image Prompt"
},
"gallery": {
"generations": "Generations",
@@ -617,6 +616,9 @@
"nodesLoaded": "Nodes Loaded",
"nodesNotValidGraph": "Not a valid InvokeAI Node Graph",
"nodesNotValidJSON": "Not a valid JSON",
+ "nodesCorruptedGraph": "Cannot load. Graph seems to be corrupted.",
+ "nodesUnrecognizedTypes": "Cannot load. Graph has unrecognized types",
+ "nodesBrokenConnections": "Cannot load. Some connections are broken.",
"nodesLoadedFailed": "Failed To Load Nodes",
"nodesCleared": "Nodes Cleared"
},
@@ -705,6 +707,7 @@
"saveGraph": "Save Graph",
"loadGraph": "Load Graph (saved from Node Editor) (Do not copy-paste metadata)",
"clearGraph": "Clear Graph",
+ "clearGraphDesc": "Are you sure you want to clear all nodes?",
"zoomInNodes": "Zoom In",
"zoomOutNodes": "Zoom Out",
"fitViewportNodes": "Fit View",
diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx
index 88fb60ee0f..432675c5cd 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx
@@ -66,7 +66,7 @@ const ClearGraphButton = () => {
- {t('common.clearGraph')}
+ {t('nodes.clearGraphDesc')}
diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx
index 437418e18a..44d93bb8fe 100644
--- a/invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/ui/LoadGraphButton.tsx
@@ -4,6 +4,7 @@ import IAIIconButton from 'common/components/IAIIconButton';
import { loadFileEdges, loadFileNodes } from 'features/nodes/store/nodesSlice';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
+import i18n from 'i18n';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaUpload } from 'react-icons/fa';
@@ -13,25 +14,70 @@ interface JsonFile {
[key: string]: unknown;
}
-function sanityCheckInvokeAIGraph(jsonFile: JsonFile): boolean {
+function sanityCheckInvokeAIGraph(jsonFile: JsonFile): {
+ isValid: boolean;
+ message: string;
+} {
+ // Check if primary keys exist
const keys = ['nodes', 'edges', 'viewport'];
for (const key of keys) {
if (!(key in jsonFile)) {
- return false;
+ return {
+ isValid: false,
+ message: i18n.t('toast.nodesNotValidGraph'),
+ };
}
}
+ // Check if nodes and edges are arrays
if (!Array.isArray(jsonFile.nodes) || !Array.isArray(jsonFile.edges)) {
- return false;
+ return {
+ isValid: false,
+ message: i18n.t('toast.nodesNotValidGraph'),
+ };
}
- for (const node of jsonFile.nodes) {
- if (!('data' in node)) {
- return false;
+ // Check if data is present in nodes
+ const nodeKeys = ['data', 'type'];
+ const nodeTypes = ['invocation', 'progress_image'];
+ if (jsonFile.nodes.length > 0) {
+ for (const node of jsonFile.nodes) {
+ for (const nodeKey of nodeKeys) {
+ if (!(nodeKey in node)) {
+ return {
+ isValid: false,
+ message: i18n.t('toast.nodesNotValidGraph'),
+ };
+ }
+ if (nodeKey === 'type' && !nodeTypes.includes(node[nodeKey])) {
+ return {
+ isValid: false,
+ message: i18n.t('toast.nodesUnrecognizedTypes'),
+ };
+ }
+ }
}
}
- return true;
+ // Check Edge Object
+ const edgeKeys = ['source', 'sourceHandle', 'target', 'targetHandle'];
+ if (jsonFile.edges.length > 0) {
+ for (const edge of jsonFile.edges) {
+ for (const edgeKey of edgeKeys) {
+ if (!(edgeKey in edge)) {
+ return {
+ isValid: false,
+ message: i18n.t('toast.nodesBrokenConnections'),
+ };
+ }
+ }
+ }
+ }
+
+ return {
+ isValid: true,
+ message: i18n.t('toast.nodesLoaded'),
+ };
}
const LoadGraphButton = () => {
@@ -50,23 +96,22 @@ const LoadGraphButton = () => {
try {
const retrievedNodeTree = await JSON.parse(String(json));
- const isSaneNodeTree = sanityCheckInvokeAIGraph(retrievedNodeTree);
+ const { isValid, message } =
+ sanityCheckInvokeAIGraph(retrievedNodeTree);
- if (isSaneNodeTree) {
+ if (isValid) {
dispatch(loadFileNodes(retrievedNodeTree.nodes));
dispatch(loadFileEdges(retrievedNodeTree.edges));
fitView();
dispatch(
- addToast(
- makeToast({ title: t('toast.nodesLoaded'), status: 'success' })
- )
+ addToast(makeToast({ title: message, status: 'success' }))
);
} else {
dispatch(
addToast(
makeToast({
- title: t('toast.nodesNotValidGraph'),
+ title: message,
status: 'error',
})
)