'use client' import { useEffect, useState, useCallback } from 'react' import ReactFlow, { Node, Edge, Background, Controls, MiniMap, useNodesState, useEdgesState, ConnectionMode, } from 'reactflow' import 'reactflow/dist/style.css' interface Cluster { clusterId: number name?: string noteIds: string[] } interface BridgeNote { noteId: string bridgeScore: number clustersConnected: number[] note?: { id: string title: string | null content: string } } interface ClusterVisualizationProps { clusters: Cluster[] bridgeNotes: BridgeNote[] onNodeClick?: (nodeId: string, type: 'note' | 'cluster') => void } // Generate HSL colors for clusters function generateClusterColor(clusterId: number, totalClusters: number): string { const hue = (clusterId * 360 / totalClusters) % 360 return `hsl(${hue}, 70%, 60%)` } export function ClusterVisualization({ clusters, bridgeNotes, onNodeClick }: ClusterVisualizationProps) { const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [selectedCluster, setSelectedCluster] = useState(null) // Build graph from clusters and bridge notes useEffect(() => { if (clusters.length === 0) return const newNodes: Node[] = [] const newEdges: Edge[] = [] const noteToCluster = new Map() const clusterColor = generateClusterColor // Create cluster nodes and position notes around them const clusterCenterX = 200 const clusterCenterY = 200 const clusterSpacing = 400 clusters.forEach((cluster, clusterIndex) => { const cx = clusterCenterX + (clusterIndex % 4) * clusterSpacing const cy = clusterCenterY + Math.floor(clusterIndex / 4) * clusterSpacing const color = clusterColor(cluster.clusterId, clusters.length) // Add cluster label node newNodes.push({ id: `cluster-${cluster.clusterId}`, type: 'default', position: { x: cx, y: cy - 150 }, data: { label: (
{cluster.name || `Cluster ${cluster.clusterId}`} ({cluster.noteIds.length} notes)
) }, style: { background: 'transparent', border: 'none', }, className: 'cursor-pointer' }) // Add note nodes for this cluster const noteRadius = 120 cluster.noteIds.forEach((noteId, noteIndex) => { const angle = (noteIndex / cluster.noteIds.length) * 2 * Math.PI const nx = cx + Math.cos(angle) * noteRadius const ny = cy + Math.sin(angle) * noteRadius noteToCluster.set(noteId, cluster.clusterId) // Check if this is a bridge note const bridge = bridgeNotes.find(b => b.noteId === noteId) const isBridge = !!bridge newNodes.push({ id: noteId, type: 'default', position: { x: nx, y: ny }, data: { label: bridge?.note?.title || noteId.slice(0, 8) }, style: { background: isBridge ? '#FFD700' : color, border: isBridge ? '3px solid #B8860B' : '2px solid rgba(0,0,0,0.1)', borderRadius: '50%', width: isBridge ? 50 : 35, height: isBridge ? 50 : 35, fontSize: '10px', fontWeight: isBridge ? 'bold' : 'normal' }, className: `cursor-pointer transition-transform hover:scale-110 ${isBridge ? 'shadow-lg' : ''}` }) }) }) // Create edges between bridge notes and their connected clusters bridgeNotes.forEach(bridge => { const bridgeNodeId = bridge.noteId const bridgeNode = newNodes.find(n => n.id === bridgeNodeId) if (!bridgeNode) return bridge.clustersConnected.forEach(clusterId => { const clusterLabelId = `cluster-${clusterId}` if (clusterLabelId !== bridgeNodeId) { newEdges.push({ id: `${bridgeNodeId}-${clusterId}`, source: bridgeNodeId, target: clusterLabelId, type: 'smoothstep', animated: true, style: { stroke: '#FFD700', strokeWidth: 2 }, label: 'bridge' }) } }) }) setNodes(newNodes) setEdges(newEdges) }, [clusters, bridgeNotes, setNodes, setEdges]) const onNodeClickHandler = useCallback((event: React.MouseEvent, node: Node) => { const id = node.id if (id.startsWith('cluster-')) { const clusterId = parseInt(id.replace('cluster-', '')) setSelectedCluster(clusterId === selectedCluster ? null : clusterId) onNodeClick?.(id, 'cluster') } else { onNodeClick?.(id, 'note') } }, [selectedCluster, onNodeClick]) if (clusters.length === 0) { return (

No clusters to display

Create more notes to generate clusters

) } return (
{selectedCluster !== null && (

{clusters.find(c => c.clusterId === selectedCluster)?.name || `Cluster ${selectedCluster}`}

{clusters.find(c => c.clusterId === selectedCluster)?.noteIds.length || 0} notes

)}
Bridge note
Regular note
) }