From 223aee1aa173c88cf2228e93407ee2fb1f025cb4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:57:50 +0000 Subject: [PATCH] feat: add persistence, clear all, and color legend - Implemented localStorage persistence for nodes and edges. - Refactored nodeId management to use a useRef and resume from max existing ID. - Added a "Clear All" button with confirmation dialog. - Added a visual color legend overlay to explain node types. - Refactored createAIChildren into the App component scope. Co-authored-by: coderdevang <85845460+coderdevang@users.noreply.github.com> --- src/App.jsx | 235 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 159 insertions(+), 76 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 5f3df15..4e5eddf 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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('nodes'); + return saved ? JSON.parse(saved) : initialNodes; + }); + const [edges, setEdges] = useState(() => { + const saved = localStorage.getItem('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 & Initialization + React.useEffect(() => { + if (nodes.length > 0) { + const maxId = Math.max(...nodes.map((n) => parseInt(n.id) || 0)); + nodeIdRef.current = maxId + 1; + } + }, []); + + React.useEffect(() => { + localStorage.setItem('nodes', JSON.stringify(nodes)); + }, [nodes]); + + React.useEffect(() => { + localStorage.setItem('edges', JSON.stringify(edges)); + }, [edges]); + + // 🤖 AI Logic with Domino Effect Thinking (Updated: Using Puter AI API) + const createAIChildren = 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); @@ -164,10 +185,20 @@ export default function App() { 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); }; + const handleClearAll = () => { + if (window.confirm('Are you sure you want to clear the entire mind map?')) { + setNodes([]); + setEdges([]); + localStorage.removeItem('nodes'); + localStorage.removeItem('edges'); + nodeIdRef.current = 1; + } + }; + return (
{/* 🔝 Header */} @@ -246,11 +277,63 @@ export default function App() { > Export as PNG +
{/* 🧠 ReactFlow Canvas */} -
+
+ {/* 🎨 Color Legend Overlay */} +
+
Legend
+
+
+ Root Node +
+
+
+ Positive Effect +
+
+
+ Neutral Effect +
+
+
+ Negative Effect +
+
+