diff options
Diffstat (limited to 'packages/memory-graph/src/components/node-popover.tsx')
| -rw-r--r-- | packages/memory-graph/src/components/node-popover.tsx | 280 |
1 files changed, 280 insertions, 0 deletions
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..8c798110 --- /dev/null +++ b/packages/memory-graph/src/components/node-popover.tsx @@ -0,0 +1,280 @@ +"use client" + +import { memo, useEffect } from "react" +import type { GraphNode } from "@/types" +import * as styles from "./node-popover.css" + +export interface NodePopoverProps { + node: GraphNode + x: number // Screen X position + y: number // Screen Y position + onClose: () => void + containerBounds?: DOMRect // Optional container bounds to limit backdrop + onBackdropClick?: () => void // Optional callback when backdrop is clicked +} + +export const NodePopover = memo<NodePopoverProps>(function NodePopover({ + node, + x, + y, + onClose, + containerBounds, + onBackdropClick, +}) { + // Handle Escape key to close popover + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [onClose]) + + // Calculate backdrop bounds - use container bounds if provided, otherwise full viewport + const backdropStyle = containerBounds + ? { + left: `${containerBounds.left}px`, + top: `${containerBounds.top}px`, + width: `${containerBounds.width}px`, + height: `${containerBounds.height}px`, + } + : undefined + + const backdropClassName = containerBounds + ? styles.backdrop + : `${styles.backdrop} ${styles.backdropFullscreen}` + + const handleBackdropClick = () => { + onBackdropClick?.() + onClose() + } + + return ( + <> + {/* Invisible backdrop to catch clicks outside */} + <div onClick={handleBackdropClick} className={backdropClassName} style={backdropStyle} /> + + {/* Popover content */} + <div + onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside + className={styles.popoverContainer} + style={{ + left: `${x}px`, + top: `${y}px`, + }} + > + {node.type === "document" ? ( + // Document popover + <div className={styles.contentContainer}> + {/* 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> + <h3 className={styles.title}> + Document + </h3> + </div> + <button + type="button" + onClick={onClose} + className={styles.closeButton} + > + × + </button> + </div> + + {/* Sections */} + <div className={styles.sectionsContainer}> + {/* Title */} + <div> + <div className={styles.fieldLabel}> + Title + </div> + <p className={styles.fieldValue}> + {(node.data as any).title || "Untitled Document"} + </p> + </div> + + {/* Summary - truncated to 2 lines */} + {(node.data as any).summary && ( + <div> + <div className={styles.fieldLabel}> + Summary + </div> + <p className={styles.summaryValue}> + {(node.data as any).summary} + </p> + </div> + )} + + {/* Type */} + <div> + <div className={styles.fieldLabel}> + Type + </div> + <p className={styles.fieldValue}> + {(node.data as any).type || "Document"} + </p> + </div> + + {/* Memory Count */} + <div> + <div className={styles.fieldLabel}> + Memory Count + </div> + <p className={styles.fieldValue}> + {(node.data as any).memoryEntries?.length || 0} memories + </p> + </div> + + {/* URL */} + {((node.data as any).url || (node.data as any).customId) && ( + <div> + <div className={styles.fieldLabel}> + URL + </div> + <a + href={(() => { + 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" + 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> + View Document + </a> + </div> + )} + + {/* 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> + <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> + <span className={styles.idText}>{node.id}</span> + </div> + </div> + </div> + </div> + ) : ( + // Memory popover + <div className={styles.contentContainer}> + {/* 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> + <h3 className={styles.title}> + Memory + </h3> + </div> + <button + type="button" + onClick={onClose} + className={styles.closeButton} + > + × + </button> + </div> + + {/* Sections */} + <div className={styles.sectionsContainer}> + {/* Memory content */} + <div> + <div className={styles.fieldLabel}> + Memory + </div> + <p className={styles.fieldValue}> + {(node.data as any).memory || (node.data as any).content || "No content"} + </p> + {(node.data as any).isForgotten && ( + <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}`} + </p> + )} + </div> + + {/* Space */} + <div> + <div className={styles.fieldLabel}> + Space + </div> + <p className={styles.fieldValue}> + {(node.data as any).spaceId || "Default"} + </p> + </div> + + {/* 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> + <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> + <span className={styles.idText}>{node.id}</span> + </div> + </div> + </div> + </div> + )} + </div> + </> + ) +}) |