mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
graph changes
This commit is contained in:
parent
475bdf63ff
commit
ef54b5805c
9 changed files with 514 additions and 571 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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([])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -436,6 +436,7 @@ export function Graph({ containerTags, children }: GraphProps) {
|
|||
isTimelineActive={isTimelineStreaming}
|
||||
timelineProgress={timelineProgress}
|
||||
nodes={nodes}
|
||||
zoom={zoom}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 > 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>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export interface NavigationControlsProps {
|
|||
timelineProgress?: { streamed: number; total: number | null }
|
||||
nodes: GraphNode[]
|
||||
className?: string
|
||||
zoom?: number
|
||||
}
|
||||
|
||||
export interface NodePopoverProps {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue