aboutsummaryrefslogtreecommitdiff
path: root/packages/memory-graph/src
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2026-01-21 03:11:53 +0000
committerMaheshtheDev <[email protected]>2026-01-21 03:11:53 +0000
commit1423bd70041c8dc0d863c13f1377865c6c875181 (patch)
tree29155642e31153d6873640aa5e4d8af5c45e213e /packages/memory-graph/src
parentfeat: create space, delete spaces and emoji picker (#687) (diff)
downloadsupermemory-1423bd70041c8dc0d863c13f1377865c6c875181.tar.xz
supermemory-1423bd70041c8dc0d863c13f1377865c6c875181.zip
feat: mobile responsive, lint formats, toast, render issue fix (#688)01-20-feat_mobile_responsive_lint_formats_ui_improvements_render_issue_fix
- Mobile responsive - new toast design - web document render issue fix - posthog analytics - ui improvements
Diffstat (limited to 'packages/memory-graph/src')
-rw-r--r--packages/memory-graph/src/components/graph-canvas.tsx78
-rw-r--r--packages/memory-graph/src/components/memory-graph.tsx35
-rw-r--r--packages/memory-graph/src/components/navigation-controls.css.ts2
-rw-r--r--packages/memory-graph/src/components/node-detail-panel.tsx4
-rw-r--r--packages/memory-graph/src/components/node-popover.tsx201
-rw-r--r--packages/memory-graph/src/hooks/use-force-simulation.ts5
-rw-r--r--packages/memory-graph/src/hooks/use-graph-data.ts126
-rw-r--r--packages/memory-graph/src/hooks/use-graph-interactions.ts20
8 files changed, 312 insertions, 159 deletions
diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx
index 28dd53c7..f4cede9f 100644
--- a/packages/memory-graph/src/components/graph-canvas.tsx
+++ b/packages/memory-graph/src/components/graph-canvas.tsx
@@ -84,11 +84,11 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const progress = Math.min(elapsed / duration, 1)
// Ease-out cubic easing for smooth deceleration
- const eased = 1 - Math.pow(1 - progress, 3)
+ const eased = 1 - (1 - progress) ** 3
dimProgress.current = startDim + (targetDim - startDim) * eased
// Force re-render to update canvas during animation
- forceRender(prev => prev + 1)
+ forceRender((prev) => prev + 1)
if (progress < 1) {
dimAnimationRef.current = requestAnimationFrame(animate)
@@ -145,8 +145,10 @@ export const GraphCanvas = memo<GraphCanvasProps>(
// Only check nodes in the clicked cell (and neighboring cells for edge cases)
const cellsToCheck = [
cellKey,
- `${cellX-1},${cellY}`, `${cellX+1},${cellY}`,
- `${cellX},${cellY-1}`, `${cellX},${cellY+1}`,
+ `${cellX - 1},${cellY}`,
+ `${cellX + 1},${cellY}`,
+ `${cellX},${cellY - 1}`,
+ `${cellX},${cellY + 1}`,
]
// Check from top-most to bottom-most: memory nodes are drawn after documents
@@ -360,7 +362,12 @@ export const GraphCanvas = memo<GraphCanvasProps>(
})
// Helper function to draw a single edge path
- const drawEdgePath = (edge: typeof edges[0], sourceNode: GraphNode, targetNode: GraphNode, edgeShouldDim: boolean) => {
+ const drawEdgePath = (
+ edge: (typeof edges)[0],
+ sourceNode: GraphNode,
+ targetNode: GraphNode,
+ edgeShouldDim: boolean,
+ ) => {
const sourceX = sourceNode.x * zoom + panX
const sourceY = sourceNode.y * zoom + panY
const targetX = targetNode.x * zoom + panX
@@ -381,9 +388,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const dy = targetY - sourceY
const distance = Math.sqrt(dx * dx + dy * dy)
const controlOffset =
- edge.edgeType === "doc-memory"
- ? 15
- : Math.min(30, distance * 0.2)
+ edge.edgeType === "doc-memory" ? 15 : Math.min(30, distance * 0.2)
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
@@ -398,7 +403,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}
// Smooth edge opacity: interpolate between full and 0.05 (dimmed)
- const edgeDimOpacity = 1 - (dimProgress.current * 0.95)
+ const edgeDimOpacity = 1 - dimProgress.current * 0.95
// BATCH 1: Draw all doc-memory edges together
if (docMemoryEdges.length > 0) {
@@ -417,7 +422,8 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
- const edgeShouldDim = selectedNodeId !== null &&
+ const edgeShouldDim =
+ selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.9
@@ -444,10 +450,13 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
- const edgeShouldDim = selectedNodeId !== null &&
+ const edgeShouldDim =
+ selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
- const opacity = edgeShouldDim ? edgeDimOpacity : Math.max(0, edge.similarity * 0.5)
+ const opacity = edgeShouldDim
+ ? edgeDimOpacity
+ : Math.max(0, edge.similarity * 0.5)
const lineWidth = Math.max(1, edge.similarity * 2)
// Set color based on similarity strength
@@ -480,7 +489,8 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
- const edgeShouldDim = selectedNodeId !== null &&
+ const edgeShouldDim =
+ selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.8
@@ -568,7 +578,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const isSelected = selectedNodeId === node.id
const shouldDim = selectedNodeId !== null && !isSelected
// Smooth opacity: interpolate between 1 (full) and 0.1 (dimmed) based on animation progress
- const nodeOpacity = shouldDim ? 1 - (dimProgress.current * 0.9) : 1
+ const nodeOpacity = shouldDim ? 1 - dimProgress.current * 0.9 : 1
const isHighlightedDocument = (() => {
if (node.type !== "document" || highlightSet.size === 0) return false
const doc = node.data as DocumentWithMemories
@@ -710,7 +720,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const radius = nodeSize / 2
ctx.fillStyle = fillColor
- ctx.globalAlpha = shouldDim ? nodeOpacity : (isLatest ? 1 : 0.4)
+ ctx.globalAlpha = shouldDim ? nodeOpacity : isLatest ? 1 : 0.4
ctx.strokeStyle = borderColor
ctx.lineWidth = isDragging ? 3 : isHovered ? 2 : 1.5
@@ -833,7 +843,17 @@ export const GraphCanvas = memo<GraphCanvasProps>(
})
ctx.globalAlpha = 1
- }, [nodes, edges, panX, panY, zoom, width, height, highlightDocumentIds, nodeMap])
+ }, [
+ nodes,
+ edges,
+ panX,
+ panY,
+ zoom,
+ width,
+ height,
+ highlightDocumentIds,
+ nodeMap,
+ ])
// Hybrid rendering: continuous when simulation active, change-based when idle
const lastRenderParams = useRef<number>(0)
@@ -857,9 +877,16 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}, 0)
// Combine all factors into a single number
- return positionHash ^ edges.length ^
- Math.round(panX) ^ Math.round(panY) ^
- Math.round(zoom * 100) ^ width ^ height ^ highlightHash
+ return (
+ positionHash ^
+ edges.length ^
+ Math.round(panX) ^
+ Math.round(panY) ^
+ Math.round(zoom * 100) ^
+ width ^
+ height ^
+ highlightHash
+ )
}, [
nodes,
edges.length,
@@ -962,13 +989,10 @@ export const GraphCanvas = memo<GraphCanvasProps>(
// Calculate effective DPR that keeps us within safe limits
// Prevent division by zero by checking for valid dimensions
- const maxDpr = width > 0 && height > 0
- ? Math.min(
- MAX_CANVAS_SIZE / width,
- MAX_CANVAS_SIZE / height,
- dpr
- )
- : dpr
+ const maxDpr =
+ width > 0 && height > 0
+ ? Math.min(MAX_CANVAS_SIZE / width, MAX_CANVAS_SIZE / height, dpr)
+ : dpr
// upscale backing store with clamped dimensions
canvas.style.width = `${width}px`
@@ -1026,4 +1050,4 @@ export const GraphCanvas = memo<GraphCanvasProps>(
},
)
-GraphCanvas.displayName = "GraphCanvas" \ No newline at end of file
+GraphCanvas.displayName = "GraphCanvas"
diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx
index b8dd493d..3368bc4d 100644
--- a/packages/memory-graph/src/components/memory-graph.tsx
+++ b/packages/memory-graph/src/components/memory-graph.tsx
@@ -2,7 +2,14 @@
import { GlassMenuEffect } from "@/ui/glass-effect"
import { AnimatePresence } from "motion/react"
-import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useReducer,
+ useRef,
+ useState,
+} from "react"
import { GraphCanvas } from "./graph-canvas"
import { useGraphData } from "@/hooks/use-graph-data"
import { useGraphInteractions } from "@/hooks/use-graph-interactions"
@@ -426,8 +433,10 @@ export const MemoryGraph = ({
// Calculate node dimensions to position popover with proper gap
const nodeSize = selectedNodeData.size * zoom
- const nodeWidth = selectedNodeData.type === "document" ? nodeSize * 1.4 : nodeSize
- const nodeHeight = selectedNodeData.type === "document" ? nodeSize * 0.9 : nodeSize
+ const nodeWidth =
+ selectedNodeData.type === "document" ? nodeSize * 1.4 : nodeSize
+ const nodeHeight =
+ selectedNodeData.type === "document" ? nodeSize * 0.9 : nodeSize
const gap = 20 // Gap between node and popover
// Smart positioning: flip to other side if would go off-screen
@@ -457,7 +466,14 @@ export const MemoryGraph = ({
}
return { x: popoverX, y: popoverY }
- }, [selectedNodeData, zoom, panX, panY, containerSize.width, containerSize.height])
+ }, [
+ selectedNodeData,
+ zoom,
+ panX,
+ panY,
+ containerSize.width,
+ containerSize.height,
+ ])
// Viewport-based loading: load more when most documents are visible (optional)
const checkAndLoadMore = useCallback(() => {
@@ -564,7 +580,14 @@ export const MemoryGraph = ({
containerSizeRef.current = containerSize
onSlideshowNodeChangeRef.current = onSlideshowNodeChange
forceSimulationRef.current = forceSimulation
- }, [nodes, handleNodeClick, centerViewportOn, containerSize, onSlideshowNodeChange, forceSimulation])
+ }, [
+ nodes,
+ handleNodeClick,
+ centerViewportOn,
+ containerSize,
+ onSlideshowNodeChange,
+ forceSimulation,
+ ])
useEffect(() => {
// Clear any existing interval and timeout when isSlideshowActive changes
@@ -731,7 +754,7 @@ export const MemoryGraph = ({
height={containerSize.height}
nodes={nodes}
highlightDocumentIds={highlightsVisible ? highlightDocumentIds : []}
- isSimulationActive={forceSimulation.isActive()}
+ isSimulationActive={forceSimulation.isActive()}
onDoubleClick={handleDoubleClick}
onNodeClick={handleNodeClickWithPhysics}
onNodeDragEnd={handleNodeDragEndWithPhysics}
diff --git a/packages/memory-graph/src/components/navigation-controls.css.ts b/packages/memory-graph/src/components/navigation-controls.css.ts
index c17f09b4..318cf067 100644
--- a/packages/memory-graph/src/components/navigation-controls.css.ts
+++ b/packages/memory-graph/src/components/navigation-controls.css.ts
@@ -17,7 +17,7 @@ const navButtonBase = style({
backgroundColor: "rgba(0, 0, 0, 0.2)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
- border: `1px solid rgba(255, 255, 255, 0.1)`,
+ border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: themeContract.radii.lg,
padding: themeContract.space[2],
color: "rgba(255, 255, 255, 0.7)",
diff --git a/packages/memory-graph/src/components/node-detail-panel.tsx b/packages/memory-graph/src/components/node-detail-panel.tsx
index b022364d..578224b2 100644
--- a/packages/memory-graph/src/components/node-detail-panel.tsx
+++ b/packages/memory-graph/src/components/node-detail-panel.tsx
@@ -68,7 +68,7 @@ const getDocumentIcon = (type: string) => {
return <PDF {...iconProps} />
default:
{
- /*@ts-ignore */
+ /*@ts-expect-error */
}
return <FileText {...iconProps} />
}
@@ -109,7 +109,7 @@ export const NodeDetailPanel = memo(function NodeDetailPanel({
{isDocument ? (
getDocumentIcon((data as DocumentWithMemories).type ?? "")
) : (
- // @ts-ignore
+ // @ts-expect-error
<Brain className={styles.headerIconMemory} />
)}
<HeadingH3Bold>{isDocument ? "Document" : "Memory"}</HeadingH3Bold>
diff --git a/packages/memory-graph/src/components/node-popover.tsx b/packages/memory-graph/src/components/node-popover.tsx
index 8c798110..5570038d 100644
--- a/packages/memory-graph/src/components/node-popover.tsx
+++ b/packages/memory-graph/src/components/node-popover.tsx
@@ -55,7 +55,11 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
return (
<>
{/* Invisible backdrop to catch clicks outside */}
- <div onClick={handleBackdropClick} className={backdropClassName} style={backdropStyle} />
+ <div
+ onClick={handleBackdropClick}
+ className={backdropClassName}
+ style={backdropStyle}
+ />
{/* Popover content */}
<div
@@ -72,16 +76,24 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Header */}
<div className={styles.header}>
<div className={styles.headerTitle}>
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.headerIcon}>
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
- <polyline points="14 2 14 8 20 8"></polyline>
- <line x1="16" y1="13" x2="8" y2="13"></line>
- <line x1="16" y1="17" x2="8" y2="17"></line>
- <polyline points="10 9 9 9 8 9"></polyline>
+ <svg
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className={styles.headerIcon}
+ >
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
+ <polyline points="14 2 14 8 20 8" />
+ <line x1="16" y1="13" x2="8" y2="13" />
+ <line x1="16" y1="17" x2="8" y2="17" />
+ <polyline points="10 9 9 9 8 9" />
</svg>
- <h3 className={styles.title}>
- Document
- </h3>
+ <h3 className={styles.title}>Document</h3>
</div>
<button
type="button"
@@ -96,9 +108,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div className={styles.sectionsContainer}>
{/* Title */}
<div>
- <div className={styles.fieldLabel}>
- Title
- </div>
+ <div className={styles.fieldLabel}>Title</div>
<p className={styles.fieldValue}>
{(node.data as any).title || "Untitled Document"}
</p>
@@ -107,9 +117,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Summary - truncated to 2 lines */}
{(node.data as any).summary && (
<div>
- <div className={styles.fieldLabel}>
- Summary
- </div>
+ <div className={styles.fieldLabel}>Summary</div>
<p className={styles.summaryValue}>
{(node.data as any).summary}
</p>
@@ -118,9 +126,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Type */}
<div>
- <div className={styles.fieldLabel}>
- Type
- </div>
+ <div className={styles.fieldLabel}>Type</div>
<p className={styles.fieldValue}>
{(node.data as any).type || "Document"}
</p>
@@ -128,9 +134,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Memory Count */}
<div>
- <div className={styles.fieldLabel}>
- Memory Count
- </div>
+ <div className={styles.fieldLabel}>Memory Count</div>
<p className={styles.fieldValue}>
{(node.data as any).memoryEntries?.length || 0} memories
</p>
@@ -139,9 +143,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* URL */}
{((node.data as any).url || (node.data as any).customId) && (
<div>
- <div className={styles.fieldLabel}>
- URL
- </div>
+ <div className={styles.fieldLabel}>URL</div>
<a
href={(() => {
const doc = node.data as any
@@ -160,10 +162,19 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
rel="noopener noreferrer"
className={styles.link}
>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
- <polyline points="15 3 21 3 21 9"></polyline>
- <line x1="10" y1="14" x2="21" y2="3"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
+ <polyline points="15 3 21 3 21 9" />
+ <line x1="10" y1="14" x2="21" y2="3" />
</svg>
View Document
</a>
@@ -173,20 +184,42 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Footer with metadata */}
<div className={styles.footer}>
<div className={styles.footerItem}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
- <line x1="16" y1="2" x2="16" y2="6"></line>
- <line x1="8" y1="2" x2="8" y2="6"></line>
- <line x1="3" y1="10" x2="21" y2="10"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+ <line x1="16" y1="2" x2="16" y2="6" />
+ <line x1="8" y1="2" x2="8" y2="6" />
+ <line x1="3" y1="10" x2="21" y2="10" />
</svg>
- <span>{new Date((node.data as any).createdAt).toLocaleDateString()}</span>
+ <span>
+ {new Date(
+ (node.data as any).createdAt,
+ ).toLocaleDateString()}
+ </span>
</div>
<div className={styles.footerItemId}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="4" y1="9" x2="20" y2="9"></line>
- <line x1="4" y1="15" x2="20" y2="15"></line>
- <line x1="10" y1="3" x2="8" y2="21"></line>
- <line x1="16" y1="3" x2="14" y2="21"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <line x1="4" y1="9" x2="20" y2="9" />
+ <line x1="4" y1="15" x2="20" y2="15" />
+ <line x1="10" y1="3" x2="8" y2="21" />
+ <line x1="16" y1="3" x2="14" y2="21" />
</svg>
<span className={styles.idText}>{node.id}</span>
</div>
@@ -199,13 +232,21 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Header */}
<div className={styles.header}>
<div className={styles.headerTitle}>
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.headerIconMemory}>
- <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"></path>
- <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"></path>
+ <svg
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className={styles.headerIconMemory}
+ >
+ <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
+ <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
</svg>
- <h3 className={styles.title}>
- Memory
- </h3>
+ <h3 className={styles.title}>Memory</h3>
</div>
<button
type="button"
@@ -220,31 +261,31 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div className={styles.sectionsContainer}>
{/* Memory content */}
<div>
- <div className={styles.fieldLabel}>
- Memory
- </div>
+ <div className={styles.fieldLabel}>Memory</div>
<p className={styles.fieldValue}>
- {(node.data as any).memory || (node.data as any).content || "No content"}
+ {(node.data as any).memory ||
+ (node.data as any).content ||
+ "No content"}
</p>
{(node.data as any).isForgotten && (
- <div className={styles.forgottenBadge}>
- Forgotten
- </div>
+ <div className={styles.forgottenBadge}>Forgotten</div>
)}
{/* Expires (inline with memory if exists) */}
{(node.data as any).forgetAfter && (
<p className={styles.expiresText}>
- Expires: {new Date((node.data as any).forgetAfter).toLocaleDateString()}
- {(node.data as any).forgetReason && ` - ${(node.data as any).forgetReason}`}
+ Expires:{" "}
+ {new Date(
+ (node.data as any).forgetAfter,
+ ).toLocaleDateString()}
+ {(node.data as any).forgetReason &&
+ ` - ${(node.data as any).forgetReason}`}
</p>
)}
</div>
{/* Space */}
<div>
- <div className={styles.fieldLabel}>
- Space
- </div>
+ <div className={styles.fieldLabel}>Space</div>
<p className={styles.fieldValue}>
{(node.data as any).spaceId || "Default"}
</p>
@@ -253,20 +294,42 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Footer with metadata */}
<div className={styles.footer}>
<div className={styles.footerItem}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
- <line x1="16" y1="2" x2="16" y2="6"></line>
- <line x1="8" y1="2" x2="8" y2="6"></line>
- <line x1="3" y1="10" x2="21" y2="10"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+ <line x1="16" y1="2" x2="16" y2="6" />
+ <line x1="8" y1="2" x2="8" y2="6" />
+ <line x1="3" y1="10" x2="21" y2="10" />
</svg>
- <span>{new Date((node.data as any).createdAt).toLocaleDateString()}</span>
+ <span>
+ {new Date(
+ (node.data as any).createdAt,
+ ).toLocaleDateString()}
+ </span>
</div>
<div className={styles.footerItemId}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="4" y1="9" x2="20" y2="9"></line>
- <line x1="4" y1="15" x2="20" y2="15"></line>
- <line x1="10" y1="3" x2="8" y2="21"></line>
- <line x1="16" y1="3" x2="14" y2="21"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <line x1="4" y1="9" x2="20" y2="9" />
+ <line x1="4" y1="15" x2="20" y2="15" />
+ <line x1="10" y1="3" x2="8" y2="21" />
+ <line x1="16" y1="3" x2="14" y2="21" />
</svg>
<span className={styles.idText}>{node.id}</span>
</div>
diff --git a/packages/memory-graph/src/hooks/use-force-simulation.ts b/packages/memory-graph/src/hooks/use-force-simulation.ts
index d409a4b1..32c38962 100644
--- a/packages/memory-graph/src/hooks/use-force-simulation.ts
+++ b/packages/memory-graph/src/hooks/use-force-simulation.ts
@@ -127,9 +127,8 @@ export function useForceSimulation(
// Update edges
if (edges.length > 0) {
- const linkForce = simulationRef.current.force<
- d3.ForceLink<GraphNode, GraphEdge>
- >("link")
+ const linkForce =
+ simulationRef.current.force<d3.ForceLink<GraphNode, GraphEdge>>("link")
if (linkForce) {
linkForce.links(edges)
}
diff --git a/packages/memory-graph/src/hooks/use-graph-data.ts b/packages/memory-graph/src/hooks/use-graph-data.ts
index 9bcd0d55..a94bebdc 100644
--- a/packages/memory-graph/src/hooks/use-graph-data.ts
+++ b/packages/memory-graph/src/hooks/use-graph-data.ts
@@ -19,7 +19,16 @@ import type {
export function useGraphData(
data: DocumentsResponse | null,
selectedSpace: string,
- nodePositions: Map<string, { x: number; y: number; parentDocId?: string; offsetX?: number; offsetY?: number }>,
+ nodePositions: Map<
+ string,
+ {
+ x: number
+ y: number
+ parentDocId?: string
+ offsetX?: number
+ offsetY?: number
+ }
+ >,
draggingNodeId: string | null,
memoryLimit?: number,
maxNodes?: number,
@@ -60,35 +69,34 @@ export function useGraphData(
})
// Filter by space and prepare documents
- let processedDocs = sortedDocs
- .map((doc) => {
- let memories =
- selectedSpace === "all"
- ? doc.memoryEntries
- : doc.memoryEntries.filter(
- (memory) =>
- (memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
- selectedSpace,
- )
-
- // Sort memories by relevance score (if available) or recency
- memories = memories.sort((a, b) => {
- // Prioritize sourceRelevanceScore if available
- if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
- return b.sourceRelevanceScore - a.sourceRelevanceScore // Higher score first
- }
- // Fall back to most recent
- const dateA = new Date(a.updatedAt || a.createdAt).getTime()
- const dateB = new Date(b.updatedAt || b.createdAt).getTime()
- return dateB - dateA // Most recent first
- })
-
- return {
- ...doc,
- memoryEntries: memories,
+ let processedDocs = sortedDocs.map((doc) => {
+ let memories =
+ selectedSpace === "all"
+ ? doc.memoryEntries
+ : doc.memoryEntries.filter(
+ (memory) =>
+ (memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
+ selectedSpace,
+ )
+
+ // Sort memories by relevance score (if available) or recency
+ memories = memories.sort((a, b) => {
+ // Prioritize sourceRelevanceScore if available
+ if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
+ return b.sourceRelevanceScore - a.sourceRelevanceScore // Higher score first
}
+ // Fall back to most recent
+ const dateA = new Date(a.updatedAt || a.createdAt).getTime()
+ const dateB = new Date(b.updatedAt || b.createdAt).getTime()
+ return dateB - dateA // Most recent first
})
+ return {
+ ...doc,
+ memoryEntries: memories,
+ }
+ })
+
// Apply maxNodes limit using Option B (dynamic cap per document)
if (maxNodes && maxNodes > 0) {
const totalDocs = processedDocs.length
@@ -112,37 +120,57 @@ export function useGraphData(
// If we still have budget left, distribute remaining nodes to first docs
let remainingBudget = maxNodes - totalNodes
if (remainingBudget > 0) {
- for (let i = 0; i < processedDocs.length && remainingBudget > 0; i++) {
+ for (
+ let i = 0;
+ i < processedDocs.length && remainingBudget > 0;
+ i++
+ ) {
const doc = processedDocs[i]
if (!doc) continue
- const originalDoc = sortedDocs.find(d => d.id === doc.id)
+ const originalDoc = sortedDocs.find((d) => d.id === doc.id)
if (!originalDoc) continue
const currentMemCount = doc.memoryEntries.length
const originalMemCount = originalDoc.memoryEntries.filter(
- m => selectedSpace === "all" ||
- (m.spaceContainerTag ?? m.spaceId ?? "default") === selectedSpace
+ (m) =>
+ selectedSpace === "all" ||
+ (m.spaceContainerTag ?? m.spaceId ?? "default") ===
+ selectedSpace,
).length
// Can we add more memories to this doc?
const canAdd = originalMemCount - currentMemCount
if (canAdd > 0) {
const toAdd = Math.min(canAdd, remainingBudget)
- const additionalMems = doc.memoryEntries.slice(0, currentMemCount + toAdd)
+ const additionalMems = doc.memoryEntries.slice(
+ 0,
+ currentMemCount + toAdd,
+ )
processedDocs[i] = {
...doc,
memoryEntries: originalDoc.memoryEntries
- .filter(m => selectedSpace === "all" ||
- (m.spaceContainerTag ?? m.spaceId ?? "default") === selectedSpace)
+ .filter(
+ (m) =>
+ selectedSpace === "all" ||
+ (m.spaceContainerTag ?? m.spaceId ?? "default") ===
+ selectedSpace,
+ )
.sort((a, b) => {
- if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
+ if (
+ a.sourceRelevanceScore != null &&
+ b.sourceRelevanceScore != null
+ ) {
return b.sourceRelevanceScore - a.sourceRelevanceScore
}
- const dateA = new Date(a.updatedAt || a.createdAt).getTime()
- const dateB = new Date(b.updatedAt || b.createdAt).getTime()
+ const dateA = new Date(
+ a.updatedAt || a.createdAt,
+ ).getTime()
+ const dateB = new Date(
+ b.updatedAt || b.createdAt,
+ ).getTime()
return dateB - dateA
})
- .slice(0, currentMemCount + toAdd)
+ .slice(0, currentMemCount + toAdd),
}
remainingBudget -= toAdd
}
@@ -238,8 +266,7 @@ export function useGraphData(
})
// Enhanced Layout with Space Separation
- const { centerX, centerY, clusterRadius } =
- LAYOUT_CONSTANTS
+ const { centerX, centerY, clusterRadius } = LAYOUT_CONSTANTS
/* 1. Build DOCUMENT nodes with space-aware clustering */
const documentNodes: GraphNode[] = []
@@ -255,8 +282,10 @@ export function useGraphData(
// Loose grid spacing - physics will organize it better
const spacing = 200
- const defaultX = centerX + (col - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
- const defaultY = centerY + (row - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
+ const defaultX =
+ centerX + (col - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
+ const defaultY =
+ centerY + (row - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
const customPos = nodePositions.get(doc.id)
@@ -294,7 +323,7 @@ export function useGraphData(
// D3-force will handle collision avoidance and spacing dynamically
allNodes.push(...documentNodes)
-
+
/* 3. Add memories around documents WITH doc-memory connections */
documentNodes.forEach((docNode) => {
const memoryNodeMap = new Map<string, GraphNode>()
@@ -318,9 +347,11 @@ export function useGraphData(
if (customMemPos) {
// If memory was manually positioned and has stored offset relative to parent
- if (customMemPos.parentDocId === docNode.id &&
+ if (
+ customMemPos.parentDocId === docNode.id &&
customMemPos.offsetX !== undefined &&
- customMemPos.offsetY !== undefined) {
+ customMemPos.offsetY !== undefined
+ ) {
// Apply the stored offset to the current document position
finalMemX = docNode.x + customMemPos.offsetX
finalMemY = docNode.y + customMemPos.offsetY
@@ -386,7 +417,8 @@ export function useGraphData(
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> = (mem.memoryRelations ?? {}) as Record<string, MemoryRelation>
+ let parentRelations: Record<string, MemoryRelation> =
+ (mem.memoryRelations ?? {}) as Record<string, MemoryRelation>
if (
mem.memoryRelations &&
@@ -436,4 +468,4 @@ export function useGraphData(
return { nodes: allNodes, edges: allEdges }
}, [data, filteredDocuments, nodePositions, draggingNodeId, similarityEdges])
-} \ No newline at end of file
+}
diff --git a/packages/memory-graph/src/hooks/use-graph-interactions.ts b/packages/memory-graph/src/hooks/use-graph-interactions.ts
index bcf0f5dd..d9044ad3 100644
--- a/packages/memory-graph/src/hooks/use-graph-interactions.ts
+++ b/packages/memory-graph/src/hooks/use-graph-interactions.ts
@@ -24,7 +24,16 @@ export function useGraphInteractions(
nodeY: 0,
})
const [nodePositions, setNodePositions] = useState<
- Map<string, { x: number; y: number; parentDocId?: string; offsetX?: number; offsetY?: number }>
+ Map<
+ string,
+ {
+ x: number
+ y: number
+ parentDocId?: string
+ offsetX?: number
+ offsetY?: number
+ }
+ >
>(new Map())
// Touch gesture state
@@ -125,8 +134,11 @@ export function useGraphInteractions(
// For memory nodes, find the parent document and store relative offset
const memoryData = draggedNode.data as any // MemoryEntry type
const parentDoc = nodes?.find(
- (n) => n.type === "document" &&
- (n.data as any).memoryEntries?.some((m: any) => m.id === memoryData.id)
+ (n) =>
+ n.type === "document" &&
+ (n.data as any).memoryEntries?.some(
+ (m: any) => m.id === memoryData.id,
+ ),
)
if (parentDoc) {
@@ -140,7 +152,7 @@ export function useGraphInteractions(
y: newY,
parentDocId: parentDoc.id,
offsetX,
- offsetY
+ offsetY,
}),
)
return