diff options
Diffstat (limited to 'apps/web/components/graph')
| -rw-r--r-- | apps/web/components/graph/constants.ts | 58 | ||||
| -rw-r--r-- | apps/web/components/graph/graph-canvas.tsx | 318 | ||||
| -rw-r--r-- | apps/web/components/graph/graph.tsx | 1 | ||||
| -rw-r--r-- | apps/web/components/graph/legend.tsx | 302 | ||||
| -rw-r--r-- | apps/web/components/graph/loading-indicator.tsx | 4 | ||||
| -rw-r--r-- | apps/web/components/graph/navigation-controls.tsx | 99 | ||||
| -rw-r--r-- | apps/web/components/graph/node-popover.tsx | 246 | ||||
| -rw-r--r-- | apps/web/components/graph/types.ts | 1 |
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 > 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 { |