From 0d31c5cf717d042075f625fdf8b407643c41deff Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 04:11:15 +0000 Subject: [PATCH] feat: Add state persistence, Clear All button, and color legend - Implement LocalStorage persistence for nodes and edges. - Add "Clear All" button with confirmation dialog. - Add a visual color legend for node types (Positive, Neutral, Negative, Root). - Improve AI node positioning with a structured horizontal branching layout. - Fix infinite recursion bug in AI node generation by adding a depth limit. - Ensure consistent nodeId initialization after page reloads. Co-authored-by: coderdevang <85845460+coderdevang@users.noreply.github.com> --- package-lock.json | 10 ------- src/App.css | 32 ++++++++++++++++++++ src/App.jsx | 75 +++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 94 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.css b/src/App.css index b9d355d..c36fa0a 100644 --- a/src/App.css +++ b/src/App.css @@ -40,3 +40,35 @@ .read-the-docs { color: #888; } + +/* 📍 Color Legend Styles */ +.color-legend { + position: absolute; + top: 20px; + left: 20px; + background: rgba(34, 34, 34, 0.9); + padding: 12px; + border-radius: 10px; + border: 1px solid #444; + color: white; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + font-weight: 500; +} + +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; +} diff --git a/src/App.jsx b/src/App.jsx index 5f3df15..f3568e4 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, @@ -8,14 +8,21 @@ import ReactFlow, { applyNodeChanges, } from 'reactflow'; import 'reactflow/dist/style.css'; +import './App.css'; import axios from 'axios'; import html2canvas from 'html2canvas'; import { saveAs } from 'file-saver'; // 🧠 Initial State & Helpers -const initialNodes = []; -const initialEdges = []; -let nodeId = 1; +const initialNodes = JSON.parse(localStorage.getItem('nodes')) || []; +const initialEdges = JSON.parse(localStorage.getItem('edges')) || []; + +const getMaxId = (nodes) => { + const ids = nodes.map((n) => parseInt(n.id)).filter((id) => !isNaN(id)); + return ids.length > 0 ? Math.max(...ids) : 0; +}; + +let nodeId = getMaxId(initialNodes) + 1; const getColor = (type) => { switch (type) { @@ -27,9 +34,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, setNodes, setEdges, stopGenerationRef, parentPos, 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', { @@ -69,8 +76,8 @@ Return only 3 outputs: one of each type.` id: newId, data: { label: line.replace(/^\d+\.\s*/, '') }, position: { - x: 300 + Math.random() * 300, - y: 200 + Math.random() * 300 + x: parentPos.x + 350, + y: parentPos.y + (idx - 1) * 150 }, style: { background: getColor(childTypes[idx]), @@ -94,9 +101,11 @@ Return only 3 outputs: one of each type.` 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, setNodes, setEdges, stopGenerationRef); + if (depth > 0) { + for (const n of newNodes) { + if (stopGenerationRef.current) break; + await createAIChildren(n.data.label, n.id, setNodes, setEdges, stopGenerationRef, n.position, depth - 1); + } } } catch (err) { @@ -112,6 +121,12 @@ export default function App() { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); const [prompt, setPrompt] = useState(''); + + // 💾 Persistence + useEffect(() => { + localStorage.setItem('nodes', JSON.stringify(nodes)); + localStorage.setItem('edges', JSON.stringify(edges)); + }, [nodes, edges]); const [loading, setLoading] = useState(false); const stopGenerationRef = useRef(false); const reactFlowWrapper = useRef(null); @@ -150,7 +165,7 @@ export default function App() { setNodes([rootNode]); setEdges([]); - await createAIChildren(prompt, rootId, setNodes, setEdges, stopGenerationRef); + await createAIChildren(prompt, rootId, setNodes, setEdges, stopGenerationRef, rootNode.position); setPrompt(''); setLoading(false); @@ -161,10 +176,21 @@ export default function App() { setLoading(false); }; + const handleClearAll = () => { + if (window.confirm('Are you sure you want to clear the entire mind map?')) { + setNodes([]); + setEdges([]); + localStorage.removeItem('nodes'); + localStorage.removeItem('edges'); + // Reset nodeId for fresh start + nodeId = 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, setNodes, setEdges, stopGenerationRef, node.position); setLoading(false); }; @@ -246,6 +272,22 @@ export default function App() { > Export as PNG + @@ -263,6 +305,13 @@ export default function App() { + {/* 📍 Color Legend */} +
+
Root
+
Positive
+
Neutral
+
Negative
+