diff options
Diffstat (limited to 'packages/memory-graph/src/hooks/use-graph-data.ts')
| -rw-r--r-- | packages/memory-graph/src/hooks/use-graph-data.ts | 231 |
1 files changed, 122 insertions, 109 deletions
diff --git a/packages/memory-graph/src/hooks/use-graph-data.ts b/packages/memory-graph/src/hooks/use-graph-data.ts index 030eea61..526277bb 100644 --- a/packages/memory-graph/src/hooks/use-graph-data.ts +++ b/packages/memory-graph/src/hooks/use-graph-data.ts @@ -1,12 +1,12 @@ -"use client"; +"use client" import { calculateSemanticSimilarity, getConnectionVisualProps, getMagicalConnectionColor, -} from "@/lib/similarity"; -import { useMemo } from "react"; -import { colors, LAYOUT_CONSTANTS } from "@/constants"; +} from "@/lib/similarity" +import { useMemo } from "react" +import { colors, LAYOUT_CONSTANTS } from "@/constants" import type { DocumentsResponse, DocumentWithMemories, @@ -14,95 +14,106 @@ import type { GraphNode, MemoryEntry, MemoryRelation, -} from "@/types"; +} from "@/types" export function useGraphData( data: DocumentsResponse | null, selectedSpace: string, nodePositions: Map<string, { x: number; y: number }>, draggingNodeId: string | null, + memoryLimit?: number, ) { return useMemo(() => { - if (!data?.documents) return { nodes: [], edges: [] }; + if (!data?.documents) return { nodes: [], edges: [] } - const allNodes: GraphNode[] = []; - const allEdges: GraphEdge[] = []; + const allNodes: GraphNode[] = [] + const allEdges: GraphEdge[] = [] // Filter documents that have memories in selected space + // AND limit memories per document when memoryLimit is provided const filteredDocuments = data.documents - .map((doc) => ({ - ...doc, - memoryEntries: + .map((doc) => { + let memories = selectedSpace === "all" ? doc.memoryEntries : doc.memoryEntries.filter( (memory) => (memory.spaceContainerTag ?? memory.spaceId ?? "default") === selectedSpace, - ), - })) - .filter((doc) => doc.memoryEntries.length > 0); + ) + + // Apply memory limit if provided and a specific space is selected + if (selectedSpace !== "all" && memoryLimit && memoryLimit > 0) { + memories = memories.slice(0, memoryLimit) + } + + return { + ...doc, + memoryEntries: memories, + } + }) + .filter((doc) => doc.memoryEntries.length > 0) // Group documents by space for better clustering - const documentsBySpace = new Map<string, typeof filteredDocuments>(); + const documentsBySpace = new Map<string, typeof filteredDocuments>() filteredDocuments.forEach((doc) => { const docSpace = doc.memoryEntries[0]?.spaceContainerTag ?? doc.memoryEntries[0]?.spaceId ?? - "default"; + "default" if (!documentsBySpace.has(docSpace)) { - documentsBySpace.set(docSpace, []); + documentsBySpace.set(docSpace, []) } - const spaceDocsArr = documentsBySpace.get(docSpace); + const spaceDocsArr = documentsBySpace.get(docSpace) if (spaceDocsArr) { - spaceDocsArr.push(doc); + spaceDocsArr.push(doc) } - }); + }) // Enhanced Layout with Space Separation const { centerX, centerY, clusterRadius, spaceSpacing, documentSpacing } = - LAYOUT_CONSTANTS; + LAYOUT_CONSTANTS /* 1. Build DOCUMENT nodes with space-aware clustering */ - const documentNodes: GraphNode[] = []; - let spaceIndex = 0; + 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; + 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; + 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 + 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; + const positionInRing = docIndex - totalDocsInPreviousRings + const angleInRing = (positionInRing / docsInCurrentRing) * Math.PI * 2 // Radius increases significantly with each ring - const baseRadius = documentSpacing * 0.8; + const baseRadius = documentSpacing * 0.8 const radius = currentRing === 0 ? baseRadius - : baseRadius + currentRing * documentSpacing * 1.2; + : baseRadius + currentRing * documentSpacing * 1.2 - const defaultX = spaceCenterX + Math.cos(angleInRing) * radius; - const defaultY = spaceCenterY + Math.sin(angleInRing) * radius; + const defaultX = spaceCenterX + Math.cos(angleInRing) * radius + const defaultY = spaceCenterY + Math.sin(angleInRing) * radius - const customPos = nodePositions.get(doc.id); + const customPos = nodePositions.get(doc.id) documentNodes.push({ id: doc.id, @@ -114,81 +125,80 @@ export function useGraphData( color: colors.document.primary, isHovered: false, isDragging: draggingNodeId === doc.id, - } satisfies GraphNode); - }); + } satisfies GraphNode) + }) - spaceIndex++; - }); + spaceIndex++ + }) /* 2. Gentle document collision avoidance with dampening */ - const minDocDist = LAYOUT_CONSTANTS.minDocDist; + 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; + 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"; + "default" const spaceB = (nodeB.data as DocumentWithMemories).memoryEntries[0] ?.spaceContainerTag ?? (nodeB.data as DocumentWithMemories).memoryEntries[0]?.spaceId ?? - "default"; + "default" - if (spaceA !== spaceB) return; + 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; + 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; + 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); + 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; + 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 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 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; + 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; + docNode.x + Math.cos(clusterAngle) * distance + offsetX const defaultMemY = - docNode.y + Math.sin(clusterAngle) * distance + offsetY; + docNode.y + Math.sin(clusterAngle) * distance + offsetY if (!memoryNodeMap.has(memoryId)) { const memoryNode: GraphNode = { @@ -204,9 +214,9 @@ export function useGraphData( color: colors.memory.primary, isHovered: false, isDragging: draggingNodeId === memoryId, - }; - memoryNodeMap.set(memoryId, memoryNode); - allNodes.push(memoryNode); + } + memoryNodeMap.set(memoryId, memoryNode) + allNodes.push(memoryNode) } // Create doc-memory edge with similarity @@ -218,23 +228,23 @@ export function useGraphData( visualProps: getConnectionVisualProps(1), color: colors.connection.memory, edgeType: "doc-memory", - }); - }); - }); + }) + }) + }) // Build mapping of memoryId -> nodeId for version chains - const memNodeIdMap = new Map<string, string>(); + const memNodeIdMap = new Map<string, string>() allNodes.forEach((n) => { if (n.type === "memory") { - memNodeIdMap.set((n.data as MemoryEntry).id, n.id); + 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> = {}; + let parentRelations: Record<string, MemoryRelation> = {} if ( mem.memoryRelations && @@ -242,18 +252,21 @@ export function useGraphData( mem.memoryRelations.length > 0 ) { // Convert array to Record - parentRelations = mem.memoryRelations.reduce((acc, rel) => { - acc[rel.targetMemoryId] = rel.relationType; - return acc; - }, {} as Record<string, MemoryRelation>); + parentRelations = mem.memoryRelations.reduce( + (acc, rel) => { + acc[rel.targetMemoryId] = rel.relationType + return acc + }, + {} as Record<string, MemoryRelation>, + ) } 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); + const fromId = memNodeIdMap.get(pid) + const toId = memNodeIdMap.get(mem.id) if (fromId && toId) { allEdges.push({ id: `version-${fromId}-${toId}`, @@ -270,25 +283,25 @@ export function useGraphData( 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; + const docI = filteredDocuments[i] + if (!docI) continue for (let j = i + 1; j < filteredDocuments.length; j++) { - const docJ = filteredDocuments[j]; - if (!docJ) continue; + 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}`, @@ -298,11 +311,11 @@ export function useGraphData( visualProps: getConnectionVisualProps(sim), color: getMagicalConnectionColor(sim, 200), edgeType: "doc-doc", - }); + }) } } } - return { nodes: allNodes, edges: allEdges }; - }, [data, selectedSpace, nodePositions, draggingNodeId]); + return { nodes: allNodes, edges: allEdges } + }, [data, selectedSpace, nodePositions, draggingNodeId, memoryLimit]) } |