feat: Save and Loads Nodes From Disk

This commit is contained in:
blessedcoolant 2023-07-11 07:18:13 +12:00
parent f46f8058be
commit b9767e9c6e
6 changed files with 157 additions and 2 deletions

View File

@ -593,7 +593,10 @@
"metadataLoadFailed": "Failed to load metadata",
"initialImageSet": "Initial Image Set",
"initialImageNotSet": "Initial Image Not Set",
"initialImageNotSetDesc": "Could not load initial image"
"initialImageNotSetDesc": "Could not load initial image",
"nodesSaved": "Nodes Saved",
"nodesLoaded": "Nodes Loaded",
"nodesLoadedFailed": "Failed To Load Nodes"
},
"tooltip": {
"feature": {
@ -674,5 +677,9 @@
"showProgressImages": "Show Progress Images",
"hideProgressImages": "Hide Progress Images",
"swapSizes": "Swap Sizes"
},
"nodes": {
"saveNodes": "Save Nodes",
"loadNodes": "Load Nodes"
}
}

View File

@ -7,6 +7,7 @@ import {
OnConnectEnd,
OnConnectStart,
OnEdgesChange,
OnInit,
OnNodesChange,
ReactFlow,
} from 'reactflow';
@ -16,6 +17,7 @@ import {
connectionStarted,
edgesChanged,
nodesChanged,
setEditorInstance,
} from '../store/nodesSlice';
import { InvocationComponent } from './InvocationComponent';
import ProgressImageNode from './ProgressImageNode';
@ -67,6 +69,13 @@ export const Flow = () => {
dispatch(connectionEnded());
}, [dispatch]);
const onInit: OnInit = useCallback(
(v) => {
dispatch(setEditorInstance(v));
},
[dispatch]
);
return (
<ReactFlow
nodeTypes={nodeTypes}
@ -77,6 +86,7 @@ export const Flow = () => {
onConnectStart={onConnectStart}
onConnect={onConnect}
onConnectEnd={onConnectEnd}
onInit={onInit}
defaultEdgeOptions={{
style: { strokeWidth: 2 },
}}

View File

@ -1,11 +1,13 @@
import { HStack } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import { memo, useCallback } from 'react';
import { Panel } from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import LoadNodesButton from '../ui/LoadNodesButton';
import NodeInvokeButton from '../ui/NodeInvokeButton';
import CancelButton from 'features/parameters/components/ProcessButtons/CancelButton';
import SaveNodesButton from '../ui/SaveNodesButton';
const TopCenterPanel = () => {
const dispatch = useAppDispatch();
@ -20,6 +22,8 @@ const TopCenterPanel = () => {
<NodeInvokeButton />
<CancelButton />
<IAIButton onClick={handleReloadSchema}>Reload Schema</IAIButton>
<SaveNodesButton />
<LoadNodesButton />
</HStack>
</Panel>
);

View File

@ -0,0 +1,80 @@
import { FileButton } from '@mantine/core';
import { makeToast } from 'app/components/Toaster';
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 { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { BiUpload } from 'react-icons/bi';
import { useReactFlow } from 'reactflow';
const LoadNodesButton = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { setViewport } = useReactFlow();
const uploadedFileRef = useRef<() => void>(null);
const restoreJSONToEditor = useCallback(
(v: File | null) => {
if (!v) return;
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',
})
)
);
}
if (retrievedNodeTree) {
const { x = 0, y = 0, zoom = 1 } = retrievedNodeTree.viewport;
dispatch(loadFileNodes(retrievedNodeTree.nodes));
dispatch(loadFileEdges(retrievedNodeTree.edges));
setViewport({ x, y, zoom });
dispatch(
addToast(
makeToast({ title: t('toast.nodesLoaded'), status: 'success' })
)
);
}
// Cleanup
reader.abort();
};
reader.readAsText(v);
// Cleanup
uploadedFileRef.current?.();
},
[setViewport, dispatch, t]
);
return (
<FileButton
resetRef={uploadedFileRef}
accept="application/json"
onChange={restoreJSONToEditor}
>
{(props) => (
<IAIIconButton
icon={<BiUpload />}
fontSize={20}
tooltip={t('nodes.loadNodes')}
aria-label={t('nodes.loadNodes')}
{...props}
/>
)}
</FileButton>
);
};
export default memo(LoadNodesButton);

View File

@ -0,0 +1,39 @@
import { RootState } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { BiSave } from 'react-icons/bi';
const SaveNodesButton = () => {
const { t } = useTranslation();
const editorInstance = useAppSelector(
(state: RootState) => state.nodes.editorInstance
);
const saveEditorToJSON = useCallback(() => {
if (editorInstance) {
const editorState = editorInstance.toObject();
const nodeSetupJSON = new Blob([JSON.stringify(editorState)]);
const nodeDownloadElement = document.createElement('a');
nodeDownloadElement.href = URL.createObjectURL(nodeSetupJSON);
nodeDownloadElement.download = 'MyNodes.json';
document.body.appendChild(nodeDownloadElement);
nodeDownloadElement.click();
// Cleanup
nodeDownloadElement.remove();
}
}, [editorInstance]);
return (
<IAIIconButton
icon={<BiSave />}
fontSize={20}
tooltip={t('nodes.saveNodes')}
aria-label={t('nodes.saveNodes')}
onClick={saveEditorToJSON}
/>
);
};
export default memo(SaveNodesButton);

View File

@ -13,6 +13,7 @@ import {
Node,
NodeChange,
OnConnectStartParams,
ReactFlowInstance,
} from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { ImageField } from 'services/api/types';
@ -25,6 +26,7 @@ export type NodesState = {
invocationTemplates: Record<string, InvocationTemplate>;
connectionStartParams: OnConnectStartParams | null;
shouldShowGraphOverlay: boolean;
editorInstance: ReactFlowInstance | undefined;
};
export const initialNodesState: NodesState = {
@ -34,6 +36,7 @@ export const initialNodesState: NodesState = {
invocationTemplates: {},
connectionStartParams: null,
shouldShowGraphOverlay: false,
editorInstance: undefined,
};
const nodesSlice = createSlice({
@ -121,6 +124,15 @@ const nodesSlice = createSlice({
nodeEditorReset: () => {
return { ...initialNodesState };
},
setEditorInstance: (state, action) => {
state.editorInstance = action.payload;
},
loadFileNodes: (state, action: PayloadAction<Node<InvocationValue>[]>) => {
state.nodes = action.payload;
},
loadFileEdges: (state, action: PayloadAction<Edge[]>) => {
state.edges = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
@ -141,6 +153,9 @@ export const {
nodeTemplatesBuilt,
nodeEditorReset,
imageCollectionFieldValueChanged,
setEditorInstance,
loadFileNodes,
loadFileEdges,
} = nodesSlice.actions;
export default nodesSlice.reducer;