graph changes

This commit is contained in:
Shoubhit Dash 2026-01-22 11:16:08 +05:30
parent 475bdf63ff
commit ef54b5805c
9 changed files with 514 additions and 571 deletions

View file

@ -14,10 +14,10 @@ export default function GraphPage() {
if (!user) {
return (
<div className="h-screen flex items-center justify-center bg-slate-900">
<div className="h-screen flex items-center justify-center bg-[#000B1B]">
<div className="flex flex-col items-center gap-4">
<LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
<p className="text-white/60">Loading...</p>
<LoaderIcon className="w-8 h-8 text-cyan-500 animate-spin" />
<p className="text-[#525D6E]">Loading...</p>
</div>
</div>
)
@ -29,15 +29,15 @@ export default function GraphPage() {
: undefined
return (
<div className="h-full w-full bg-slate-900">
<div className="h-full w-full bg-[#000B1B]" style={{ background: "radial-gradient(ellipse at center, #01173C 0%, #000B1B 70%)" }}>
<Graph containerTags={containerTags}>
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-xl overflow-hidden p-6 text-center">
<p className="text-slate-400 mb-4">No memories found</p>
<p className="text-[#525D6E] mb-4">No memories found</p>
<button
type="button"
onClick={() => setShowAddMemory(true)}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
className="text-sm text-cyan-400 hover:text-cyan-300 transition-colors underline"
>
Add your first memory
</button>

View file

@ -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)",
},
}

View file

@ -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
// 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 (
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
}
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
// 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
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()
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
// All nodes are now rendered as hexagons
const isDocument = node.type === "document"
const isMemory = node.type === "memory"
ctx.fillStyle = isHovered
? colors.document.secondary
: colors.document.primary
ctx.globalAlpha = nodeOpacity
// 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
}
ctx.strokeStyle = isHovered
? colors.document.accent
: colors.document.border
ctx.lineWidth = isHovered ? 2 : 1
// Use consistent node colors for all nodes
let fillColor = colors.node.primary
let borderColor = colors.node.border
const radius = useSimplifiedRendering ? 6 : 12
if (isHovered) {
fillColor = colors.node.secondary
}
if (isNew) {
borderColor = colors.status.new
}
// Document nodes are slightly larger
const radius = isDocument ? nodeSize * 0.6 : nodeSize / 2
ctx.fillStyle = fillColor
ctx.globalAlpha = shouldDim ? nodeOpacity : 1
ctx.strokeStyle = borderColor
ctx.lineWidth = isHovered ? 2 : 1.5
if (useSimplifiedRendering) {
// Simple circle for low zoom
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2,
screenY - docHeight / 2,
docWidth,
docHeight,
radius,
)
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()
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()
}
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 (!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
let fillColor = colors.memory.primary
let borderColor = colors.memory.border
// Inner highlight on hover
if (isHovered) {
fillColor = colors.memory.secondary
}
if (isNew) {
borderColor = colors.status.new
}
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
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()
}
}
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()
}
}
// 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()
}
// 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([])
}

View file

@ -436,6 +436,7 @@ export function Graph({ containerTags, children }: GraphProps) {
isTimelineActive={isTimelineStreaming}
timelineProgress={timelineProgress}
nodes={nodes}
zoom={zoom}
/>
)}
</div>

View file

@ -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">
<>
{/* 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="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="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>
{/* 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>
{/* 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>
{/* 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 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-[#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 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 text-white/70 hover:text-white transition-colors"
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"
>
<span className="text-sm font-medium">?</span>
<ChevronUp className="w-4 h-4" />
<ChevronDown className="w-4 h-4" />
<span>Legend</span>
</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="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>
</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>
</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>
</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>
</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>
</div>
</div>
<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>
<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>
</div>
</div>
</div>
</>
)}
</div>
</div>
</>
)
})

View file

@ -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..."

View file

@ -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">
<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"
>
+
</button>
{/* 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={onZoomOut}
className="px-3 py-1.5 text-sm font-medium text-white/80 hover:bg-white/10 hover:text-white transition-colors"
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={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>
</>

View file

@ -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"
{/* Main content */}
<div className="p-6">
<p className="text-xl text-white/90 leading-relaxed">
{getContent()}
</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>
{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>
<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 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>
{(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>
)}
<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"
>
×
</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>
</div>
</div>
)}
)}
</div>
</div>
</>
)

View file

@ -85,6 +85,7 @@ export interface NavigationControlsProps {
timelineProgress?: { streamed: number; total: number | null }
nodes: GraphNode[]
className?: string
zoom?: number
}
export interface NodePopoverProps {