"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactFlow, { Background, Controls, MiniMap, type ReactFlowInstance } from "reactflow"; import "reactflow/dist/style.css"; import MindmapNodeCard from "@/components/MindmapNodeCard"; import { treeToGraph } from "@/lib/treeToGraph"; import type { MindmapNode } from "@/types/mindmap"; type MindmapCanvasProps = { tree: MindmapNode; className?: string; onNodeChat?: (nodeLabel: string) => void; }; const NODE_WIDTH = 220; const NODE_HEIGHT = 84; const nodeTypes = { mindmapNode: MindmapNodeCard, }; function createInitialExpandedSet(tree: MindmapNode): Set { return new Set([tree.id]); } export default function MindmapCanvas({ tree, className, onNodeChat }: MindmapCanvasProps) { const [expandedNodeIds, setExpandedNodeIds] = useState>(() => createInitialExpandedSet(tree), ); const [reactFlowInstance, setReactFlowInstance] = useState(null); const hasFittedInitiallyRef = useRef(false); useEffect(() => { setExpandedNodeIds(createInitialExpandedSet(tree)); hasFittedInitiallyRef.current = false; }, [tree]); const panToNode = useCallback( (nodeId: string, nextExpandedNodeIds: Set) => { if (!reactFlowInstance) { return; } const nextGraph = treeToGraph(tree, { expandedNodeIds: nextExpandedNodeIds, }); const focusNode = nextGraph.nodes.find((node) => node.id === nodeId); if (!focusNode) { return; } const viewport = reactFlowInstance.getViewport(); const revealOffset = nextExpandedNodeIds.has(nodeId) ? NODE_WIDTH * 0.9 : NODE_WIDTH * 0.45; window.requestAnimationFrame(() => { reactFlowInstance.setCenter( focusNode.position.x + revealOffset, focusNode.position.y + NODE_HEIGHT / 2, { zoom: viewport.zoom, duration: 320, }, ); }); }, [reactFlowInstance, tree], ); const handleToggle = useCallback( (nodeId: string) => { setExpandedNodeIds((currentExpandedNodeIds) => { const nextExpandedNodeIds = new Set(currentExpandedNodeIds); if (nextExpandedNodeIds.has(nodeId)) { nextExpandedNodeIds.delete(nodeId); } else { nextExpandedNodeIds.add(nodeId); } panToNode(nodeId, nextExpandedNodeIds); return nextExpandedNodeIds; }); }, [panToNode], ); const graph = useMemo( () => treeToGraph(tree, { expandedNodeIds, onToggleNode: handleToggle, onOpenNodeChat: onNodeChat, }), [expandedNodeIds, handleToggle, onNodeChat, tree], ); const keepNodeInteractive = useCallback(() => { // React Flow only enables pointer events for nodes when at least one node handler exists. }, []); useEffect(() => { if (!reactFlowInstance || hasFittedInitiallyRef.current || graph.nodes.length === 0) { return; } const frame = window.requestAnimationFrame(() => { reactFlowInstance.fitView({ padding: 0.16, duration: 420, maxZoom: 1, }); hasFittedInitiallyRef.current = true; }); return () => window.cancelAnimationFrame(frame); }, [graph.nodes, reactFlowInstance]); return (
); }