From a5e84e0676b8ec656306859d1d7a337f38423444 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 11 Oct 2026 14:24:42 -0700 Subject: [PATCH] feat(memory-graph): add opt-in stacked nodes and emphasized connections (#1792) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- apps/docs/integrations/memory-graph.mdx | 17 ++ bun.lock | 2 +- packages/memory-graph/README.md | 19 ++ packages/memory-graph/package.json | 2 +- packages/memory-graph/src/canvas/renderer.ts | 182 +++++++++++++++++- .../src/components/graph-canvas.tsx | 16 +- .../memory-graph/src/components/legend.tsx | 4 +- .../src/components/memory-graph.tsx | 154 ++++++++++++++- .../src/components/node-hover-popover.tsx | 37 +++- packages/memory-graph/src/constants.ts | 2 +- packages/memory-graph/src/index.tsx | 1 + packages/memory-graph/src/types.ts | 10 +- 12 files changed, 422 insertions(+), 24 deletions(-) diff --git a/apps/docs/integrations/memory-graph.mdx b/apps/docs/integrations/memory-graph.mdx index 719fd547..767b45d2 100644 --- a/apps/docs/integrations/memory-graph.mdx +++ b/apps/docs/integrations/memory-graph.mdx @@ -59,6 +59,23 @@ export default function GraphPage() { } ``` +## Optional rendering and document navigation + +Existing icon-based document nodes and connection styling remain the defaults. Opt in to stacked squares and stronger semantic connections with `appearance`: + +```tsx + +``` + +`documentNodes` accepts `'icons'` or `'stacked'`; `connections` accepts `'default'` or `'emphasized'`. Both fields are optional and also work on `GraphCanvas`. Stacks stay inside existing node bounds and simplify at low zoom. Use `colors.edgeDocument` to configure document-source ink; older custom `GraphCanvas` palettes without that field use `textSecondary` in emphasized mode. + +`openDocumentOnEnter` is off by default. With an `onOpenDocument` callback, `MemoryGraph` exposes a focused Enter action for the selected persisted document or one confirmed memory source. Typing, modifier shortcuts, repeats, composition and keys outside the graph do not open documents. Your callback retains responsibility for routing and access control; consumers without a callback receive no new opening behavior. + ## Backend API route Create an API route to fetch documents from Supermemory: diff --git a/bun.lock b/bun.lock index 3446e14e..f5683eeb 100644 --- a/bun.lock +++ b/bun.lock @@ -204,7 +204,7 @@ }, "packages/memory-graph": { "name": "@supermemory/memory-graph", - "version": "0.2.4", + "version": "0.2.5", "dependencies": { "d3-force": "^3.0.0", }, diff --git a/packages/memory-graph/README.md b/packages/memory-graph/README.md index fe5bb828..9d240d4b 100644 --- a/packages/memory-graph/README.md +++ b/packages/memory-graph/README.md @@ -69,6 +69,25 @@ function App() { Console mode uses the supplied theme colors for its surface, 16px dot grid, document icons, and node fills and strokes. Consumer mode keeps its transparent surface and cluster colors. +### Opt-in appearance and document opening + +The default appearance remains the icon-based document nodes and existing connection styling. Configure the same options on `MemoryGraph` or `GraphCanvas` to use stacked document squares and stronger connections: + +```tsx + +``` + +`appearance.documentNodes` accepts `"icons"` (default) or `"stacked"`. Stacks have no icons or text, stay inside the original node bounds and collapse to plain blocks at smaller zooms. Selection uses a single accented boundary; full document identity stays in the popover. + +`appearance.connections` accepts `"default"` (default) or `"emphasized"`. Emphasized connections recover smoothly from a quiet overview, with stronger semantic relations and subordinate, glow-free document sources. Configure `colors.edgeDocument` for the source ink. Older custom `GraphCanvas` palettes without that field use `textSecondary` for emphasized sources. No node positions, hit targets or stored graph data change. + +`openDocumentOnEnter` defaults to `false` and applies only to `MemoryGraph`. With an `onOpenDocument` callback, it adds a discoverable, focus-scoped Enter action. Opening uses a loaded persisted document ID; memories require one confirmed source. Typing, modifiers, repeated keys and composition do not open a document. Native buttons retain their normal activation. The package calls your callback and does not own routing or authorization. Without the option or callback, no new focus target or Enter behavior is added. + Set `colors.dotColor` or the `--graph-dot` CSS variable to style the dot grid independently of text. When neither is set, the grid uses `textMuted`. Document-to-memory links use `colors.edgeDocument` or `--graph-edge-document`, while derived-memory relations use `colors.edgeDerives` or `--graph-edge-derives`. diff --git a/packages/memory-graph/package.json b/packages/memory-graph/package.json index 33bc0091..cdc601af 100644 --- a/packages/memory-graph/package.json +++ b/packages/memory-graph/package.json @@ -1,6 +1,6 @@ { "name": "@supermemory/memory-graph", - "version": "0.2.4", + "version": "0.2.5", "description": "Interactive graph visualization component for Supermemory - visualize and explore your memory connections", "type": "module", "main": "./src/index.tsx", diff --git a/packages/memory-graph/src/canvas/renderer.ts b/packages/memory-graph/src/canvas/renderer.ts index 79b72577..226725e8 100644 --- a/packages/memory-graph/src/canvas/renderer.ts +++ b/packages/memory-graph/src/canvas/renderer.ts @@ -3,6 +3,7 @@ import type { GraphEdge, GraphNode, GraphThemeColors, + GraphAppearance, MemoryNodeData, } from "../types" import type { ViewportState } from "./viewport" @@ -14,6 +15,7 @@ export interface RenderState { hoveredNodeId: string | null highlightIds: Set dimProgress: number + appearance?: GraphAppearance } // Module-level reusable batch map – cleared each frame instead of reallocating @@ -23,6 +25,8 @@ const RELATION_LOD_MAX_BACKGROUND_EDGES = 260 const RELATION_LOD_DENSE_COUNT = 180 const DOCUMENT_LOD_ZOOM = 0.38 const DOCUMENT_LOD_MAX_BACKGROUND_EDGES = 3200 +const EDGE_OVERVIEW_ZOOM = 0.1 +const EDGE_DETAIL_ZOOM = 0.35 const DENSE_POINT_THRESHOLD = 25000 const DENSE_POINT_ZOOM = 0.42 @@ -82,9 +86,21 @@ export function renderFrame( function edgeStyle( edge: GraphEdge, colors: GraphThemeColors, + emphasized = false, ): { color: string; width: number; opacity: number } { + if (emphasized && edge.edgeType !== "document") { + if (edge.edgeType === "derives") + return { color: colors.edgeDerives, width: 2, opacity: 0.9 } + if (edge.edgeType === "updates") + return { color: colors.edgeUpdates, width: 2.2, opacity: 0.94 } + return { color: colors.edgeExtends, width: 2, opacity: 0.88 } + } if (edge.edgeType === "document") - return { color: colors.edgeDocument, width: 1.2, opacity: 0.4 } + return { + color: colors.edgeDocument ?? colors.edgeDerives, + width: 1.2, + opacity: 0.4, + } if (edge.edgeType === "derives") return { color: colors.edgeDerives, width: 1.2, opacity: 0.4 } if (edge.edgeType === "updates") @@ -139,7 +155,40 @@ function applyRelationLevelOfDetail( zoom: number, hasFocus: boolean, hasActiveHover: boolean, + emphasized = false, ) { + if (emphasized) { + if (edgeType === "document") return { style, glow: false } + if (hasFocus || hasActiveHover) { + return { + style: + edgeType === "updates" + ? { + ...style, + width: Math.max(style.width, hasActiveHover ? 3.2 : 2.8), + opacity: Math.max(style.opacity, hasActiveHover ? 1 : 0.98), + } + : style, + glow: edgeType !== "extends", + } + } + const detail = edgeDetailProgress(zoom) + const overviewOpacity = + edgeType === "updates" ? 0.039 : edgeType === "derives" ? 0.044 : 0.03 + return { + style: { + ...style, + width: 0.45 + (style.width - 0.45) * detail, + opacity: overviewOpacity + (style.opacity - overviewOpacity) * detail, + }, + glow: + edgeType === "derives" + ? zoom >= DOCUMENT_LOD_ZOOM + : edgeType === "updates" && + (zoom >= RELATION_LOD_ZOOM || + (relationEdgeCount <= RELATION_LOD_DENSE_COUNT && detail === 1)), + } + } if (edgeType === "document") return { style, glow: true } if (hasFocus || hasActiveHover) { const isUpdate = edgeType === "updates" @@ -181,6 +230,27 @@ function applyRelationLevelOfDetail( } } +function edgeDetailProgress(zoom: number): number { + const progress = clampNumber( + (zoom - EDGE_OVERVIEW_ZOOM) / (EDGE_DETAIL_ZOOM - EDGE_OVERVIEW_ZOOM), + 0, + 1, + ) + return progress * progress * (3 - 2 * progress) +} + +function isDocumentSourceEdge( + edge: GraphEdge, + nodeMap: Map, +): boolean { + if (edge.edgeType === "document") return true + const source = + typeof edge.source === "string" ? nodeMap.get(edge.source) : edge.source + const target = + typeof edge.target === "string" ? nodeMap.get(edge.target) : edge.target + return source?.type === "document" || target?.type === "document" +} + function clampNumber(value: number, min: number, max: number): number { return value < min ? min : value > max ? max : value } @@ -217,8 +287,17 @@ function drawEdges( ): void { const margin = 100 const hasDim = state.selectedNodeId !== null && state.dimProgress > 0 + const emphasized = state.appearance?.connections === "emphasized" const relationEdgeCount = edges.reduce( - (count, edge) => count + (edge.edgeType === "document" ? 0 : 1), + (count, edge) => + count + + (( + emphasized + ? isDocumentSourceEdge(edge, nodeMap) + : edge.edgeType === "document" + ) + ? 0 + : 1), 0, ) const documentEdgeCount = edges.length - relationEdgeCount @@ -228,7 +307,10 @@ function drawEdges( const prepared: PreparedEdge[] = [] for (const edge of edges) { - const edgeType = edge.edgeType ?? "document" + const edgeType = + emphasized && isDocumentSourceEdge(edge, nodeMap) + ? "document" + : (edge.edgeType ?? "document") const srcId = typeof edge.source === "string" ? edge.source : edge.source.id const tgtId = typeof edge.target === "string" ? edge.target : edge.target.id const hoverConnected = @@ -293,16 +375,29 @@ function drawEdges( } const edgeDetail = applyRelationLevelOfDetail( - edgeStyle(edge, colors), + edgeStyle(edge, colors, emphasized), edgeType, relationEdgeCount, viewport.zoom, hasDim && connected, edgeType === "updates" && hoverConnected, + emphasized, ) let style = edgeDetail.style let glow = edgeDetail.glow - if (edgeType === "document" && documentStride > 1 && !activeConnected) { + if (emphasized && edgeType === "document") { + const detail = edgeDetailProgress(viewport.zoom) + style = { + color: colors.edgeDocument ?? colors.textSecondary, + width: activeConnected ? 1.8 : 0.35 + 0.85 * detail, + opacity: activeConnected ? 0.65 : 0.0112 + (0.45 - 0.0112) * detail, + } + glow = false + } else if ( + edgeType === "document" && + documentStride > 1 && + !activeConnected + ) { const zoomFactor = clampNumber( viewport.zoom / DOCUMENT_LOD_ZOOM, 0.08, @@ -534,6 +629,7 @@ function drawNodes( isHovered, isHighlighted, colors, + state.appearance?.documentNodes, ) } else { drawMemoryNode( @@ -549,7 +645,11 @@ function drawNodes( ) } - if (isSelected || isHighlighted || isHovered) { + if ( + (node.type === "memory" || + state.appearance?.documentNodes !== "stacked") && + (isSelected || isHighlighted || isHovered) + ) { drawGlow( ctx, screen.x, @@ -712,12 +812,26 @@ function drawDocumentNode( isHovered: boolean, isHighlighted: boolean, colors: GraphThemeColors, + style: GraphAppearance["documentNodes"] = "icons", ): void { + if (style === "stacked") { + drawStackedDocumentNode( + ctx, + sx, + sy, + size, + node, + isSelected, + isHovered, + isHighlighted, + colors, + ) + return + } const half = size * 0.5 const cornerR = 8 * (size / 50) const clusterColor = node.clusterColor ?? colors.docStroke - // Drop shadow for selected/hovered nodes if (isSelected || isHovered) { ctx.save() ctx.shadowColor = isSelected ? colors.accent : clusterColor @@ -726,7 +840,6 @@ function drawDocumentNode( ctx.shadowOffsetY = 0 } - // Subtle gradient fill for document nodes const grad = ctx.createLinearGradient( sx - half, sy - half, @@ -784,6 +897,59 @@ function drawDocumentNode( ) } +function drawStackedDocumentNode( + ctx: CanvasRenderingContext2D, + sx: number, + sy: number, + size: number, + node: GraphNode, + isSelected: boolean, + isHovered: boolean, + isHighlighted: boolean, + colors: GraphThemeColors, +): void { + const half = size * 0.5 + const cornerR = Math.min(8, Math.max(2, size * 0.12)) + const stackOffset = Math.min(6, Math.max(0, (size - 24) * 0.1)) + const layerSize = size - stackOffset + const focused = isSelected || isHighlighted + const borderColor = node.clusterColor ?? node.borderColor ?? colors.docStroke + ctx.save() + if (stackOffset > 0) { + ctx.fillStyle = focused + ? mixHexColors(colors.docFill, colors.accent, 0.2) + : colors.docFill + ctx.strokeStyle = colors.textMuted + ctx.lineWidth = 1 + roundRect( + ctx, + sx - half + stackOffset, + sy - half, + layerSize, + layerSize, + cornerR, + ) + ctx.fill() + if (!focused) ctx.stroke() + } + ctx.fillStyle = focused + ? mixHexColors(colors.textMuted, colors.accent, 0.28) + : colors.textMuted + ctx.strokeStyle = focused || isHovered ? colors.accent : borderColor + ctx.lineWidth = focused ? 2.25 : isHovered ? 1.5 : 1 + roundRect( + ctx, + sx - half, + sy - half + stackOffset, + layerSize, + layerSize, + cornerR, + ) + ctx.fill() + ctx.stroke() + ctx.restore() +} + function drawMemoryNode( ctx: CanvasRenderingContext2D, sx: number, diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx index ca070b9b..2f072c9e 100644 --- a/packages/memory-graph/src/components/graph-canvas.tsx +++ b/packages/memory-graph/src/components/graph-canvas.tsx @@ -19,6 +19,7 @@ export const GraphCanvas = memo(function GraphCanvas({ width, height, colors, + appearance, highlightDocumentIds, selectedNodeId = null, onNodeHover, @@ -62,6 +63,7 @@ export const GraphCanvas = memo(function GraphCanvas({ width, height, colors, + appearance, selectedNodeId, hoveredNodeId: null as string | null, highlightIds: new Set(highlightDocumentIds ?? []), @@ -75,6 +77,7 @@ export const GraphCanvas = memo(function GraphCanvas({ s.current.width = width s.current.height = height s.current.colors = colors + s.current.appearance = appearance // Stable callback refs so InputHandler never needs recreation const cb = useRef({ @@ -128,8 +131,18 @@ export const GraphCanvas = memo(function GraphCanvas({ useEffect(() => { s.current.selectedNodeId = selectedNodeId s.current.dimTarget = selectedNodeId ? 1 : 0 + s.current.colors = colors + s.current.appearance = { + documentNodes: appearance?.documentNodes, + connections: appearance?.connections, + } renderNeeded.current = true - }, [selectedNodeId]) + }, [ + selectedNodeId, + colors, + appearance?.documentNodes, + appearance?.connections, + ]) // Create viewport + input handler (once per variant) // biome-ignore lint/correctness/useExhaustiveDependencies: canvasRef and externalViewportRef are refs — mutations do not trigger re-renders, intentionally omitted @@ -278,6 +291,7 @@ export const GraphCanvas = memo(function GraphCanvas({ hoveredNodeId: cur.hoveredNodeId, highlightIds: cur.highlightIds, dimProgress: cur.dimProgress, + appearance: cur.appearance, }, nodeMapRef.current, cur.colors, diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx index c65073f4..acf69c39 100644 --- a/packages/memory-graph/src/components/legend.tsx +++ b/packages/memory-graph/src/components/legend.tsx @@ -532,7 +532,9 @@ export const Legend = memo(function Legend({
- + {labels.documentSourceEdge} diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 016d9428..b62b10d9 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -18,6 +18,8 @@ import { useGraphTheme } from "../hooks/use-graph-theme" import { useInitialGraphFit } from "../hooks/use-initial-graph-fit" import type { GraphApiDocument, + GraphNode, + MemoryNodeData, GraphThemeColors, MemoryGraphProps, ResolvedMemoryGraphLabels, @@ -28,6 +30,31 @@ import { LoadingIndicator } from "./loading-indicator" import { NavigationControls } from "./navigation-controls" import { NodeHoverPopover } from "./node-hover-popover" +function documentIdForNode( + node: GraphNode | null, + documents: GraphApiDocument[], +): string | null { + if (!node || typeof node.data.id !== "string" || !node.data.id.trim()) + return null + if (node.type === "document") { + return documents.some((document) => document.id === node.data.id) + ? node.data.id + : null + } + const sourceId = (node.data as MemoryNodeData).documentId + if (typeof sourceId !== "string" || !sourceId.trim()) return null + const ids = new Set( + documents + .filter( + (document) => + document.id.trim() && + document.memories.some((memory) => memory.id === node.data.id), + ) + .map((document) => document.id), + ) + return ids.size === 1 && ids.has(sourceId) ? sourceId : null +} + export function MemoryGraph({ documents = [], isLoading: externalIsLoading = false, @@ -46,9 +73,11 @@ export function MemoryGraph({ canvasRef: externalCanvasRef, colors: colorOverrides, totalCount, - onOpenDocument, + onOpenDocument: externalOnOpenDocument, labels: labelOverrides, layering, + appearance, + openDocumentOnEnter = false, }: MemoryGraphProps) { const resolvedLabels = useMemo( () => ({ ...DEFAULT_LABELS, ...labelOverrides }), @@ -358,9 +387,38 @@ export function MemoryGraph({ vp.zoomTo(vp.zoom / 1.3, containerSize.width / 2, graphFitHeight / 2) }, [containerSize.width, graphFitHeight, stopFollowing]) - // Wrap onOpenDocument to dismiss the popover before opening the modal. - // Without this, the popover overlay stays mounted on top of the - // document modal, obscuring it and intercepting clicks. + const activeDocumentId = useMemo( + () => + openDocumentOnEnter + ? documentIdForNode( + nodes.find((node) => node.id === (selectedNode ?? hoveredNode)) ?? + null, + documents, + ) + : null, + [openDocumentOnEnter, selectedNode, hoveredNode, nodes, documents], + ) + const onOpenDocument = useMemo( + () => + !openDocumentOnEnter + ? externalOnOpenDocument + : externalOnOpenDocument && activeDocumentId + ? (_documentId: string) => externalOnOpenDocument(activeDocumentId) + : undefined, + [openDocumentOnEnter, externalOnOpenDocument, activeDocumentId], + ) + const selectedDocumentId = useMemo( + () => + openDocumentOnEnter + ? documentIdForNode( + nodes.find((node) => node.id === selectedNode) ?? null, + documents, + ) + : null, + [openDocumentOnEnter, selectedNode, nodes, documents], + ) + const canOpenOnEnter = openDocumentOnEnter && !!externalOnOpenDocument + const handleOpenDocument = useCallback( (documentId: string) => { setSelectedNode(null) @@ -370,11 +428,53 @@ export function MemoryGraph({ [onOpenDocument], ) - // Keyboard shortcuts — using useEffect with keydown listener + const documentNavigationProps = canOpenOnEnter + ? { + tabIndex: 0, + role: "region" as const, + "aria-label": + "Memory graph. Select a node, then press Enter to open its document.", + onKeyDown: (event: React.KeyboardEvent) => { + if ( + !selectedDocumentId || + event.key !== "Enter" || + event.defaultPrevented || + event.repeat || + event.nativeEvent.isComposing || + event.ctrlKey || + event.metaKey || + event.altKey || + event.shiftKey + ) + return + if ( + event.target !== event.currentTarget && + !(event.target instanceof HTMLCanvasElement) + ) + return + event.preventDefault() + event.stopPropagation() + handleOpenDocument(selectedDocumentId) + }, + } + : {} + useEffect(() => { const handler = (e: KeyboardEvent) => { - // Don't handle if user is typing in an input const target = e.target as HTMLElement + if ( + openDocumentOnEnter && + (e.defaultPrevented || + e.isComposing || + e.ctrlKey || + e.metaKey || + e.altKey || + (target instanceof Element && + target.closest( + "input, textarea, select, [contenteditable], [role=textbox]", + ))) + ) + return if ( target.tagName === "INPUT" || target.tagName === "TEXTAREA" || @@ -407,7 +507,13 @@ export function MemoryGraph({ window.addEventListener("keydown", handler) return () => window.removeEventListener("keydown", handler) - }, [handleAutoFit, handleCenter, handleZoomIn, handleZoomOut]) + }, [ + handleAutoFit, + handleCenter, + handleZoomIn, + handleZoomOut, + openDocumentOnEnter, + ]) // Arrow key navigation through nodes const selectAndCenter = useCallback( @@ -523,6 +629,20 @@ export function MemoryGraph({ useEffect(() => { const handler = (e: KeyboardEvent) => { const target = e.target as HTMLElement + if ( + openDocumentOnEnter && + (e.defaultPrevented || + e.isComposing || + e.ctrlKey || + e.metaKey || + e.altKey || + e.shiftKey || + (target instanceof Element && + target.closest( + "input, textarea, select, [contenteditable], [role=textbox]", + ))) + ) + return if ( target.tagName === "INPUT" || target.tagName === "TEXTAREA" || @@ -552,7 +672,13 @@ export function MemoryGraph({ window.addEventListener("keydown", handler) return () => window.removeEventListener("keydown", handler) - }, [navigateUp, navigateDown, navigateNext, navigatePrev]) + }, [ + navigateUp, + navigateDown, + navigateNext, + navigatePrev, + openDocumentOnEnter, + ]) // Slideshow — use refs to avoid re-creating the interval on resize or node updates const nodesRef = useRef(nodes) @@ -728,8 +854,13 @@ export function MemoryGraph({
{ - if (event.target instanceof HTMLCanvasElement) stopFollowing() + if (event.target instanceof HTMLCanvasElement) { + stopFollowing() + if (canOpenOnEnter) + containerRef.current?.focus({ preventScroll: true }) + } }} onWheelCapture={(event) => { if (event.target instanceof HTMLCanvasElement) stopFollowing() @@ -737,6 +868,7 @@ export function MemoryGraph({ > {hasContainerSize && ( void onSelectNode?: (nodeId: string) => void onOpenDocument?: (documentId: string) => void + openDocumentShortcut?: string + showDocumentTitle?: boolean } function useCopyToClipboard(timeout = 2000) { @@ -105,11 +107,13 @@ function NavButton({ label, onClick, colors, + shortcut, }: { icon: React.ReactNode label: string onClick?: () => void colors: GraphThemeColors + shortcut?: string }) { const buttonStyle: React.CSSProperties = { display: "flex", @@ -130,6 +134,8 @@ function NavButton({ return ( ) } @@ -322,6 +342,8 @@ export const NodeHoverPopover = memo( onNavigateDown, onSelectNode, onOpenDocument, + openDocumentShortcut, + showDocumentTitle = false, }) { const CARD_W = 280 const SHORTCUTS_W = 160 @@ -532,6 +554,18 @@ export const NodeHoverPopover = memo( /> ) : (
+ {showDocumentTitle && docData && ( +
+ {docData.title || "Untitled document"} +
+ )}

{content || "No content"}

)} @@ -632,7 +666,7 @@ export const NodeHoverPopover = memo( )}
@@ -644,6 +678,7 @@ export const NodeHoverPopover = memo( colors={colors} icon={} label={labels.viewDocument} + shortcut={openDocumentShortcut} onClick={() => onOpenDocument(documentId)} /> )} diff --git a/packages/memory-graph/src/constants.ts b/packages/memory-graph/src/constants.ts index f3c539ff..1267b964 100644 --- a/packages/memory-graph/src/constants.ts +++ b/packages/memory-graph/src/constants.ts @@ -61,7 +61,7 @@ export const ANIMATION = { dimDuration: 1500, } -export const DEFAULT_COLORS: GraphThemeColors = { +export const DEFAULT_COLORS: GraphThemeColors & { edgeDocument: string } = { bg: "#0f1419", docFill: "#1B1F24", docStroke: "#2A2F36", diff --git a/packages/memory-graph/src/index.tsx b/packages/memory-graph/src/index.tsx index f87baaba..bb3b3c6b 100644 --- a/packages/memory-graph/src/index.tsx +++ b/packages/memory-graph/src/index.tsx @@ -26,6 +26,7 @@ export type { MemoryGraphProps, MemoryGraphLabels, MemoryGraphLayering, + GraphAppearance, ResolvedMemoryGraphLabels, GraphNode, GraphEdge, diff --git a/packages/memory-graph/src/types.ts b/packages/memory-graph/src/types.ts index 1c66419e..6a53f78c 100644 --- a/packages/memory-graph/src/types.ts +++ b/packages/memory-graph/src/types.ts @@ -118,7 +118,7 @@ export interface GraphThemeColors { textPrimary: string textSecondary: string textMuted: string - edgeDocument: string + edgeDocument?: string edgeDerives: string edgeUpdates: string edgeExtends: string @@ -157,12 +157,18 @@ export interface MemoryGraphLayering { hoverPopoverZIndex?: number } +export interface GraphAppearance { + documentNodes?: "icons" | "stacked" + connections?: "default" | "emphasized" +} + export interface GraphCanvasProps { nodes: GraphNode[] edges: GraphEdge[] width: number height: number colors: GraphThemeColors + appearance?: GraphAppearance highlightDocumentIds?: string[] selectedNodeId?: string | null onNodeHover: (nodeId: string | null) => void @@ -224,6 +230,8 @@ export interface MemoryGraphProps { labels?: MemoryGraphLabels /** Overlay layering controls */ layering?: MemoryGraphLayering + appearance?: GraphAppearance + openDocumentOnEnter?: boolean } export interface ChainEntry {