aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-01-22 11:16:08 +0530
committerShoubhit Dash <[email protected]>2026-01-22 11:16:08 +0530
commitef54b5805cf610909131724425fb648ef758fd17 (patch)
tree95edacf1f15768b5ae0cb085bfff21aa946d81c1 /apps/web/components
parentnew graph (diff)
downloadsupermemory-01-16-new_graph.tar.xz
supermemory-01-16-new_graph.zip
graph changes01-16-new_graph
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/graph/constants.ts58
-rw-r--r--apps/web/components/graph/graph-canvas.tsx318
-rw-r--r--apps/web/components/graph/graph.tsx1
-rw-r--r--apps/web/components/graph/legend.tsx302
-rw-r--r--apps/web/components/graph/loading-indicator.tsx4
-rw-r--r--apps/web/components/graph/navigation-controls.tsx99
-rw-r--r--apps/web/components/graph/node-popover.tsx246
-rw-r--r--apps/web/components/graph/types.ts1
8 files changed, 486 insertions, 543 deletions
diff --git a/apps/web/components/graph/constants.ts b/apps/web/components/graph/constants.ts
index 15dbe9b8..8383b6ff 100644
--- a/apps/web/components/graph/constants.ts
+++ b/apps/web/components/graph/constants.ts
@@ -1,38 +1,50 @@
export const colors = {
background: {
- primary: "#0f1419",
- secondary: "#1a1f29",
- accent: "#252a35",
+ primary: "#000B1B",
+ secondary: "#01173C",
+ accent: "#0A2351",
},
+ // All nodes are now hexagons with cyan color
+ node: {
+ primary: "rgba(0, 180, 216, 0.25)",
+ secondary: "rgba(0, 180, 216, 0.35)",
+ accent: "rgba(0, 180, 216, 0.45)",
+ border: "rgba(0, 180, 216, 0.7)",
+ glow: "rgba(0, 180, 216, 0.5)",
+ },
+ // Legacy document colors (for compatibility)
document: {
- primary: "rgba(255, 255, 255, 0.21)",
- secondary: "rgba(255, 255, 255, 0.31)",
- accent: "rgba(255, 255, 255, 0.31)",
- border: "rgba(255, 255, 255, 0.6)",
- glow: "rgba(147, 197, 253, 0.4)",
+ primary: "rgba(0, 180, 216, 0.25)",
+ secondary: "rgba(0, 180, 216, 0.35)",
+ accent: "rgba(0, 180, 216, 0.45)",
+ border: "rgba(0, 180, 216, 0.7)",
+ glow: "rgba(0, 180, 216, 0.5)",
},
+ // Legacy memory colors (for compatibility)
memory: {
- primary: "rgba(147, 196, 253, 0.21)",
- secondary: "rgba(147, 196, 253, 0.31)",
- accent: "rgba(147, 197, 253, 0.31)",
- border: "rgba(147, 196, 253, 0.6)",
- glow: "rgba(147, 197, 253, 0.5)",
+ primary: "rgba(0, 180, 216, 0.25)",
+ secondary: "rgba(0, 180, 216, 0.35)",
+ accent: "rgba(0, 180, 216, 0.45)",
+ border: "rgba(0, 180, 216, 0.7)",
+ glow: "rgba(0, 180, 216, 0.5)",
},
connection: {
- weak: "rgba(35, 189, 255, 0.3)",
- memory: "rgba(148, 163, 184, 0.35)",
- medium: "rgba(35, 189, 255, 0.6)",
- strong: "rgba(35, 189, 255, 0.9)",
+ weak: "rgba(0, 180, 216, 0.3)",
+ memory: "rgba(0, 180, 216, 0.4)",
+ medium: "rgba(0, 180, 216, 0.6)",
+ strong: "rgba(0, 180, 216, 0.9)",
+ // Pink/magenta for special relationships
+ relation: "rgba(236, 72, 153, 0.6)",
},
text: {
primary: "#ffffff",
secondary: "#e2e8f0",
- muted: "#94a3b8",
+ muted: "#525D6E",
},
accent: {
- primary: "rgba(59, 130, 246, 0.7)",
- secondary: "rgba(99, 102, 241, 0.6)",
- glow: "rgba(147, 197, 253, 0.6)",
+ primary: "rgba(0, 180, 216, 0.7)",
+ secondary: "rgba(34, 97, 202, 0.6)",
+ glow: "rgba(0, 180, 216, 0.6)",
amber: "rgba(251, 165, 36, 0.8)",
emerald: "rgba(16, 185, 129, 0.4)",
},
@@ -42,9 +54,9 @@ export const colors = {
new: "rgba(16, 185, 129, 0.4)",
},
relations: {
- updates: "rgba(147, 77, 253, 0.5)",
+ updates: "rgba(236, 72, 153, 0.5)",
extends: "rgba(16, 185, 129, 0.5)",
- derives: "rgba(147, 197, 253, 0.5)",
+ derives: "rgba(0, 180, 216, 0.5)",
},
}
diff --git a/apps/web/components/graph/graph-canvas.tsx b/apps/web/components/graph/graph-canvas.tsx
index f78a9b25..634cdfad 100644
--- a/apps/web/components/graph/graph-canvas.tsx
+++ b/apps/web/components/graph/graph-canvas.tsx
@@ -138,28 +138,14 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const screenY = node.y * zoom + panY
const nodeSize = node.size * zoom
- if (node.type === "document") {
- const docWidth = nodeSize * 1.4
- const docHeight = nodeSize * 0.9
- const halfW = docWidth / 2
- const halfH = docHeight / 2
-
- if (
- x >= screenX - halfW &&
- x <= screenX + halfW &&
- y >= screenY - halfH &&
- y <= screenY + halfH
- ) {
- return node.id
- }
- } else {
- const dx = x - screenX
- const dy = y - screenY
- const distance = Math.sqrt(dx * dx + dy * dy)
-
- if (distance <= nodeSize / 2) {
- return node.id
- }
+ // All nodes are hexagons now, use circular hit detection
+ const radius = node.type === "document" ? nodeSize * 0.6 : nodeSize / 2
+ const dx = x - screenX
+ const dy = y - screenY
+ const distance = Math.sqrt(dx * dx + dy * dy)
+
+ if (distance <= radius) {
+ return node.id
}
}
}
@@ -227,23 +213,20 @@ export const GraphCanvas = memo<GraphCanvasProps>(
ctx.clearRect(0, 0, width, height)
- ctx.strokeStyle = "rgba(148, 163, 184, 0.03)"
- ctx.lineWidth = 1
- const gridSpacing = 100 * zoom
- const offsetX = panX % gridSpacing
- const offsetY = panY % gridSpacing
-
- for (let x = offsetX; x < width; x += gridSpacing) {
- ctx.beginPath()
- ctx.moveTo(x, 0)
- ctx.lineTo(x, height)
- ctx.stroke()
- }
- for (let y = offsetY; y < height; y += gridSpacing) {
- ctx.beginPath()
- ctx.moveTo(0, y)
- ctx.lineTo(width, y)
- ctx.stroke()
+ // Draw dotted background pattern
+ const dotSpacing = 30 * Math.max(zoom, 0.5) // Maintain reasonable spacing
+ const offsetX = panX % dotSpacing
+ const offsetY = panY % dotSpacing
+ const dotRadius = 1
+
+ ctx.fillStyle = "rgba(0, 180, 216, 0.15)"
+
+ for (let x = offsetX; x < width; x += dotSpacing) {
+ for (let y = offsetY; y < height; y += dotSpacing) {
+ ctx.beginPath()
+ ctx.arc(x, y, dotRadius, 0, 2 * Math.PI)
+ ctx.fill()
+ }
}
ctx.lineCap = "round"
@@ -420,115 +403,71 @@ export const GraphCanvas = memo<GraphCanvasProps>(
return highlightSet.has(doc.id)
})()
- if (node.type === "document") {
- const docWidth = nodeSize * 1.4
- const docHeight = nodeSize * 0.9
-
- ctx.fillStyle = isHovered
- ? colors.document.secondary
- : colors.document.primary
- ctx.globalAlpha = nodeOpacity
-
- ctx.strokeStyle = isHovered
- ? colors.document.accent
- : colors.document.border
- ctx.lineWidth = isHovered ? 2 : 1
+ // All nodes are now rendered as hexagons
+ const isDocument = node.type === "document"
+ const isMemory = node.type === "memory"
- const radius = useSimplifiedRendering ? 6 : 12
- ctx.beginPath()
- ctx.roundRect(
- screenX - docWidth / 2,
- screenY - docHeight / 2,
- docWidth,
- docHeight,
- radius,
- )
- ctx.fill()
- ctx.stroke()
+ // Get node data for status checks
+ let isNew = false
+ if (isMemory) {
+ const mem = node.data as ViewportMemoryEntry
+ isNew = new Date(mem.createdAt).getTime() > Date.now() - 1000 * 60 * 60 * 24
+ }
- if (!useSimplifiedRendering && isHovered) {
- ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"
- ctx.lineWidth = 1
- ctx.beginPath()
- ctx.roundRect(
- screenX - docWidth / 2 + 1,
- screenY - docHeight / 2 + 1,
- docWidth - 2,
- docHeight - 2,
- radius - 1,
- )
- ctx.stroke()
- }
+ // Use consistent node colors for all nodes
+ let fillColor = colors.node.primary
+ let borderColor = colors.node.border
- if (isHighlightedDocument) {
- ctx.save()
- ctx.globalAlpha = 0.9
- ctx.strokeStyle = colors.accent.primary
- ctx.lineWidth = 3
- ctx.setLineDash([6, 4])
- const avgDimension = (docWidth + docHeight) / 2
- const ringPadding = avgDimension * 0.1
- ctx.beginPath()
- ctx.roundRect(
- screenX - docWidth / 2 - ringPadding,
- screenY - docHeight / 2 - ringPadding,
- docWidth + ringPadding * 2,
- docHeight + ringPadding * 2,
- radius + 6,
- )
- ctx.stroke()
- ctx.setLineDash([])
- ctx.restore()
- }
+ if (isHovered) {
+ fillColor = colors.node.secondary
+ }
- if (!useSimplifiedRendering) {
- const doc = node.data as ViewportDocument
- const iconSize = docHeight * 0.4
-
- drawDocumentIcon(
- ctx,
- screenX,
- screenY,
- iconSize,
- doc.type || "text",
- "rgba(255, 255, 255, 0.8)",
- )
- }
- } else {
- const mem = node.data as ViewportMemoryEntry
- const isNew =
- new Date(mem.createdAt).getTime() > Date.now() - 1000 * 60 * 60 * 24
+ if (isNew) {
+ borderColor = colors.status.new
+ }
- let fillColor = colors.memory.primary
- let borderColor = colors.memory.border
+ // Document nodes are slightly larger
+ const radius = isDocument ? nodeSize * 0.6 : nodeSize / 2
- if (isHovered) {
- fillColor = colors.memory.secondary
- }
+ ctx.fillStyle = fillColor
+ ctx.globalAlpha = shouldDim ? nodeOpacity : 1
+ ctx.strokeStyle = borderColor
+ ctx.lineWidth = isHovered ? 2 : 1.5
- if (isNew) {
- borderColor = colors.status.new
+ if (useSimplifiedRendering) {
+ // Simple circle for low zoom
+ ctx.beginPath()
+ ctx.arc(screenX, screenY, radius, 0, 2 * Math.PI)
+ ctx.fill()
+ ctx.stroke()
+ } else {
+ // Hexagon for normal zoom
+ const sides = 6
+ ctx.beginPath()
+ for (let i = 0; i < sides; i++) {
+ const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
+ const x = screenX + radius * Math.cos(angle)
+ const y = screenY + radius * Math.sin(angle)
+ if (i === 0) {
+ ctx.moveTo(x, y)
+ } else {
+ ctx.lineTo(x, y)
+ }
}
+ ctx.closePath()
+ ctx.fill()
+ ctx.stroke()
- const radius = nodeSize / 2
-
- ctx.fillStyle = fillColor
- ctx.globalAlpha = shouldDim ? nodeOpacity : 1
- ctx.strokeStyle = borderColor
- ctx.lineWidth = isHovered ? 2 : 1.5
-
- if (useSimplifiedRendering) {
- ctx.beginPath()
- ctx.arc(screenX, screenY, radius, 0, 2 * Math.PI)
- ctx.fill()
- ctx.stroke()
- } else {
- const sides = 6
+ // Inner highlight on hover
+ if (isHovered) {
+ ctx.strokeStyle = "rgba(0, 180, 216, 0.3)"
+ ctx.lineWidth = 1
+ const innerRadius = radius - 2
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
- const x = screenX + radius * Math.cos(angle)
- const y = screenY + radius * Math.sin(angle)
+ const x = screenX + innerRadius * Math.cos(angle)
+ const y = screenY + innerRadius * Math.sin(angle)
if (i === 0) {
ctx.moveTo(x, y)
} else {
@@ -536,80 +475,73 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}
}
ctx.closePath()
- ctx.fill()
ctx.stroke()
+ }
+ }
- if (isHovered) {
- ctx.strokeStyle = "rgba(147, 197, 253, 0.3)"
- ctx.lineWidth = 1
- const innerRadius = radius - 2
- ctx.beginPath()
- for (let i = 0; i < sides; i++) {
- const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
- const x = screenX + innerRadius * Math.cos(angle)
- const y = screenY + innerRadius * Math.sin(angle)
- if (i === 0) {
- ctx.moveTo(x, y)
- } else {
- ctx.lineTo(x, y)
- }
- }
- ctx.closePath()
- ctx.stroke()
+ // Highlighted document indicator
+ if (isHighlightedDocument) {
+ ctx.save()
+ ctx.globalAlpha = 0.9
+ ctx.strokeStyle = colors.accent.primary
+ ctx.lineWidth = 3
+ ctx.setLineDash([6, 4])
+ const highlightRadius = radius * 1.2
+ const sides = 6
+ ctx.beginPath()
+ for (let i = 0; i < sides; i++) {
+ const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
+ const x = screenX + highlightRadius * Math.cos(angle)
+ const y = screenY + highlightRadius * Math.sin(angle)
+ if (i === 0) {
+ ctx.moveTo(x, y)
+ } else {
+ ctx.lineTo(x, y)
}
}
+ ctx.closePath()
+ ctx.stroke()
+ ctx.setLineDash([])
+ ctx.restore()
+ }
- if (isNew) {
- ctx.fillStyle = colors.status.new
- ctx.beginPath()
- ctx.arc(
- screenX + nodeSize * 0.25,
- screenY - nodeSize * 0.25,
- Math.max(2, nodeSize * 0.15),
- 0,
- 2 * Math.PI,
- )
- ctx.fill()
- }
+ // New item indicator dot
+ if (isNew) {
+ ctx.fillStyle = colors.status.new
+ ctx.beginPath()
+ ctx.arc(
+ screenX + nodeSize * 0.25,
+ screenY - nodeSize * 0.25,
+ Math.max(2, nodeSize * 0.15),
+ 0,
+ 2 * Math.PI,
+ )
+ ctx.fill()
}
+ // Unified glow effect for all nodes (hexagon)
if (!useSimplifiedRendering && isHovered) {
- const glowColor =
- node.type === "document" ? colors.document.glow : colors.memory.glow
+ const glowColor = colors.node.glow
ctx.strokeStyle = glowColor
ctx.lineWidth = 1
ctx.setLineDash([3, 3])
ctx.globalAlpha = 0.6
+ const glowRadius = (node.type === "document" ? nodeSize * 0.6 : nodeSize / 2) * 1.3
+ const sides = 6
ctx.beginPath()
- if (node.type === "document") {
- const docWidth = nodeSize * 1.4
- const docHeight = nodeSize * 0.9
- const avgDimension = (docWidth + docHeight) / 2
- const glowPadding = avgDimension * 0.1
- ctx.roundRect(
- screenX - docWidth / 2 - glowPadding,
- screenY - docHeight / 2 - glowPadding,
- docWidth + glowPadding * 2,
- docHeight + glowPadding * 2,
- 15,
- )
- } else {
- const glowRadius = nodeSize * 0.7
- const sides = 6
- for (let i = 0; i < sides; i++) {
- const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
- const x = screenX + glowRadius * Math.cos(angle)
- const y = screenY + glowRadius * Math.sin(angle)
- if (i === 0) {
- ctx.moveTo(x, y)
- } else {
- ctx.lineTo(x, y)
- }
+ for (let i = 0; i < sides; i++) {
+ const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
+ const x = screenX + glowRadius * Math.cos(angle)
+ const y = screenY + glowRadius * Math.sin(angle)
+ if (i === 0) {
+ ctx.moveTo(x, y)
+ } else {
+ ctx.lineTo(x, y)
}
- ctx.closePath()
}
+ ctx.closePath()
ctx.stroke()
ctx.setLineDash([])
}
diff --git a/apps/web/components/graph/graph.tsx b/apps/web/components/graph/graph.tsx
index ed5542e2..a9e7e73e 100644
--- a/apps/web/components/graph/graph.tsx
+++ b/apps/web/components/graph/graph.tsx
@@ -436,6 +436,7 @@ export function Graph({ containerTags, children }: GraphProps) {
isTimelineActive={isTimelineStreaming}
timelineProgress={timelineProgress}
nodes={nodes}
+ zoom={zoom}
/>
)}
</div>
diff --git a/apps/web/components/graph/legend.tsx b/apps/web/components/graph/legend.tsx
index ca04f1e5..9410f33b 100644
--- a/apps/web/components/graph/legend.tsx
+++ b/apps/web/components/graph/legend.tsx
@@ -1,8 +1,7 @@
"use client"
-import { Brain, ChevronDown, ChevronUp, FileText } from "lucide-react"
+import { ChevronUp, ChevronDown, Settings } from "lucide-react"
import { memo, useEffect, useState } from "react"
-import { colors } from "./constants"
import type { LegendProps } from "./types"
const setCookie = (name: string, value: string, days = 365) => {
@@ -25,22 +24,51 @@ const getCookie = (name: string): string | null => {
return null
}
+// Toggle switch component
+function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) {
+ return (
+ <button
+ type="button"
+ onClick={() => onChange(!enabled)}
+ className={`relative w-11 h-6 rounded-full transition-colors ${
+ enabled ? "bg-blue-600" : "bg-[#1E3A5F]"
+ }`}
+ >
+ <span
+ className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${
+ enabled ? "left-6" : "left-1"
+ }`}
+ />
+ </button>
+ )
+}
+
export const Legend = memo(function Legend({
id,
nodes = [],
edges = [],
isLoading = false,
}: LegendProps) {
- const [isExpanded, setIsExpanded] = useState(true)
+ const [isExpanded, setIsExpanded] = useState(false)
const [isInitialized, setIsInitialized] = useState(false)
+ const [connectionsExpanded, setConnectionsExpanded] = useState(true)
+
+ // Toggle states for filtering
+ const [showDocMemory, setShowDocMemory] = useState(true)
+ const [showDocSimilarity, setShowDocSimilarity] = useState(true)
+ const [showUpdates, setShowUpdates] = useState(false)
+ const [showExtends, setShowExtends] = useState(true)
+ const [showInferences, setShowInferences] = useState(false)
+ const [showStrong, setShowStrong] = useState(true)
+ const [showWeak, setShowWeak] = useState(true)
useEffect(() => {
if (!isInitialized) {
const savedState = getCookie("legendCollapsed")
- if (savedState === "true") {
- setIsExpanded(false)
- } else if (savedState === "false") {
+ if (savedState === "false") {
setIsExpanded(true)
+ } else {
+ setIsExpanded(false)
}
setIsInitialized(true)
}
@@ -54,124 +82,202 @@ export const Legend = memo(function Legend({
const memoryCount = nodes.filter((n) => n.type === "memory").length
const documentCount = nodes.filter((n) => n.type === "document").length
+ const connectionCount = edges.length
return (
- <div
- id={id}
- className="absolute bottom-4 right-4 z-10 bg-white/5 backdrop-blur-xl border border-white/20 rounded-xl overflow-hidden"
- >
- <div className="p-3">
- {!isExpanded && (
- <button
- type="button"
- onClick={handleToggleExpanded}
- className="flex items-center gap-2 text-white/70 hover:text-white transition-colors"
- >
- <span className="text-sm font-medium">?</span>
- <ChevronUp className="w-4 h-4" />
- </button>
- )}
+ <>
+ {/* Settings button - Bottom right */}
+ <button
+ type="button"
+ className="absolute bottom-4 right-4 z-10 flex items-center justify-center w-10 h-10 text-white/70 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg hover:bg-[#0A1628] hover:text-white transition-colors"
+ title="Settings"
+ >
+ <Settings className="w-5 h-5" />
+ </button>
+ {/* Legend - Positioned at bottom left */}
+ <div
+ id={id}
+ className="absolute bottom-4 left-4 z-10"
+ >
+ {/* Legend expanded content - appears above the button */}
{isExpanded && (
- <>
- <div className="flex items-center justify-between mb-3">
- <span className="text-sm font-medium text-white">Legend</span>
- <button
- type="button"
- onClick={handleToggleExpanded}
- className="text-white/60 hover:text-white transition-colors"
- >
- <ChevronDown className="w-4 h-4" />
- </button>
- </div>
+ <div className="mb-2 bg-[#0A1628]/95 backdrop-blur-xl border border-[#1E3A5F] rounded-xl overflow-hidden min-w-[280px]">
+ {/* Header */}
+ <button
+ type="button"
+ onClick={handleToggleExpanded}
+ className="w-full flex items-center gap-2 px-4 py-3 text-white font-medium hover:bg-white/5 transition-colors"
+ >
+ <ChevronUp className="w-4 h-4" />
+ <span>Legend</span>
+ </button>
- <div className="space-y-4">
- {!isLoading && (
- <div>
- <div className="text-xs text-white/50 mb-2">Statistics</div>
- <div className="space-y-1.5">
- <div className="flex items-center gap-2">
- <Brain className="w-3.5 h-3.5 text-blue-400" />
- <span className="text-xs text-white/70">
- {memoryCount} memories
- </span>
- </div>
- <div className="flex items-center gap-2">
- <FileText className="w-3.5 h-3.5 text-slate-300" />
- <span className="text-xs text-white/70">
- {documentCount} documents
- </span>
- </div>
- <div className="flex items-center gap-2">
- <div className="w-3.5 h-3.5 rounded-full bg-gradient-to-r from-blue-400 to-purple-400" />
- <span className="text-xs text-white/70">
- {edges.length} connections
- </span>
+ <div className="px-4 pb-4">
+ {/* STATISTICS */}
+ <div className="mb-4">
+ <div className="text-xs text-[#525D6E] uppercase tracking-wider mb-3">
+ Statistics
+ </div>
+ <div className="space-y-2">
+ {/* Memories */}
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div
+ className="w-4 h-4"
+ style={{
+ clipPath:
+ "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
+ backgroundColor: "rgba(0, 180, 216, 0.4)",
+ border: "1px solid rgba(0, 180, 216, 0.6)",
+ }}
+ />
+ <span className="text-white/90">Memories</span>
+ <ChevronDown className="w-3 h-3 text-white/50" />
</div>
+ <span className="text-white/60">{memoryCount}</span>
</div>
- </div>
- )}
- <div>
- <div className="text-xs text-white/50 mb-2">Nodes</div>
- <div className="space-y-1.5">
- <div className="flex items-center gap-2">
- <div className="w-4 h-3 rounded bg-white/20 border border-white/40" />
- <span className="text-xs text-white/70">Document</span>
+ {/* Documents */}
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-4 h-4 rounded-sm bg-[#1E3A5F] border border-[#2E4A6F]" />
+ <span className="text-white/90">Documents</span>
+ <ChevronDown className="w-3 h-3 text-white/50" />
+ </div>
+ <span className="text-white/60">{documentCount}</span>
</div>
- <div className="flex items-center gap-2">
- <div
- className="w-3.5 h-3.5"
- style={{
- clipPath:
- "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
- backgroundColor: "rgba(147, 196, 253, 0.4)",
- border: "1px solid rgba(147, 196, 253, 0.6)",
- }}
- />
- <span className="text-xs text-white/70">Memory</span>
+
+ {/* Connections */}
+ <div>
+ <button
+ type="button"
+ onClick={() => setConnectionsExpanded(!connectionsExpanded)}
+ className="w-full flex items-center justify-between"
+ >
+ <div className="flex items-center gap-3">
+ <svg
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ className="text-white/70"
+ >
+ <circle cx="18" cy="5" r="3" />
+ <circle cx="6" cy="12" r="3" />
+ <circle cx="18" cy="19" r="3" />
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
+ </svg>
+ <span className="text-white/90">Connections</span>
+ {connectionsExpanded ? (
+ <ChevronUp className="w-3 h-3 text-white/50" />
+ ) : (
+ <ChevronDown className="w-3 h-3 text-white/50" />
+ )}
+ </div>
+ <span className="text-white/60">{connectionCount}</span>
+ </button>
+
+ {/* Expanded connections */}
+ {connectionsExpanded && (
+ <div className="ml-7 mt-2 space-y-2">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-4 h-0.5 bg-cyan-500/60" />
+ <span className="text-white/70 text-sm">Doc &gt; Memory</span>
+ </div>
+ </div>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div
+ className="w-4 h-0.5"
+ style={{
+ background:
+ "repeating-linear-gradient(90deg, rgba(0, 180, 216, 0.6) 0px, rgba(0, 180, 216, 0.6) 2px, transparent 2px, transparent 4px)",
+ }}
+ />
+ <span className="text-white/70 text-sm">Doc similarity</span>
+ </div>
+ <Toggle enabled={showDocSimilarity} onChange={setShowDocSimilarity} />
+ </div>
+ </div>
+ )}
</div>
</div>
</div>
- <div>
- <div className="text-xs text-white/50 mb-2">Connections</div>
- <div className="space-y-1.5">
- <div className="flex items-center gap-2">
- <div className="w-4 h-0.5 bg-slate-400/40" />
- <span className="text-xs text-white/70">Doc → Memory</span>
+ {/* RELATIONS */}
+ <div className="mb-4">
+ <div className="text-xs text-[#525D6E] uppercase tracking-wider mb-3">
+ Relations
+ </div>
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-4 h-0.5 bg-pink-500" />
+ <span className="text-white/90">Updates</span>
+ </div>
+ <Toggle enabled={showUpdates} onChange={setShowUpdates} />
</div>
- <div className="flex items-center gap-2">
- <div
- className="w-4 h-0.5"
- style={{
- background:
- "repeating-linear-gradient(90deg, rgba(35, 189, 255, 0.6) 0px, rgba(35, 189, 255, 0.6) 3px, transparent 3px, transparent 6px)",
- }}
- />
- <span className="text-xs text-white/70">Doc similarity</span>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-4 h-0.5 bg-emerald-500" />
+ <span className="text-white/90">Extends</span>
+ </div>
+ <Toggle enabled={showExtends} onChange={setShowExtends} />
+ </div>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-4 h-0.5 bg-cyan-500" />
+ <span className="text-white/90">Inferences</span>
+ </div>
+ <Toggle enabled={showInferences} onChange={setShowInferences} />
</div>
</div>
</div>
+ {/* SIMILARITY */}
<div>
- <div className="text-xs text-white/50 mb-2">Similarity</div>
- <div className="space-y-1.5">
- <div className="flex items-center gap-2">
- <div className="w-4 h-0.5 bg-cyan-500/30" />
- <span className="text-xs text-white/70">Weak</span>
+ <div className="text-xs text-[#525D6E] uppercase tracking-wider mb-3">
+ Similarity
+ </div>
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-[#1E3A5F] border border-[#3E5A7F]" />
+ <span className="text-white/90">Strong</span>
+ </div>
+ <Toggle enabled={showStrong} onChange={setShowStrong} />
</div>
- <div className="flex items-center gap-2">
- <div className="w-4 h-1 bg-cyan-400/90" />
- <span className="text-xs text-white/70">Strong</span>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="w-3 h-3 rounded-full bg-[#1E3A5F] border border-[#3E5A7F]" />
+ <span className="text-white/90">Weak</span>
+ </div>
+ <Toggle enabled={showWeak} onChange={setShowWeak} />
</div>
</div>
</div>
</div>
- </>
+ </div>
+ )}
+
+ {/* Legend toggle button (collapsed state) */}
+ {!isExpanded && (
+ <button
+ type="button"
+ onClick={handleToggleExpanded}
+ className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white/70 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg hover:bg-[#0A1628] hover:text-white transition-colors"
+ >
+ <ChevronDown className="w-4 h-4" />
+ <span>Legend</span>
+ </button>
)}
</div>
- </div>
+ </>
)
})
diff --git a/apps/web/components/graph/loading-indicator.tsx b/apps/web/components/graph/loading-indicator.tsx
index 3af7167b..06df2cd4 100644
--- a/apps/web/components/graph/loading-indicator.tsx
+++ b/apps/web/components/graph/loading-indicator.tsx
@@ -9,10 +9,10 @@ export const LoadingIndicator = memo<LoadingIndicatorProps>(
if (!isLoading && !isLoadingMore) return null
return (
- <div className="absolute top-20 right-4 z-10 bg-white/5 backdrop-blur-xl border border-white/20 rounded-xl overflow-hidden">
+ <div className="absolute top-20 right-4 z-10 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg overflow-hidden">
<div className="p-3">
<div className="flex items-center gap-2">
- <Sparkles className="w-4 h-4 text-orange-400 animate-pulse" />
+ <Sparkles className="w-4 h-4 text-cyan-400 animate-pulse" />
<span className="text-sm text-white/70">
{isLoading
? "Loading memory graph..."
diff --git a/apps/web/components/graph/navigation-controls.tsx b/apps/web/components/graph/navigation-controls.tsx
index e7457e1f..ce4f862e 100644
--- a/apps/web/components/graph/navigation-controls.tsx
+++ b/apps/web/components/graph/navigation-controls.tsx
@@ -1,6 +1,7 @@
"use client"
import { memo } from "react"
+import { Play, Minus, Plus } from "lucide-react"
import type { NavigationControlsProps } from "./types"
export const NavigationControls = memo<NavigationControlsProps>(
@@ -14,67 +15,95 @@ export const NavigationControls = memo<NavigationControlsProps>(
timelineProgress,
nodes,
className = "",
+ zoom = 1,
}) => {
+ const zoomPercent = Math.round(zoom * 100)
+
return (
<div
- className={`absolute bottom-4 left-4 z-10 flex items-center gap-2 ${className}`}
+ className={`absolute bottom-[60px] left-4 z-10 flex flex-col gap-1.5 ${className}`}
>
+ {/* Timeline controls */}
{onTimeline && (
- <button
- type="button"
- onClick={onTimeline}
- disabled={isTimelineActive}
- className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-2 ${
- isTimelineActive
- ? "bg-blue-500/20 text-blue-400 border border-blue-500/40"
- : "text-white/80 bg-white/5 backdrop-blur-xl border border-white/20 hover:bg-white/10 hover:text-white"
- }`}
- title="Play timeline animation"
- >
- {isTimelineActive ? (
- <>
- <span className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
- {timelineProgress?.streamed ?? 0}
- </>
- ) : (
- "Timeline"
+ <div className="flex items-center gap-2 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg p-2">
+ <button
+ type="button"
+ onClick={onTimeline}
+ disabled={isTimelineActive}
+ className={`flex items-center justify-center w-8 h-8 rounded-lg transition-colors ${
+ isTimelineActive
+ ? "bg-cyan-500/20 text-cyan-400"
+ : "text-white/70 hover:bg-white/10 hover:text-white"
+ }`}
+ title="Play timeline (⌘P)"
+ >
+ {isTimelineActive ? (
+ <span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
+ ) : (
+ <Play className="w-4 h-4" />
+ )}
+ </button>
+ <span className="text-[10px] text-[#525D6E] bg-[#161F2C] px-1.5 py-0.5 rounded">
+ ⌘ P
+ </span>
+ {isTimelineActive && timelineProgress && (
+ <span className="text-xs text-cyan-400 ml-1">
+ {timelineProgress.streamed}
+ </span>
)}
- </button>
+ <span className="text-xs text-white/70 ml-1">Today</span>
+ </div>
)}
+
+ {/* Navigation controls */}
{nodes.length > 0 && (
<>
+ {/* Fit button */}
<button
type="button"
onClick={onAutoFit}
- className="px-3 py-1.5 text-sm font-medium text-white/80 bg-white/5 backdrop-blur-xl border border-white/20 rounded-lg hover:bg-white/10 hover:text-white transition-colors"
- title="Auto-fit graph to viewport"
+ className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white/70 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg hover:bg-[#0A1628] hover:text-white transition-colors"
+ title="Auto-fit graph to viewport (Z)"
>
- Fit
+ <span>Fit</span>
+ <span className="text-[10px] text-[#525D6E] bg-[#161F2C] px-1.5 py-0.5 rounded">
+ Z
+ </span>
</button>
+
+ {/* Center button */}
<button
type="button"
onClick={onCenter}
- className="px-3 py-1.5 text-sm font-medium text-white/80 bg-white/5 backdrop-blur-xl border border-white/20 rounded-lg hover:bg-white/10 hover:text-white transition-colors"
- title="Center view on graph"
+ className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-white/70 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg hover:bg-[#0A1628] hover:text-white transition-colors"
+ title="Center view on graph (C)"
>
- Center
+ <span>Center</span>
+ <span className="text-[10px] text-[#525D6E] bg-[#161F2C] px-1.5 py-0.5 rounded">
+ C
+ </span>
</button>
- <div className="flex items-center bg-white/5 backdrop-blur-xl border border-white/20 rounded-lg overflow-hidden">
+
+ {/* Zoom controls */}
+ <div className="flex items-center gap-1 bg-[#0A1628]/80 backdrop-blur-xl border border-[#1E3A5F] rounded-lg p-1">
+ <span className="text-sm font-medium text-white/70 px-2 min-w-[50px]">
+ {zoomPercent}%
+ </span>
<button
type="button"
- onClick={onZoomIn}
- className="px-3 py-1.5 text-sm font-medium text-white/80 hover:bg-white/10 hover:text-white transition-colors border-r border-white/20"
- title="Zoom in"
+ onClick={onZoomOut}
+ className="flex items-center justify-center w-7 h-7 text-white/70 hover:bg-white/10 hover:text-white transition-colors rounded"
+ title="Zoom out"
>
- +
+ <Minus className="w-3.5 h-3.5" />
</button>
<button
type="button"
- onClick={onZoomOut}
- className="px-3 py-1.5 text-sm font-medium text-white/80 hover:bg-white/10 hover:text-white transition-colors"
- title="Zoom out"
+ onClick={onZoomIn}
+ className="flex items-center justify-center w-7 h-7 text-white/70 hover:bg-white/10 hover:text-white transition-colors rounded"
+ title="Zoom in"
>
- −
+ <Plus className="w-3.5 h-3.5" />
</button>
</div>
</>
diff --git a/apps/web/components/graph/node-popover.tsx b/apps/web/components/graph/node-popover.tsx
index 48205320..956da541 100644
--- a/apps/web/components/graph/node-popover.tsx
+++ b/apps/web/components/graph/node-popover.tsx
@@ -31,6 +31,30 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
}
: undefined
+ // Get content based on node type
+ const getContent = () => {
+ if (node.type === "document") {
+ const doc = node.data as ViewportDocument
+ return doc.summary || doc.title || "No content available"
+ }
+ const mem = node.data as ViewportMemoryEntry
+ return mem.content || mem.summary || "No content available"
+ }
+
+ // Check if this is the latest/new item
+ const isLatest = () => {
+ if (node.type === "memory") {
+ const mem = node.data as ViewportMemoryEntry
+ return new Date(mem.createdAt).getTime() > Date.now() - 1000 * 60 * 60 * 24
+ }
+ return false
+ }
+
+ // Get version info
+ const getVersion = () => {
+ return "v1"
+ }
+
return (
<>
<div
@@ -41,207 +65,45 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div
onClick={(e) => e.stopPropagation()}
- className="fixed z-30 w-80 bg-slate-900/95 backdrop-blur-xl border border-white/20 rounded-xl shadow-2xl overflow-hidden"
+ className="fixed z-30 w-[400px] bg-[#0A1628]/95 backdrop-blur-xl border border-[#1E3A5F] rounded-2xl shadow-2xl overflow-hidden"
style={{
left: `${x}px`,
top: `${y}px`,
}}
>
- {node.type === "document" ? (
- <div className="p-4">
- <div className="flex items-center justify-between mb-3">
- <div className="flex items-center gap-2">
- <svg
- width="20"
- height="20"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- className="text-slate-400"
- >
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
- <polyline points="14 2 14 8 20 8" />
- <line x1="16" y1="13" x2="8" y2="13" />
- <line x1="16" y1="17" x2="8" y2="17" />
- <polyline points="10 9 9 9 8 9" />
- </svg>
- <h3 className="text-sm font-medium text-white">Document</h3>
- </div>
- <button
- type="button"
- onClick={onClose}
- className="text-white/40 hover:text-white transition-colors text-xl leading-none"
- >
- ×
- </button>
- </div>
-
- <div className="space-y-3">
- <div>
- <div className="text-xs text-white/50 mb-1">Title</div>
- <p className="text-sm text-white/90">
- {(node.data as ViewportDocument).title || "Untitled Document"}
- </p>
- </div>
+ {/* Main content */}
+ <div className="p-6">
+ <p className="text-xl text-white/90 leading-relaxed">
+ {getContent()}
+ </p>
+ </div>
- {(node.data as ViewportDocument).summary && (
- <div>
- <div className="text-xs text-white/50 mb-1">Summary</div>
- <p className="text-sm text-white/70 line-clamp-2">
- {(node.data as ViewportDocument).summary}
- </p>
- </div>
- )}
+ {/* Bottom bar with version and latest badge */}
+ <div className="px-6 pb-5 flex items-center justify-between">
+ <span className="text-cyan-400 font-medium text-lg">
+ {getVersion()}
+ </span>
- <div>
- <div className="text-xs text-white/50 mb-1">Type</div>
- <p className="text-sm text-white/70">
- {(node.data as ViewportDocument).type || "Document"}
- </p>
- </div>
-
- <div>
- <div className="text-xs text-white/50 mb-1">Memory Count</div>
- <p className="text-sm text-white/70">
- {(node.data as ViewportDocument).memoryEntries?.length || 0}{" "}
- memories
- </p>
- </div>
-
- {(node.data as ViewportDocument).url && (
- <div>
- <div className="text-xs text-white/50 mb-1">URL</div>
- <a
- href={(node.data as ViewportDocument).url || undefined}
- target="_blank"
- rel="noopener noreferrer"
- className="text-sm text-blue-400 hover:text-blue-300 flex items-center gap-1"
- >
- <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" />
- <polyline points="15 3 21 3 21 9" />
- <line x1="10" y1="14" x2="21" y2="3" />
- </svg>
- View Document
- </a>
- </div>
- )}
-
- <div className="flex items-center justify-between pt-2 border-t border-white/10 text-xs text-white/40">
- <div className="flex items-center gap-1">
- <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" />
- <line x1="16" y1="2" x2="16" y2="6" />
- <line x1="8" y1="2" x2="8" y2="6" />
- <line x1="3" y1="10" x2="21" y2="10" />
- </svg>
- <span>
- {new Date(
- (node.data as ViewportDocument).createdAt,
- ).toLocaleDateString()}
- </span>
- </div>
- <span className="font-mono text-[10px] truncate max-w-[100px]">
- {node.id}
- </span>
- </div>
- </div>
- </div>
- ) : (
- <div className="p-4">
- <div className="flex items-center justify-between mb-3">
- <div className="flex items-center gap-2">
- <svg
- width="20"
- height="20"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- className="text-blue-400"
- >
- <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 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" />
- </svg>
- <h3 className="text-sm font-medium text-white">Memory</h3>
- </div>
- <button
- type="button"
- onClick={onClose}
- className="text-white/40 hover:text-white transition-colors text-xl leading-none"
+ {isLatest() && (
+ <div className="flex items-center gap-2 text-emerald-400">
+ <svg
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
>
- ×
- </button>
- </div>
-
- <div className="space-y-3">
- <div>
- <div className="text-xs text-white/50 mb-1">Memory</div>
- <p className="text-sm text-white/90">
- {(node.data as ViewportMemoryEntry).content || "No content"}
- </p>
- </div>
-
- <div>
- <div className="text-xs text-white/50 mb-1">Space</div>
- <p className="text-sm text-white/70">
- {(node.data as ViewportMemoryEntry).spaceId || "Default"}
- </p>
- </div>
-
- <div className="flex items-center justify-between pt-2 border-t border-white/10 text-xs text-white/40">
- <div className="flex items-center gap-1">
- <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" />
- <line x1="16" y1="2" x2="16" y2="6" />
- <line x1="8" y1="2" x2="8" y2="6" />
- <line x1="3" y1="10" x2="21" y2="10" />
- </svg>
- <span>
- {new Date(
- (node.data as ViewportMemoryEntry).createdAt,
- ).toLocaleDateString()}
- </span>
- </div>
- <span className="font-mono text-[10px] truncate max-w-[100px]">
- {node.id}
- </span>
- </div>
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
+ <line x1="12" y1="22.08" x2="12" y2="12" />
+ </svg>
+ <span className="font-medium text-lg">Latest</span>
</div>
- </div>
- )}
+ )}
+ </div>
</div>
</>
)
diff --git a/apps/web/components/graph/types.ts b/apps/web/components/graph/types.ts
index 0461b84d..fa732b94 100644
--- a/apps/web/components/graph/types.ts
+++ b/apps/web/components/graph/types.ts
@@ -85,6 +85,7 @@ export interface NavigationControlsProps {
timelineProgress?: { streamed: number; total: number | null }
nodes: GraphNode[]
className?: string
+ zoom?: number
}
export interface NodePopoverProps {