From 5a895490863657afacffb0c9fbd5cf397447c0b4 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 3 Feb 2026 03:53:59 +0000
Subject: [PATCH] feat: add localstorage persistence and clear all
functionality
- Implement localstorage persistence for nodes and edges.
- Add node ID recovery logic to maintain consistent IDs across reloads.
- Add 'Clear All' button with confirmation dialog.
- Refactor createAIChildren with useCallback for optimization.
- Add __pycache__/ to .gitignore.
Co-authored-by: coderdevang <85845460+coderdevang@users.noreply.github.com>
---
.gitignore | 2 +
package-lock.json | 10 ---
src/App.jsx | 199 ++++++++++++++++++++++++++++------------------
3 files changed, 125 insertions(+), 86 deletions(-)
diff --git a/.gitignore b/.gitignore
index a547bf3..c763289 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ dist
dist-ssr
*.local
+__pycache__/
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
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..eeeb3ed 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, useCallback } from 'react';
import ReactFlow, {
Background,
Controls,
@@ -15,7 +15,6 @@ import { saveAs } from 'file-saver';
// 🧠 Initial State & Helpers
const initialNodes = [];
const initialEdges = [];
-let nodeId = 1;
const getColor = (type) => {
switch (type) {
@@ -26,18 +25,50 @@ const getColor = (type) => {
}
};
-// 🤖 AI Logic with Domino Effect Thinking (Updated: Using Puter AI API)
-const createAIChildren = async (text, parentId, setNodes, setEdges, stopGenerationRef) => {
- try {
- if (stopGenerationRef.current) return;
- const res = await axios.post(
- 'https://api.puter.com/v1/chat/completions',
- {
- model: 'gpt-3.5-turbo',
- messages: [
- {
- role: 'system',
- content: `You are an expert in cause-and-effect analysis.
+// 🔧 App Component
+export default function App() {
+ const [nodes, setNodes] = useState(() => {
+ const saved = localStorage.getItem('mindmap-nodes');
+ return saved ? JSON.parse(saved) : initialNodes;
+ });
+ const [edges, setEdges] = useState(() => {
+ const saved = localStorage.getItem('mindmap-edges');
+ return saved ? JSON.parse(saved) : initialEdges;
+ });
+ const [prompt, setPrompt] = useState('');
+ const [loading, setLoading] = useState(false);
+ const stopGenerationRef = useRef(false);
+ const reactFlowWrapper = useRef(null);
+ const nodeIdRef = useRef(1);
+
+ // 💾 Persistence: Save to LocalStorage
+ useEffect(() => {
+ localStorage.setItem('mindmap-nodes', JSON.stringify(nodes));
+ localStorage.setItem('mindmap-edges', JSON.stringify(edges));
+ }, [nodes, edges]);
+
+ // 🆔 Node ID Recovery
+ useEffect(() => {
+ if (nodes.length > 0) {
+ const ids = nodes.map(n => parseInt(n.id)).filter(id => !isNaN(id));
+ if (ids.length > 0) {
+ nodeIdRef.current = Math.max(...ids) + 1;
+ }
+ }
+ }, []);
+
+ // 🤖 AI Logic with Domino Effect Thinking (Updated: Using Puter AI API)
+ const createAIChildren = useCallback(async (text, parentId) => {
+ try {
+ if (stopGenerationRef.current) return;
+ const res = await axios.post(
+ 'https://api.puter.com/v1/chat/completions',
+ {
+ model: 'gpt-3.5-turbo',
+ messages: [
+ {
+ role: 'system',
+ content: `You are an expert in cause-and-effect analysis.
Given a news headline or event, break it down into a chain of consequences, like a domino effect.
@@ -48,73 +79,63 @@ Each node must:
- Be logically linked to the prompt
Return only 3 outputs: one of each type.`
- },
- { role: 'user', content: text }
- ]
- },
- {
- headers: {
- 'Content-Type': 'application/json'
- }
- }
- );
-
- const content = res.data.choices?.[0]?.message?.content || '';
- const lines = content.split('\n').filter(Boolean).slice(0, 3);
- const childTypes = ['positive', 'neutral', 'negative'];
-
- const newNodes = lines.map((line, idx) => {
- const newId = `${nodeId++}`;
- return {
- id: newId,
- data: { label: line.replace(/^\d+\.\s*/, '') },
- position: {
- x: 300 + Math.random() * 300,
- y: 200 + Math.random() * 300
+ },
+ { role: 'user', content: text }
+ ]
},
- style: {
- background: getColor(childTypes[idx]),
- color: '#fff',
- borderRadius: 12,
- padding: 10,
- fontWeight: 'bold',
- fontSize: 14,
- maxWidth: 300
+ {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
}
- };
- });
+ );
- const newEdges = newNodes.map((n) => ({
- id: `${parentId}-${n.id}`,
- source: parentId,
- target: n.id
- }));
+ const content = res.data.choices?.[0]?.message?.content || '';
+ const lines = content.split('\n').filter(Boolean).slice(0, 3);
+ const childTypes = ['positive', 'neutral', 'negative'];
- setNodes((nds) => [...nds, ...newNodes]);
- setEdges((eds) => [...eds, ...newEdges]);
+ const newNodes = lines.map((line, idx) => {
+ const newId = `${nodeIdRef.current++}`;
+ return {
+ id: newId,
+ data: { label: line.replace(/^\d+\.\s*/, '') },
+ position: {
+ x: 300 + Math.random() * 300,
+ y: 200 + Math.random() * 300
+ },
+ style: {
+ background: getColor(childTypes[idx]),
+ color: '#fff',
+ borderRadius: 12,
+ padding: 10,
+ fontWeight: 'bold',
+ fontSize: 14,
+ maxWidth: 300
+ }
+ };
+ });
- // 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);
- }
+ const newEdges = newNodes.map((n) => ({
+ id: `${parentId}-${n.id}`,
+ source: parentId,
+ target: n.id
+ }));
- } catch (err) {
- const errMsg = err.response?.data?.error?.message || err.message;
- alert(`❌ AI Error: ${errMsg}`);
- console.error('❌ Full error:', err.response?.data || err);
- }
-};
+ setNodes((nds) => [...nds, ...newNodes]);
+ setEdges((eds) => [...eds, ...newEdges]);
+ // Recursively expand each child one level deep
+ for (const n of newNodes) {
+ if (stopGenerationRef.current) break;
+ await createAIChildren(n.data.label, n.id);
+ }
-// 🔧 App Component
-export default function App() {
- const [nodes, setNodes] = useState(initialNodes);
- const [edges, setEdges] = useState(initialEdges);
- const [prompt, setPrompt] = useState('');
- const [loading, setLoading] = useState(false);
- const stopGenerationRef = useRef(false);
- const reactFlowWrapper = useRef(null);
+ } catch (err) {
+ const errMsg = err.response?.data?.error?.message || err.message;
+ alert(`❌ AI Error: ${errMsg}`);
+ console.error('❌ Full error:', err.response?.data || err);
+ }
+ }, []);
const handleExport = () => {
if (reactFlowWrapper.current) {
@@ -131,7 +152,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 +171,7 @@ export default function App() {
setNodes([rootNode]);
setEdges([]);
- await createAIChildren(prompt, rootId, setNodes, setEdges, stopGenerationRef);
+ await createAIChildren(prompt, rootId);
setPrompt('');
setLoading(false);
@@ -161,10 +182,20 @@ export default function App() {
setLoading(false);
};
+ const handleClear = () => {
+ if (window.confirm('Are you sure you want to clear the entire mind map?')) {
+ setNodes([]);
+ setEdges([]);
+ localStorage.removeItem('mindmap-nodes');
+ localStorage.removeItem('mindmap-edges');
+ nodeIdRef.current = 1;
+ }
+ };
+
const onNodeClick = async (_event, node) => {
stopGenerationRef.current = false;
setLoading(true);
- await createAIChildren(node.data.label, node.id, setNodes, setEdges, stopGenerationRef);
+ await createAIChildren(node.data.label, node.id);
setLoading(false);
};
@@ -230,6 +261,22 @@ export default function App() {
>
⏹ Stop
+