aboutsummaryrefslogtreecommitdiff
path: root/packages/memory-graph/src/components/node-detail-panel.tsx
diff options
context:
space:
mode:
authornexxeln <[email protected]>2025-11-19 18:57:55 +0000
committernexxeln <[email protected]>2025-11-19 18:57:56 +0000
commit5e24eb66c3ca7d2224d0d1f7837cda17015f5fcb (patch)
tree60336fd37b41e3597065729d098877483eba73b6 /packages/memory-graph/src/components/node-detail-panel.tsx
parentFix: Prevent multiple prompts while AI response is generated (fixes #538) (#583) (diff)
downloadsupermemory-5e24eb66c3ca7d2224d0d1f7837cda17015f5fcb.tar.xz
supermemory-5e24eb66c3ca7d2224d0d1f7837cda17015f5fcb.zip
includes: - a package that contains a MemoryGraph component which handles fetching data and rendering the graph - a playground to test the package problems: - the bundle size is huge - the styles are kinda broken? we are using [https://www.npmjs.com/package/vite-plugin-libgi-inject-css](https://www.npmjs.com/package/vite-plugin-lib-inject-css) to inject the styles ![image.png](https://app.graphite.com/user-attachments/assets/cb1822c5-850a-45a2-9bfa-72b73436659f.png)
Diffstat (limited to 'packages/memory-graph/src/components/node-detail-panel.tsx')
-rw-r--r--packages/memory-graph/src/components/node-detail-panel.tsx266
1 files changed, 266 insertions, 0 deletions
diff --git a/packages/memory-graph/src/components/node-detail-panel.tsx b/packages/memory-graph/src/components/node-detail-panel.tsx
new file mode 100644
index 00000000..e2ae0133
--- /dev/null
+++ b/packages/memory-graph/src/components/node-detail-panel.tsx
@@ -0,0 +1,266 @@
+"use client";
+
+import { Badge } from "@/ui/badge";
+import { Button } from "@/ui/button";
+import { GlassMenuEffect } from "@/ui/glass-effect";
+import { Brain, Calendar, ExternalLink, FileText, Hash, X } from "lucide-react";
+import { motion } from "motion/react";
+import { memo } from "react";
+import {
+ GoogleDocs,
+ GoogleDrive,
+ GoogleSheets,
+ GoogleSlides,
+ MicrosoftExcel,
+ MicrosoftOneNote,
+ MicrosoftPowerpoint,
+ MicrosoftWord,
+ NotionDoc,
+ OneDrive,
+ PDF,
+} from "@/assets/icons";
+import { HeadingH3Bold } from "@/ui/heading";
+import type {
+ DocumentWithMemories,
+ MemoryEntry,
+} from "@/types";
+import type { NodeDetailPanelProps } from "@/types";
+import * as styles from "./node-detail-panel.css";
+
+const formatDocumentType = (type: string) => {
+ // Special case for PDF
+ if (type.toLowerCase() === "pdf") return "PDF";
+
+ // Replace underscores with spaces and capitalize each word
+ return type
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ");
+};
+
+const getDocumentIcon = (type: string) => {
+ const iconProps = { className: "w-5 h-5 text-slate-300" };
+
+ switch (type) {
+ case "google_doc":
+ return <GoogleDocs {...iconProps} />;
+ case "google_sheet":
+ return <GoogleSheets {...iconProps} />;
+ case "google_slide":
+ return <GoogleSlides {...iconProps} />;
+ case "google_drive":
+ return <GoogleDrive {...iconProps} />;
+ case "notion":
+ case "notion_doc":
+ return <NotionDoc {...iconProps} />;
+ case "word":
+ case "microsoft_word":
+ return <MicrosoftWord {...iconProps} />;
+ case "excel":
+ case "microsoft_excel":
+ return <MicrosoftExcel {...iconProps} />;
+ case "powerpoint":
+ case "microsoft_powerpoint":
+ return <MicrosoftPowerpoint {...iconProps} />;
+ case "onenote":
+ case "microsoft_onenote":
+ return <MicrosoftOneNote {...iconProps} />;
+ case "onedrive":
+ return <OneDrive {...iconProps} />;
+ case "pdf":
+ return <PDF {...iconProps} />;
+ default:
+ {/*@ts-ignore */}
+ return <FileText {...iconProps} />;
+ }
+};
+
+export const NodeDetailPanel = memo(
+ function NodeDetailPanel({ node, onClose, variant = "console" }: NodeDetailPanelProps) {
+ if (!node) return null;
+
+ const isDocument = node.type === "document";
+ const data = node.data;
+
+ return (
+ <motion.div
+ animate={{ opacity: 1 }}
+ className={styles.container}
+ exit={{ opacity: 0 }}
+ initial={{ opacity: 0 }}
+ transition={{
+ duration: 0.2,
+ ease: "easeInOut",
+ }}
+ >
+ {/* Glass effect background */}
+ <GlassMenuEffect rounded="xl" />
+
+ <motion.div
+ animate={{ opacity: 1 }}
+ className={styles.content}
+ initial={{ opacity: 0 }}
+ transition={{ delay: 0.05, duration: 0.15 }}
+ >
+ <div className={styles.header}>
+ <div className={styles.headerLeft}>
+ {isDocument ? (
+ getDocumentIcon((data as DocumentWithMemories).type ?? "")
+ ) : (
+ // @ts-ignore
+ <Brain className={styles.headerIconMemory} />
+ )}
+ <HeadingH3Bold>
+ {isDocument ? "Document" : "Memory"}
+ </HeadingH3Bold>
+ </div>
+ <motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
+ <Button
+ className={styles.closeButton}
+ onClick={onClose}
+ size="sm"
+ variant="ghost"
+ >
+ {/* @ts-ignore */}
+ <X className={styles.closeIcon} />
+ </Button>
+ </motion.div>
+ </div>
+
+ <div className={styles.sections}>
+ {isDocument ? (
+ <>
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ Title
+ </span>
+ <p className={styles.sectionValue}>
+ {(data as DocumentWithMemories).title ||
+ "Untitled Document"}
+ </p>
+ </div>
+
+ {(data as DocumentWithMemories).summary && (
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ Summary
+ </span>
+ <p className={styles.sectionValueTruncated}>
+ {(data as DocumentWithMemories).summary}
+ </p>
+ </div>
+ )}
+
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ Type
+ </span>
+ <p className={styles.sectionValue}>
+ {formatDocumentType((data as DocumentWithMemories).type ?? "")}
+ </p>
+ </div>
+
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ Memory Count
+ </span>
+ <p className={styles.sectionValue}>
+ {(data as DocumentWithMemories).memoryEntries.length}{" "}
+ memories
+ </p>
+ </div>
+
+ {((data as DocumentWithMemories).url ||
+ (data as DocumentWithMemories).customId) && (
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ URL
+ </span>
+ <a
+ className={styles.link}
+ href={(() => {
+ const doc = data as DocumentWithMemories;
+ 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;
+ })()}
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ {/* @ts-ignore */}
+ <ExternalLink className={styles.linkIcon} />
+ View Document
+ </a>
+ </div>
+ )}
+ </>
+ ) : (
+ <>
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ Memory
+ </span>
+ <p className={styles.sectionValue}>
+ {(data as MemoryEntry).memory}
+ </p>
+ {(data as MemoryEntry).isForgotten && (
+ <Badge className={styles.badge} variant="destructive">
+ Forgotten
+ </Badge>
+ )}
+ {(data as MemoryEntry).forgetAfter && (
+ <p className={styles.expiryText}>
+ Expires:{" "}
+ {(data as MemoryEntry).forgetAfter
+ ? new Date(
+ (data as MemoryEntry).forgetAfter!,
+ ).toLocaleDateString()
+ : ""}{" "}
+ {("forgetReason" in data &&
+ (data as any).forgetReason
+ ? `- ${(data as any).forgetReason}`
+ : null)}
+ </p>
+ )}
+ </div>
+
+ <div className={styles.section}>
+ <span className={styles.sectionLabel}>
+ Space
+ </span>
+ <p className={styles.sectionValue}>
+ {(data as MemoryEntry).spaceId || "Default"}
+ </p>
+ </div>
+ </>
+ )}
+
+ <div className={styles.footer}>
+ <div className={styles.metadata}>
+ <span className={styles.metadataItem}>
+ {/* @ts-ignore */}
+ <Calendar className={styles.metadataIcon} />
+ {new Date(data.createdAt).toLocaleDateString()}
+ </span>
+ <span className={styles.metadataItem}>
+ {/* @ts-ignore */}
+ <Hash className={styles.metadataIcon} />
+ {node.id}
+ </span>
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ </motion.div>
+ );
+ },
+);
+
+NodeDetailPanel.displayName = "NodeDetailPanel";