diff options
| author | Mahesh Sanikommu <[email protected]> | 2025-12-06 17:23:42 -0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-12-06 17:23:42 -0800 |
| commit | 41d24a4a1cc7a3c054fb5460d19cff7aac1d609d (patch) | |
| tree | c91d2837f43f99cc6979a169a7facfd6e148ce79 /packages/ui/memory-graph/hooks/use-graph-data.ts | |
| parent | feat(tools): allow passing apiKey via options for browser support (#599) (diff) | |
| download | supermemory-41d24a4a1cc7a3c054fb5460d19cff7aac1d609d.tar.xz supermemory-41d24a4a1cc7a3c054fb5460d19cff7aac1d609d.zip | |
fix ui issues and package issue (#610)
Diffstat (limited to 'packages/ui/memory-graph/hooks/use-graph-data.ts')
| -rw-r--r-- | packages/ui/memory-graph/hooks/use-graph-data.ts | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/packages/ui/memory-graph/hooks/use-graph-data.ts b/packages/ui/memory-graph/hooks/use-graph-data.ts new file mode 100644 index 00000000..3e9fa5cc --- /dev/null +++ b/packages/ui/memory-graph/hooks/use-graph-data.ts @@ -0,0 +1,304 @@ +"use client"; + +import { + calculateSemanticSimilarity, + getConnectionVisualProps, + getMagicalConnectionColor, +} from "@repo/lib/similarity"; +import { useMemo } from "react"; +import { colors, LAYOUT_CONSTANTS } from "../constants"; +import type { + DocumentsResponse, + DocumentWithMemories, + GraphEdge, + GraphNode, + MemoryEntry, + MemoryRelation, +} from "../types"; + +export function useGraphData( + data: DocumentsResponse | null, + selectedSpace: string, + nodePositions: Map<string, { x: number; y: number }>, + draggingNodeId: string | null, +) { + return useMemo(() => { + if (!data?.documents) return { nodes: [], edges: [] }; + + const allNodes: GraphNode[] = []; + const allEdges: GraphEdge[] = []; + + // Filter documents that have memories in selected space + const filteredDocuments = data.documents + .map((doc) => ({ + ...doc, + memoryEntries: + selectedSpace === "all" + ? doc.memoryEntries + : doc.memoryEntries.filter( + (memory) => + (memory.spaceContainerTag ?? memory.spaceId ?? "default") === + selectedSpace, + ), + })) + .filter((doc) => doc.memoryEntries.length > 0); + + // Group documents by space for better clustering + const documentsBySpace = new Map<string, typeof filteredDocuments>(); + filteredDocuments.forEach((doc) => { + const docSpace = + doc.memoryEntries[0]?.spaceContainerTag ?? + doc.memoryEntries[0]?.spaceId ?? + "default"; + if (!documentsBySpace.has(docSpace)) { + documentsBySpace.set(docSpace, []); + } + const spaceDocsArr = documentsBySpace.get(docSpace); + if (spaceDocsArr) { + spaceDocsArr.push(doc); + } + }); + + // Enhanced Layout with Space Separation + const { centerX, centerY, clusterRadius, spaceSpacing, documentSpacing } = + LAYOUT_CONSTANTS; + + /* 1. Build DOCUMENT nodes with space-aware clustering */ + const documentNodes: GraphNode[] = []; + let spaceIndex = 0; + + documentsBySpace.forEach((spaceDocs) => { + const spaceAngle = (spaceIndex / documentsBySpace.size) * Math.PI * 2; + const spaceOffsetX = Math.cos(spaceAngle) * spaceSpacing; + const spaceOffsetY = Math.sin(spaceAngle) * spaceSpacing; + const spaceCenterX = centerX + spaceOffsetX; + const spaceCenterY = centerY + spaceOffsetY; + + spaceDocs.forEach((doc, docIndex) => { + // Create proper circular layout with concentric rings + const docsPerRing = 6; // Start with 6 docs in inner ring + let currentRing = 0; + let docsInCurrentRing = docsPerRing; + let totalDocsInPreviousRings = 0; + + // Find which ring this document belongs to + while (totalDocsInPreviousRings + docsInCurrentRing <= docIndex) { + totalDocsInPreviousRings += docsInCurrentRing; + currentRing++; + docsInCurrentRing = docsPerRing + currentRing * 4; // Each ring has more docs + } + + // Position within the ring + const positionInRing = docIndex - totalDocsInPreviousRings; + const angleInRing = (positionInRing / docsInCurrentRing) * Math.PI * 2; + + // Radius increases significantly with each ring + const baseRadius = documentSpacing * 0.8; + const radius = + currentRing === 0 + ? baseRadius + : baseRadius + currentRing * documentSpacing * 1.2; + + const defaultX = spaceCenterX + Math.cos(angleInRing) * radius; + const defaultY = spaceCenterY + Math.sin(angleInRing) * radius; + + const customPos = nodePositions.get(doc.id); + + documentNodes.push({ + id: doc.id, + type: "document", + x: customPos?.x ?? defaultX, + y: customPos?.y ?? defaultY, + data: doc, + size: 58, + color: colors.document.primary, + isHovered: false, + isDragging: draggingNodeId === doc.id, + } satisfies GraphNode); + }); + + spaceIndex++; + }); + + /* 2. Gentle document collision avoidance with dampening */ + const minDocDist = LAYOUT_CONSTANTS.minDocDist; + + // Reduced iterations and gentler repulsion for smoother movement + for (let iter = 0; iter < 2; iter++) { + documentNodes.forEach((nodeA) => { + documentNodes.forEach((nodeB) => { + if (nodeA.id >= nodeB.id) return; + + // Only repel documents in the same space + const spaceA = + (nodeA.data as DocumentWithMemories).memoryEntries[0] + ?.spaceContainerTag ?? + (nodeA.data as DocumentWithMemories).memoryEntries[0]?.spaceId ?? + "default"; + const spaceB = + (nodeB.data as DocumentWithMemories).memoryEntries[0] + ?.spaceContainerTag ?? + (nodeB.data as DocumentWithMemories).memoryEntries[0]?.spaceId ?? + "default"; + + if (spaceA !== spaceB) return; + + const dx = nodeB.x - nodeA.x; + const dy = nodeB.y - nodeA.y; + const dist = Math.sqrt(dx * dx + dy * dy) || 1; + + if (dist < minDocDist) { + // Much gentler push with dampening + const push = (minDocDist - dist) / 8; + const dampening = Math.max(0.1, Math.min(1, dist / minDocDist)); + const smoothPush = push * dampening * 0.5; + + const nx = dx / dist; + const ny = dy / dist; + nodeA.x -= nx * smoothPush; + nodeA.y -= ny * smoothPush; + nodeB.x += nx * smoothPush; + nodeB.y += ny * smoothPush; + } + }); + }); + } + + allNodes.push(...documentNodes); + + /* 3. Add memories around documents WITH doc-memory connections */ + documentNodes.forEach((docNode) => { + const memoryNodeMap = new Map<string, GraphNode>(); + const doc = docNode.data as DocumentWithMemories; + + doc.memoryEntries.forEach((memory, memIndex) => { + const memoryId = `${memory.id}`; + const customMemPos = nodePositions.get(memoryId); + + const clusterAngle = + (memIndex / doc.memoryEntries.length) * Math.PI * 2; + const variation = Math.sin(memIndex * 2.5) * 0.3 + 0.7; + const distance = clusterRadius * variation; + + const seed = + memIndex * 12345 + Number.parseInt(docNode.id.slice(0, 6), 36); + const offsetX = Math.sin(seed) * 0.5 * 40; + const offsetY = Math.cos(seed) * 0.5 * 40; + + const defaultMemX = + docNode.x + Math.cos(clusterAngle) * distance + offsetX; + const defaultMemY = + docNode.y + Math.sin(clusterAngle) * distance + offsetY; + + if (!memoryNodeMap.has(memoryId)) { + const memoryNode: GraphNode = { + id: memoryId, + type: "memory", + x: customMemPos?.x ?? defaultMemX, + y: customMemPos?.y ?? defaultMemY, + data: memory, + size: Math.max( + 32, + Math.min(48, (memory.memory?.length || 50) * 0.5), + ), + color: colors.memory.primary, + isHovered: false, + isDragging: draggingNodeId === memoryId, + }; + memoryNodeMap.set(memoryId, memoryNode); + allNodes.push(memoryNode); + } + + // Create doc-memory edge with similarity + allEdges.push({ + id: `edge-${docNode.id}-${memory.id}`, + source: docNode.id, + target: memoryId, + similarity: 1, + visualProps: getConnectionVisualProps(1), + color: colors.connection.memory, + edgeType: "doc-memory", + }); + }); + }); + + // Build mapping of memoryId -> nodeId for version chains + const memNodeIdMap = new Map<string, string>(); + allNodes.forEach((n) => { + if (n.type === "memory") { + memNodeIdMap.set((n.data as MemoryEntry).id, n.id); + } + }); + + // Add version-chain edges (old -> new) + data.documents.forEach((doc) => { + doc.memoryEntries.forEach((mem: MemoryEntry) => { + // Support both new object structure and legacy array/single parent fields + let parentRelations: Record<string, MemoryRelation> = {}; + + if ( + mem.memoryRelations && + typeof mem.memoryRelations === "object" && + Object.keys(mem.memoryRelations).length > 0 + ) { + parentRelations = mem.memoryRelations; + } else if (mem.parentMemoryId) { + parentRelations = { + [mem.parentMemoryId]: "updates" as MemoryRelation, + }; + } + Object.entries(parentRelations).forEach(([pid, relationType]) => { + const fromId = memNodeIdMap.get(pid); + const toId = memNodeIdMap.get(mem.id); + if (fromId && toId) { + allEdges.push({ + id: `version-${fromId}-${toId}`, + source: fromId, + target: toId, + similarity: 1, + visualProps: { + opacity: 0.8, + thickness: 1, + glow: 0, + pulseDuration: 3000, + }, + // choose color based on relation type + color: colors.relations[relationType] ?? colors.relations.updates, + edgeType: "version", + relationType: relationType as MemoryRelation, + }); + } + }); + }); + }); + + // Document-to-document similarity edges + for (let i = 0; i < filteredDocuments.length; i++) { + const docI = filteredDocuments[i]; + if (!docI) continue; + + for (let j = i + 1; j < filteredDocuments.length; j++) { + const docJ = filteredDocuments[j]; + if (!docJ) continue; + + const sim = calculateSemanticSimilarity( + docI.summaryEmbedding ? Array.from(docI.summaryEmbedding) : null, + docJ.summaryEmbedding ? Array.from(docJ.summaryEmbedding) : null, + ); + if (sim > 0.725) { + allEdges.push({ + id: `doc-doc-${docI.id}-${docJ.id}`, + source: docI.id, + target: docJ.id, + similarity: sim, + visualProps: getConnectionVisualProps(sim), + color: getMagicalConnectionColor(sim, 200), + edgeType: "doc-doc", + }); + } + } + } + + return { nodes: allNodes, edges: allEdges }; + }, [data, selectedSpace, nodePositions, draggingNodeId]); +} |