diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx
new file mode 100644
index 0000000..ff0c041
--- /dev/null
+++ b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx
@@ -0,0 +1,136 @@
+import React from "react";
+import useFlowStore from "../VisProgStores";
+import styles from "../../VisProg.module.css";
+import { useReactFlow, type Edge } from "@xyflow/react";
+import type { AppNode } from "../VisProgTypes";
+
+type SavedProject = {
+ version: 1;
+ name: string;
+ savedAt: string; // ISO timestamp
+ nodes: AppNode[];
+ edges: Edge[];
+};
+
+
+
+function makeProjectBlob(name: string, nodes: AppNode[], edges: Edge[]): Blob {
+ const payload = {
+ version: 1,
+ name,
+ savedAt: new Date().toISOString(),
+ nodes,
+ edges,
+ };
+ return new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
+}
+
+async function saveWithPicker(defaultName: string, blob: Blob) {
+ // @ts-expect-error: not in lib.dom.d.ts everywhere
+ if (window.showSaveFilePicker) {
+ // @ts-expect-error
+ const handle = await window.showSaveFilePicker({
+ suggestedName: `${defaultName}.visprog.json`,
+ types: [{ description: "Visual Program Project", accept: { "application/json": [".visprog.json", ".json"] } }],
+ });
+ const writable = await handle.createWritable();
+ await writable.write(blob);
+ await writable.close();
+ return;
+ }
+ // Fallback if File system API is not supported
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${defaultName}.visprog.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
+async function loadWithPicker(): Promise
{
+ try {
+ // @ts-expect-error
+ if (window.showOpenFilePicker) {
+ // @ts-expect-error
+ const [handle] = await window.showOpenFilePicker({
+ multiple: false,
+ types: [{ description: "Visual Program Project", accept: { "application/json": [".visprog.json", ".json", ".txt"] } }],
+ });
+ const file = await handle.getFile();
+ return JSON.parse(await file.text()) as SavedProject;
+ }
+ // Fallback: input
+ return await new Promise((resolve) => {
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = ".visprog.json,.json,.txt,application/json,text/plain";
+ input.onchange = async () => {
+ const file = input.files?.[0];
+ if (!file) return resolve(null);
+ try {
+ resolve(JSON.parse(await file.text()) as SavedProject);
+ } catch {
+ resolve(null);
+ }
+ };
+ input.click();
+ });
+ } catch {
+ return null;
+ }
+}
+
+export default function SaveLoadPanel() {
+ const nodes = useFlowStore((s) => s.nodes) as AppNode[];
+ const edges = useFlowStore((s) => s.edges) as Edge[];
+ const setNodes = useFlowStore((s) => s.setNodes);
+ const setEdges = useFlowStore((s) => s.setEdges);
+
+ const onSave = async () => {
+ try {
+ const nameGuess =
+ (nodes.find((n) => n.type === "start")?.data?.label as string) || "visual-program";
+ const blob = makeProjectBlob(nameGuess, nodes, edges);
+ await saveWithPicker(nameGuess, blob);
+ } catch (e) {
+ console.error(e);
+ alert("Saving failed. See console.");
+ }
+ };
+
+ const onLoad = async () => {
+ try {
+ const proj = await loadWithPicker();
+ if (!proj) return;
+
+ if (proj.version !== 1 || !Array.isArray(proj.nodes) || !Array.isArray(proj.edges)) {
+ alert("Invalid project file format.");
+ return;
+ }
+
+ //We clear all the current edges and nodes
+ setEdges([]);
+ setNodes([]);
+
+ //set all loaded nodes and edges into the VisProg
+ const loadedNodes = proj.nodes as AppNode[];
+ const loadedEdges = proj.edges as Edge[];
+ setNodes(loadedNodes);
+ setEdges(loadedEdges);
+
+ } catch (e) {
+ console.error(e);
+ alert("Loading failed. See console.");
+ }
+ };
+
+ return (
+
+
You can save and load your graph here.
+
+
+
+
+
+ );
+}