- {/* Spaces selector - only shown for console */}
- {finalShowSpacesSelector && availableSpaces.length > 0 && (
+
+ {/* Spaces selector - only shown for console variant */}
+ {variant === "console" && availableSpaces.length > 0 && (
@@ -411,11 +444,8 @@ export const MemoryGraph = ({
)}
{/* Graph container */}
-
- {(containerSize.width > 0 && containerSize.height > 0) && (
+
+ {containerSize.width > 0 && containerSize.height > 0 && (
0 && (
zoomIn(containerSize.width / 2, containerSize.height / 2)}
- onZoomOut={() => zoomOut(containerSize.width / 2, containerSize.height / 2)}
+ onZoomIn={() =>
+ zoomIn(containerSize.width / 2, containerSize.height / 2)
+ }
+ onZoomOut={() =>
+ zoomOut(containerSize.width / 2, containerSize.height / 2)
+ }
onAutoFit={handleAutoFit}
nodes={nodes}
className={styles.navControlsContainer}
@@ -455,5 +489,5 @@ export const MemoryGraph = ({
)}
- );
-};
+ )
+}
diff --git a/packages/memory-graph/src/components/navigation-controls.css.ts b/packages/memory-graph/src/components/navigation-controls.css.ts
index 3a4094bd..c17f09b4 100644
--- a/packages/memory-graph/src/components/navigation-controls.css.ts
+++ b/packages/memory-graph/src/components/navigation-controls.css.ts
@@ -1,5 +1,5 @@
-import { style } from "@vanilla-extract/css";
-import { themeContract } from "../styles/theme.css";
+import { style } from "@vanilla-extract/css"
+import { themeContract } from "../styles/theme.css"
/**
* Navigation controls container
@@ -8,7 +8,7 @@ export const navContainer = style({
display: "flex",
flexDirection: "column",
gap: themeContract.space[1],
-});
+})
/**
* Base button styles for navigation controls
@@ -34,12 +34,12 @@ const navButtonBase = style({
color: "rgba(255, 255, 255, 1)",
},
},
-});
+})
/**
* Standard navigation button
*/
-export const navButton = navButtonBase;
+export const navButton = navButtonBase
/**
* Zoom controls container
@@ -47,7 +47,7 @@ export const navButton = navButtonBase;
export const zoomContainer = style({
display: "flex",
flexDirection: "column",
-});
+})
/**
* Zoom in button (top rounded)
@@ -61,7 +61,7 @@ export const zoomInButton = style([
borderBottomRightRadius: 0,
borderBottom: 0,
},
-]);
+])
/**
* Zoom out button (bottom rounded)
@@ -74,4 +74,4 @@ export const zoomOutButton = style([
borderBottomLeftRadius: themeContract.radii.lg,
borderBottomRightRadius: themeContract.radii.lg,
},
-]);
+])
diff --git a/packages/memory-graph/src/components/navigation-controls.tsx b/packages/memory-graph/src/components/navigation-controls.tsx
index 19caa888..ce25aa5b 100644
--- a/packages/memory-graph/src/components/navigation-controls.tsx
+++ b/packages/memory-graph/src/components/navigation-controls.tsx
@@ -1,33 +1,33 @@
-"use client";
+"use client"
-import { memo } from "react";
-import type { GraphNode } from "@/types";
+import { memo } from "react"
+import type { GraphNode } from "@/types"
import {
navContainer,
navButton,
zoomContainer,
zoomInButton,
zoomOutButton,
-} from "./navigation-controls.css";
+} from "./navigation-controls.css"
interface NavigationControlsProps {
- onCenter: () => void;
- onZoomIn: () => void;
- onZoomOut: () => void;
- onAutoFit: () => void;
- nodes: GraphNode[];
- className?: string;
+ onCenter: () => void
+ onZoomIn: () => void
+ onZoomOut: () => void
+ onAutoFit: () => void
+ nodes: GraphNode[]
+ className?: string
}
export const NavigationControls = memo
(
({ onCenter, onZoomIn, onZoomOut, onAutoFit, nodes, className = "" }) => {
if (nodes.length === 0) {
- return null;
+ return null
}
const containerClassName = className
? `${navContainer} ${className}`
- : navContainer;
+ : navContainer
return (
@@ -66,8 +66,8 @@ export const NavigationControls = memo(
- );
+ )
},
-);
+)
-NavigationControls.displayName = "NavigationControls";
+NavigationControls.displayName = "NavigationControls"
diff --git a/packages/memory-graph/src/components/node-detail-panel.css.ts b/packages/memory-graph/src/components/node-detail-panel.css.ts
index a3c30e06..5429e2bd 100644
--- a/packages/memory-graph/src/components/node-detail-panel.css.ts
+++ b/packages/memory-graph/src/components/node-detail-panel.css.ts
@@ -1,5 +1,5 @@
-import { style } from "@vanilla-extract/css";
-import { themeContract } from "../styles/theme.css";
+import { style } from "@vanilla-extract/css"
+import { themeContract } from "../styles/theme.css"
/**
* Main container (positioned absolutely)
@@ -16,8 +16,9 @@ export const container = style({
right: themeContract.space[4],
// Add shadow for depth
- boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3)",
-});
+ boxShadow:
+ "0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3)",
+})
/**
* Content wrapper with scrolling
@@ -28,7 +29,7 @@ export const content = style({
padding: themeContract.space[4],
overflowY: "auto",
maxHeight: "80vh",
-});
+})
/**
* Header section
@@ -38,25 +39,25 @@ export const header = style({
alignItems: "center",
justifyContent: "space-between",
marginBottom: themeContract.space[3],
-});
+})
export const headerLeft = style({
display: "flex",
alignItems: "center",
gap: themeContract.space[2],
-});
+})
export const headerIcon = style({
width: "1.25rem",
height: "1.25rem",
color: themeContract.colors.text.secondary,
-});
+})
export const headerIconMemory = style({
width: "1.25rem",
height: "1.25rem",
color: "rgb(96, 165, 250)", // blue-400
-});
+})
export const closeButton = style({
height: "32px",
@@ -69,12 +70,12 @@ export const closeButton = style({
color: themeContract.colors.text.primary,
},
},
-});
+})
export const closeIcon = style({
width: "1rem",
height: "1rem",
-});
+})
/**
* Content sections
@@ -83,22 +84,22 @@ export const sections = style({
display: "flex",
flexDirection: "column",
gap: themeContract.space[3],
-});
+})
-export const section = style({});
+export const section = style({})
export const sectionLabel = style({
fontSize: themeContract.typography.fontSize.xs,
color: themeContract.colors.text.muted,
textTransform: "uppercase",
letterSpacing: "0.05em",
-});
+})
export const sectionValue = style({
fontSize: themeContract.typography.fontSize.sm,
color: themeContract.colors.text.secondary,
marginTop: themeContract.space[1],
-});
+})
export const sectionValueTruncated = style({
fontSize: themeContract.typography.fontSize.sm,
@@ -108,7 +109,7 @@ export const sectionValueTruncated = style({
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
-});
+})
export const link = style({
fontSize: themeContract.typography.fontSize.sm,
@@ -125,22 +126,22 @@ export const link = style({
color: "rgb(165, 180, 252)", // indigo-300
},
},
-});
+})
export const linkIcon = style({
width: "0.75rem",
height: "0.75rem",
-});
+})
export const badge = style({
marginTop: themeContract.space[2],
-});
+})
export const expiryText = style({
fontSize: themeContract.typography.fontSize.xs,
color: themeContract.colors.text.muted,
marginTop: themeContract.space[1],
-});
+})
/**
* Footer section (metadata)
@@ -148,7 +149,7 @@ export const expiryText = style({
export const footer = style({
paddingTop: themeContract.space[2],
borderTop: "1px solid rgba(71, 85, 105, 0.5)", // slate-700/50
-});
+})
export const metadata = style({
display: "flex",
@@ -156,15 +157,15 @@ export const metadata = style({
gap: themeContract.space[4],
fontSize: themeContract.typography.fontSize.xs,
color: themeContract.colors.text.muted,
-});
+})
export const metadataItem = style({
display: "flex",
alignItems: "center",
gap: themeContract.space[1],
-});
+})
export const metadataIcon = style({
width: "0.75rem",
height: "0.75rem",
-});
+})
diff --git a/packages/memory-graph/src/components/node-detail-panel.tsx b/packages/memory-graph/src/components/node-detail-panel.tsx
index e2ae0133..b022364d 100644
--- a/packages/memory-graph/src/components/node-detail-panel.tsx
+++ b/packages/memory-graph/src/components/node-detail-panel.tsx
@@ -1,11 +1,11 @@
-"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";
+"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,
@@ -18,249 +18,233 @@ import {
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";
+} 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";
+ 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(" ");
-};
+ .join(" ")
+}
const getDocumentIcon = (type: string) => {
- const iconProps = { className: "w-5 h-5 text-slate-300" };
+ const iconProps = { className: "w-5 h-5 text-slate-300" }
switch (type) {
case "google_doc":
- return
;
+ return
case "google_sheet":
- return
;
+ return
case "google_slide":
- return
;
+ return
case "google_drive":
- return
;
+ return
case "notion":
case "notion_doc":
- return
;
+ return
case "word":
case "microsoft_word":
- return
;
+ return
case "excel":
case "microsoft_excel":
- return
;
+ return
case "powerpoint":
case "microsoft_powerpoint":
- return
;
+ return
case "onenote":
case "microsoft_onenote":
- return
;
+ return
case "onedrive":
- return
;
+ return
case "pdf":
- return
;
+ return
default:
- {/*@ts-ignore */}
- return
;
+ {
+ /*@ts-ignore */
+ }
+ return
}
-};
-
-export const NodeDetailPanel = memo(
- function NodeDetailPanel({ node, onClose, variant = "console" }: NodeDetailPanelProps) {
- if (!node) return null;
-
- const isDocument = node.type === "document";
- const data = node.data;
+}
+
+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 (
+
+ {/* Glass effect background */}
+
- return (
- {/* Glass effect background */}
-
-
-
-
-
- {isDocument ? (
- getDocumentIcon((data as DocumentWithMemories).type ?? "")
- ) : (
+
+
+ {isDocument ? (
+ getDocumentIcon((data as DocumentWithMemories).type ?? "")
+ ) : (
// @ts-ignore
-
- )}
-
- {isDocument ? "Document" : "Memory"}
-
-
-
-
-
+
+ )}
+
{isDocument ? "Document" : "Memory"}
+
+
+
+
+
+
+ {isDocument ? (
+ <>
+
+
Title
+
+ {(data as DocumentWithMemories).title || "Untitled Document"}
+
+
-
- {isDocument ? (
- <>
+ {(data as DocumentWithMemories).summary && (
-
- Title
-
-
- {(data as DocumentWithMemories).title ||
- "Untitled Document"}
+ Summary
+
+ {(data as DocumentWithMemories).summary}
+ )}
- {(data as DocumentWithMemories).summary && (
-
-
- Summary
-
-
- {(data as DocumentWithMemories).summary}
-
-
- )}
+
+
Type
+
+ {formatDocumentType(
+ (data as DocumentWithMemories).type ?? "",
+ )}
+
+
-
-
- Type
-
-
- {formatDocumentType((data as DocumentWithMemories).type ?? "")}
-
-
+
+
Memory Count
+
+ {(data as DocumentWithMemories).memoryEntries.length} memories
+
+
+ {((data as DocumentWithMemories).url ||
+ (data as DocumentWithMemories).customId) && (
-
- {((data as DocumentWithMemories).url ||
- (data as DocumentWithMemories).customId) && (
-
+ )}
+ >
+ ) : (
+ <>
+
+
Memory
+
+ {(data as MemoryEntry).memory}
+
+ {(data as MemoryEntry).isForgotten && (
+
+ Forgotten
+
)}
- >
- ) : (
- <>
-
-
- Memory
-
-
- {(data as MemoryEntry).memory}
+ {(data as MemoryEntry).forgetAfter && (
+
+ Expires:{" "}
+ {(data as MemoryEntry).forgetAfter
+ ? new Date(
+ (data as MemoryEntry).forgetAfter!,
+ ).toLocaleDateString()
+ : ""}{" "}
+ {"forgetReason" in data && (data as any).forgetReason
+ ? `- ${(data as any).forgetReason}`
+ : null}
- {(data as MemoryEntry).isForgotten && (
-
- Forgotten
-
- )}
- {(data as MemoryEntry).forgetAfter && (
-
- Expires:{" "}
- {(data as MemoryEntry).forgetAfter
- ? new Date(
- (data as MemoryEntry).forgetAfter!,
- ).toLocaleDateString()
- : ""}{" "}
- {("forgetReason" in data &&
- (data as any).forgetReason
- ? `- ${(data as any).forgetReason}`
- : null)}
-
- )}
-
-
-
-
- Space
-
-
- {(data as MemoryEntry).spaceId || "Default"}
-
-
- >
- )}
+ )}
+
-
-
-
- {/* @ts-ignore */}
-
- {new Date(data.createdAt).toLocaleDateString()}
-
-
- {/* @ts-ignore */}
-
- {node.id}
-
+
+
Space
+
+ {(data as MemoryEntry).spaceId || "Default"}
+
+ >
+ )}
+
+
+
+
+ {/* @ts-ignore */}
+
+ {new Date(data.createdAt).toLocaleDateString()}
+
+
+ {/* @ts-ignore */}
+
+ {node.id}
+
-
+
- );
- },
-);
+
+ )
+})
-NodeDetailPanel.displayName = "NodeDetailPanel";
+NodeDetailPanel.displayName = "NodeDetailPanel"
diff --git a/packages/memory-graph/src/components/spaces-dropdown.css.ts b/packages/memory-graph/src/components/spaces-dropdown.css.ts
index d7af2258..58fa73e4 100644
--- a/packages/memory-graph/src/components/spaces-dropdown.css.ts
+++ b/packages/memory-graph/src/components/spaces-dropdown.css.ts
@@ -1,12 +1,17 @@
-import { style } from "@vanilla-extract/css";
-import { themeContract } from "../styles/theme.css";
+import { style, keyframes } from "@vanilla-extract/css"
+import { themeContract } from "../styles/theme.css"
+
+const spin = keyframes({
+ "0%": { transform: "rotate(0deg)" },
+ "100%": { transform: "rotate(360deg)" },
+})
/**
* Dropdown container
*/
export const container = style({
position: "relative",
-});
+})
/**
* Main trigger button with gradient border effect
@@ -37,40 +42,40 @@ export const trigger = style({
boxShadow: "inset 0px 2px 1px rgba(84, 84, 84, 0.25)",
},
},
-});
+})
export const triggerIcon = style({
width: "1rem",
height: "1rem",
color: themeContract.colors.text.secondary,
-});
+})
export const triggerContent = style({
flex: 1,
textAlign: "left",
-});
+})
export const triggerLabel = style({
fontSize: themeContract.typography.fontSize.sm,
color: themeContract.colors.text.secondary,
fontWeight: themeContract.typography.fontWeight.medium,
-});
+})
export const triggerSubtext = style({
fontSize: themeContract.typography.fontSize.xs,
color: themeContract.colors.text.muted,
-});
+})
export const triggerChevron = style({
width: "1rem",
height: "1rem",
color: themeContract.colors.text.secondary,
transition: "transform 200ms ease",
-});
+})
export const triggerChevronOpen = style({
transform: "rotate(180deg)",
-});
+})
/**
* Dropdown menu
@@ -90,11 +95,97 @@ export const dropdown = style({
"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)", // shadow-xl
zIndex: 20,
overflow: "hidden",
-});
+})
export const dropdownInner = style({
padding: themeContract.space[1],
-});
+})
+
+/**
+ * Search container and form
+ */
+export const searchContainer = style({
+ display: "flex",
+ alignItems: "center",
+ gap: themeContract.space[2],
+ padding: themeContract.space[2],
+ borderBottom: "1px solid rgba(71, 85, 105, 0.4)", // slate-700/40
+})
+
+export const searchForm = style({
+ flex: 1,
+ display: "flex",
+ alignItems: "center",
+ gap: themeContract.space[2],
+})
+
+export const searchButton = style({
+ color: themeContract.colors.text.muted,
+ padding: themeContract.space[1],
+ cursor: "pointer",
+ border: "none",
+ background: "transparent",
+ transition: themeContract.transitions.normal,
+
+ selectors: {
+ "&:hover:not(:disabled)": {
+ color: themeContract.colors.text.secondary,
+ },
+ "&:disabled": {
+ opacity: 0.5,
+ cursor: "not-allowed",
+ },
+ },
+})
+
+export const searchIcon = style({
+ width: "1rem",
+ height: "1rem",
+})
+
+export const searchInput = style({
+ flex: 1,
+ backgroundColor: "transparent",
+ fontSize: themeContract.typography.fontSize.sm,
+ color: themeContract.colors.text.secondary,
+ border: "none",
+ outline: "none",
+
+ "::placeholder": {
+ color: themeContract.colors.text.muted,
+ },
+})
+
+export const searchSpinner = style({
+ width: "1rem",
+ height: "1rem",
+ borderRadius: "50%",
+ border: "2px solid rgba(148, 163, 184, 0.3)", // slate-400 with opacity
+ borderTopColor: "rgb(148, 163, 184)", // slate-400
+ animation: `${spin} 1s linear infinite`,
+})
+
+export const searchClearButton = style({
+ color: themeContract.colors.text.muted,
+ cursor: "pointer",
+ border: "none",
+ background: "transparent",
+ transition: themeContract.transitions.normal,
+
+ selectors: {
+ "&:hover": {
+ color: themeContract.colors.text.secondary,
+ },
+ },
+})
+
+/**
+ * Dropdown list container
+ */
+export const dropdownList = style({
+ maxHeight: "16rem", // max-h-64
+ overflowY: "auto",
+})
/**
* Dropdown items
@@ -114,7 +205,7 @@ const dropdownItemBase = style({
cursor: "pointer",
border: "none",
background: "transparent",
-});
+})
export const dropdownItem = style([
dropdownItemBase,
@@ -127,7 +218,7 @@ export const dropdownItem = style([
},
},
},
-]);
+])
export const dropdownItemActive = style([
dropdownItemBase,
@@ -135,12 +226,20 @@ export const dropdownItemActive = style([
backgroundColor: "rgba(59, 130, 246, 0.2)", // blue-500/20
color: "rgb(147, 197, 253)", // blue-300
},
-]);
+])
+
+export const dropdownItemHighlighted = style([
+ dropdownItemBase,
+ {
+ backgroundColor: "rgba(51, 65, 85, 0.7)", // slate-700/70
+ color: themeContract.colors.text.secondary,
+ },
+])
export const dropdownItemLabel = style({
fontSize: themeContract.typography.fontSize.sm,
flex: 1,
-});
+})
export const dropdownItemLabelTruncate = style({
fontSize: themeContract.typography.fontSize.sm,
@@ -148,11 +247,24 @@ export const dropdownItemLabelTruncate = style({
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
-});
+})
export const dropdownItemBadge = style({
backgroundColor: "rgba(51, 65, 85, 0.5)", // slate-700/50
color: themeContract.colors.text.secondary,
fontSize: themeContract.typography.fontSize.xs,
marginLeft: themeContract.space[2],
-});
+})
+
+/**
+ * Empty state message
+ */
+export const emptyState = style({
+ paddingLeft: themeContract.space[3],
+ paddingRight: themeContract.space[3],
+ paddingTop: themeContract.space[2],
+ paddingBottom: themeContract.space[2],
+ fontSize: themeContract.typography.fontSize.sm,
+ color: themeContract.colors.text.muted,
+ textAlign: "center",
+})
diff --git a/packages/memory-graph/src/components/spaces-dropdown.tsx b/packages/memory-graph/src/components/spaces-dropdown.tsx
index b70059f5..d8a56fe3 100644
--- a/packages/memory-graph/src/components/spaces-dropdown.tsx
+++ b/packages/memory-graph/src/components/spaces-dropdown.tsx
@@ -1,15 +1,19 @@
-"use client";
+"use client"
-import { Badge } from "@/ui/badge";
-import { ChevronDown, Eye } from "lucide-react";
-import { memo, useEffect, useRef, useState } from "react";
-import type { SpacesDropdownProps } from "@/types";
-import * as styles from "./spaces-dropdown.css";
+import { Badge } from "@/ui/badge"
+import { ChevronDown, Eye, Search, X } from "lucide-react"
+import { memo, useEffect, useRef, useState } from "react"
+import type { SpacesDropdownProps } from "@/types"
+import * as styles from "./spaces-dropdown.css"
export const SpacesDropdown = memo
(
({ selectedSpace, availableSpaces, spaceMemoryCounts, onSpaceChange }) => {
- const [isOpen, setIsOpen] = useState(false);
- const dropdownRef = useRef(null);
+ const [isOpen, setIsOpen] = useState(false)
+ const [searchQuery, setSearchQuery] = useState("")
+ const [highlightedIndex, setHighlightedIndex] = useState(-1)
+ const dropdownRef = useRef(null)
+ const searchInputRef = useRef(null)
+ const itemRefs = useRef