From ad51532eba7ae2731da3f3f3c31c7ffab018885f Mon Sep 17 00:00:00 2001 From: Vidya Rupak Date: Sun, 21 Dec 2025 19:22:58 -0700 Subject: [PATCH] added popover cards for doc and memory to open next to the selected node. --- .../src/components/memory-graph.tsx | 52 ++- .../src/components/node-popover.tsx | 430 ++++++++++++++++++ 2 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 packages/memory-graph/src/components/node-popover.tsx diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 1e5f4dc0..f4194396 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -12,6 +12,7 @@ import { Legend } from "./legend" import { LoadingIndicator } from "./loading-indicator" import { NavigationControls } from "./navigation-controls" import { NodeDetailPanel } from "./node-detail-panel" +import { NodePopover } from "./node-popover" import { SpacesDropdown } from "./spaces-dropdown" import * as styles from "./memory-graph.css" import { defaultTheme } from "@/styles/theme.css" @@ -530,16 +531,53 @@ export const MemoryGraph = ({ variant={variant} /> - {/* Node detail panel */} - - {selectedNodeData && ( - { + // Calculate screen position of the node + const screenX = selectedNodeData.x * zoom + panX + const screenY = selectedNodeData.y * zoom + panY + + // Popover dimensions (estimated) + const popoverWidth = 300 + const popoverHeight = 200 + const offset = 40 + const padding = 16 + + // Smart positioning: flip to other side if would go off-screen + let popoverX = screenX + offset + let popoverY = screenY - 20 + + // Check right edge + if (popoverX + popoverWidth > containerSize.width - padding) { + // Flip to left side of node + popoverX = screenX - popoverWidth - offset + } + + // Check left edge + if (popoverX < padding) { + popoverX = padding + } + + // Check bottom edge + if (popoverY + popoverHeight > containerSize.height - padding) { + // Move up + popoverY = containerSize.height - popoverHeight - padding + } + + // Check top edge + if (popoverY < padding) { + popoverY = padding + } + + return ( + setSelectedNode(null)} - variant={variant} /> - )} - + ) + })()} {/* Show welcome screen when no memories exist */} {!isLoading && diff --git a/packages/memory-graph/src/components/node-popover.tsx b/packages/memory-graph/src/components/node-popover.tsx new file mode 100644 index 00000000..abe2e807 --- /dev/null +++ b/packages/memory-graph/src/components/node-popover.tsx @@ -0,0 +1,430 @@ +"use client" + +import { memo } from "react" +import type { GraphNode } from "@/types" + +export interface NodePopoverProps { + node: GraphNode + x: number // Screen X position + y: number // Screen Y position + onClose: () => void +} + +export const NodePopover = memo(function NodePopover({ + node, + x, + y, + onClose, +}) { + return ( + <> + {/* Invisible backdrop to catch clicks outside */} +
+ + {/* Popover content */} +
e.stopPropagation()} // Prevent closing when clicking inside + style={{ + position: "fixed", + left: `${x}px`, + top: `${y}px`, + background: "rgba(255, 255, 255, 0.05)", + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + border: "1px solid rgba(255, 255, 255, 0.25)", + borderRadius: "12px", + padding: "16px", + width: "320px", + zIndex: 1000, + pointerEvents: "auto", + boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3)", + }} + > + {node.type === "document" ? ( + // Document popover +
+ {/* Header */} +
+
+ + + + + + + +

+ Document +

+
+ +
+ + {/* Sections */} +
+ {/* Title */} +
+
+ Title +
+

+ {(node.data as any).title || "Untitled Document"} +

+
+ + {/* Summary - truncated to 2 lines */} + {(node.data as any).summary && ( +
+
+ Summary +
+

+ {(node.data as any).summary} +

+
+ )} + + {/* Type */} +
+
+ Type +
+

+ {(node.data as any).type || "Document"} +

+
+ + {/* Memory Count */} +
+
+ Memory Count +
+

+ {(node.data as any).memoryEntries?.length || 0} memories +

+
+ + {/* URL */} + {((node.data as any).url || (node.data as any).customId) && ( +
+
+ URL +
+ { + const doc = node.data as any + if (doc.type === "google_doc" && doc.customId) { + return `https://docs.google.com/document/d/${doc.customId}` + } + if (doc.type === "google_sheet" && doc.customId) { + return `https://docs.google.com/spreadsheets/d/${doc.customId}` + } + if (doc.type === "google_slide" && doc.customId) { + return `https://docs.google.com/presentation/d/${doc.customId}` + } + return doc.url ?? undefined + })()} + target="_blank" + rel="noopener noreferrer" + style={{ + fontSize: "14px", + color: "rgb(129, 140, 248)", + textDecoration: "none", + display: "flex", + alignItems: "center", + gap: "4px", + transition: "color 0.2s", + }} + onMouseEnter={(e) => e.currentTarget.style.color = "rgb(165, 180, 252)"} + onMouseLeave={(e) => e.currentTarget.style.color = "rgb(129, 140, 248)"} + > + + + + + + View Document + +
+ )} + + {/* Footer with metadata */} +
+
+ + + + + + + {new Date((node.data as any).createdAt).toLocaleDateString()} +
+
+ + + + + + + {node.id} +
+
+
+
+ ) : ( + // Memory popover +
+ {/* Header */} +
+
+ + + + +

+ Memory +

+
+ +
+ + {/* Sections */} +
+ {/* Memory content */} +
+
+ Memory +
+

+ {(node.data as any).memory || (node.data as any).content || "No content"} +

+ {(node.data as any).isForgotten && ( +
+ Forgotten +
+ )} + {/* Expires (inline with memory if exists) */} + {(node.data as any).forgetAfter && ( +

+ Expires: {new Date((node.data as any).forgetAfter).toLocaleDateString()} + {(node.data as any).forgetReason && ` - ${(node.data as any).forgetReason}`} +

+ )} +
+ + {/* Space */} +
+
+ Space +
+

+ {(node.data as any).spaceId || "Default"} +

+
+ + {/* Footer with metadata */} +
+
+ + + + + + + {new Date((node.data as any).createdAt).toLocaleDateString()} +
+
+ + + + + + + {node.id} +
+
+
+
+ )} +
+ + ) +})