From 26d50fffadbdf0cfed9fe92b1ae32f4216d62514 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 31 Jan 2026 03:53:09 +0000
Subject: [PATCH] feat: add persistence, clear all, and improve node
positioning
- Implement LocalStorage persistence for nodes and edges.
- Add 'Clear All' button with confirmation prompt.
- Refactor node positioning to use predictable branching instead of random coordinates.
- Fix infinite recursion bug by adding a depth limit to AI generation.
- Move nodeId management to useRef for better React state practices.
Co-authored-by: coderdevang <85845460+coderdevang@users.noreply.github.com>
---
package-lock.json | 10 -------
src/App.jsx | 75 +++++++++++++++++++++++++++++++++++++++--------
2 files changed, 62 insertions(+), 23 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b72de8c..df13162 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,7 +72,6 @@
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1738,7 +1737,6 @@
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -1780,7 +1778,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1902,7 +1899,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -2117,7 +2113,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=12"
}
@@ -2337,7 +2332,6 @@
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3180,7 +3174,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3248,7 +3241,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3258,7 +3250,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -3523,7 +3514,6 @@
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
diff --git a/src/App.jsx b/src/App.jsx
index 5f3df15..6d519be 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,5 @@
// 📦 Imports
-import React, { useState, useRef } from 'react';
+import React, { useState, useRef, useEffect } from 'react';
import ReactFlow, {
Background,
Controls,
@@ -15,7 +15,7 @@ import { saveAs } from 'file-saver';
// 🧠 Initial State & Helpers
const initialNodes = [];
const initialEdges = [];
-let nodeId = 1;
+// let nodeId = 1; // Moved into App component as useRef
const getColor = (type) => {
switch (type) {
@@ -27,9 +27,9 @@ const getColor = (type) => {
};
// 🤖 AI Logic with Domino Effect Thinking (Updated: Using Puter AI API)
-const createAIChildren = async (text, parentId, setNodes, setEdges, stopGenerationRef) => {
+const createAIChildren = async (text, parentId, parentPosition, setNodes, setEdges, stopGenerationRef, nodeIdRef, depth = 1) => {
try {
- if (stopGenerationRef.current) return;
+ if (stopGenerationRef.current || depth < 0) return;
const res = await axios.post(
'https://api.puter.com/v1/chat/completions',
{
@@ -64,13 +64,18 @@ Return only 3 outputs: one of each type.`
const childTypes = ['positive', 'neutral', 'negative'];
const newNodes = lines.map((line, idx) => {
- const newId = `${nodeId++}`;
+ const newId = `${nodeIdRef.current++}`;
+ const offsets = [
+ { x: 350, y: -150 }, // positive
+ { x: 350, y: 0 }, // neutral
+ { x: 350, y: 150 } // negative
+ ];
return {
id: newId,
data: { label: line.replace(/^\d+\.\s*/, '') },
position: {
- x: 300 + Math.random() * 300,
- y: 200 + Math.random() * 300
+ x: parentPosition.x + offsets[idx].x,
+ y: parentPosition.y + offsets[idx].y
},
style: {
background: getColor(childTypes[idx]),
@@ -96,7 +101,7 @@ Return only 3 outputs: one of each type.`
// Recursively expand each child one level deep
for (const n of newNodes) {
if (stopGenerationRef.current) break;
- await createAIChildren(n.data.label, n.id, setNodes, setEdges, stopGenerationRef);
+ await createAIChildren(n.data.label, n.id, n.position, setNodes, setEdges, stopGenerationRef, nodeIdRef, depth - 1);
}
} catch (err) {
@@ -109,13 +114,40 @@ Return only 3 outputs: one of each type.`
// 🔧 App Component
export default function App() {
- const [nodes, setNodes] = useState(initialNodes);
- const [edges, setEdges] = useState(initialEdges);
+ const [nodes, setNodes] = useState(() => {
+ const saved = localStorage.getItem('nodeverse-nodes');
+ return saved ? JSON.parse(saved) : initialNodes;
+ });
+ const [edges, setEdges] = useState(() => {
+ const saved = localStorage.getItem('nodeverse-edges');
+ return saved ? JSON.parse(saved) : initialEdges;
+ });
const [prompt, setPrompt] = useState('');
const [loading, setLoading] = useState(false);
const stopGenerationRef = useRef(false);
+
+ const nodeIdRef = useRef(
+ nodes.length > 0
+ ? Math.max(...nodes.map(n => parseInt(n.id) || 0)) + 1
+ : 1
+ );
+
+ useEffect(() => {
+ localStorage.setItem('nodeverse-nodes', JSON.stringify(nodes));
+ localStorage.setItem('nodeverse-edges', JSON.stringify(edges));
+ }, [nodes, edges]);
+
const reactFlowWrapper = useRef(null);
+ const handleClear = () => {
+ if (nodes.length === 0) return;
+ if (window.confirm('Are you sure you want to clear the entire mind map?')) {
+ setNodes([]);
+ setEdges([]);
+ nodeIdRef.current = 1;
+ }
+ };
+
const handleExport = () => {
if (reactFlowWrapper.current) {
html2canvas(reactFlowWrapper.current).then((canvas) => {
@@ -131,7 +163,7 @@ export default function App() {
setLoading(true);
stopGenerationRef.current = false;
- const rootId = `${nodeId++}`;
+ const rootId = `${nodeIdRef.current++}`;
const rootNode = {
id: rootId,
data: { label: prompt },
@@ -150,7 +182,7 @@ export default function App() {
setNodes([rootNode]);
setEdges([]);
- await createAIChildren(prompt, rootId, setNodes, setEdges, stopGenerationRef);
+ await createAIChildren(prompt, rootId, rootNode.position, setNodes, setEdges, stopGenerationRef, nodeIdRef);
setPrompt('');
setLoading(false);
@@ -162,9 +194,10 @@ export default function App() {
};
const onNodeClick = async (_event, node) => {
+ if (loading) return;
stopGenerationRef.current = false;
setLoading(true);
- await createAIChildren(node.data.label, node.id, setNodes, setEdges, stopGenerationRef);
+ await createAIChildren(node.data.label, node.id, node.position, setNodes, setEdges, stopGenerationRef, nodeIdRef);
setLoading(false);
};
@@ -230,6 +263,22 @@ export default function App() {
>
⏹ Stop
+