import { useState, useRef, useCallback, useEffect } from "react"; import { createRoute } from "@tanstack/react-router"; import { Route as appRoute } from "../_app"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api, useApiQuery, useApiMutation } from "@/lib/api"; import { useRealtime } from "@/hooks/use-realtime"; import { Search, ZoomIn, ZoomOut, RotateCcw, Filter } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; import type { GraphNode, GraphEdge } from "@/lib/types"; const ENTITY_TYPES = ["task", "habit", "project", "note", "section", "tag", "domain"]; const ENTITY_COLORS: Record = { task: "#3b82f6", habit: "#10b981", project: "#8b5cf6", note: "#f59e0b", section: "#ec4899", tag: "#6b7280", domain: "#6366f1", }; function GraphPage() { const queryClient = useQueryClient(); const containerRef = useRef(null); const [search, setSearch] = useState(""); const [filterOpen, setFilterOpen] = useState(false); const [enabledTypes, setEnabledTypes] = useState>(new Set(ENTITY_TYPES)); const [selectedNode, setSelectedNode] = useState(null); const [detailOpen, setDetailOpen] = useState(false); useRealtime({ enabled: true }); const { data: nodesData } = useApiQuery<{ items: GraphNode[]; totalItems: number }>( ["graph", "nodes"], "/graph/nodes?domain=placeholder" ); const { data: edgesData } = useApiQuery<{ items: GraphEdge[]; totalItems: number }>( ["graph", "edges"], "/graph/edges?domain=placeholder" ); const allNodes = nodesData?.items || []; const allEdges = edgesData?.items || []; const filteredNodes = allNodes.filter((n) => enabledTypes.has(n.type)); const filteredNodeIds = new Set(filteredNodes.map((n) => n.id)); const filteredEdges = allEdges.filter((e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target)); const toggleType = (type: string) => { const next = new Set(enabledTypes); if (next.has(type)) next.delete(type); else next.add(type); setEnabledTypes(next); }; return (
{/* Graph canvas area */}
{/* Toolbar */}
setSearch(e.target.value)} className="pl-8 w-64 bg-background/90 backdrop-blur" />
{/* Graph visualization */}
{/* Simple force-directed graph visualization */} {filteredEdges.map((edge, i) => { const source = filteredNodes.find((n) => n.id === edge.source); const target = filteredNodes.find((n) => n.id === edge.target); if (!source || !target) return null; // Simple circular layout const srcIdx = filteredNodes.indexOf(source); const tgtIdx = filteredNodes.indexOf(target); const total = filteredNodes.length; const angle1 = (2 * Math.PI * srcIdx) / Math.max(total, 1); const angle2 = (2 * Math.PI * tgtIdx) / Math.max(total, 1); const r = 150; const x1 = 200 + r * Math.cos(angle1); const y1 = 200 + r * Math.sin(angle1); const x2 = 200 + r * Math.cos(angle2); const y2 = 200 + r * Math.sin(angle2); return ; })} {filteredNodes.map((node, i) => { const total = filteredNodes.length; const angle = (2 * Math.PI * i) / Math.max(total, 1); const r = 150; const x = 200 + r * Math.cos(angle); const y = 200 + r * Math.sin(angle); return ( { setSelectedNode(node); setDetailOpen(true); }} style={{ cursor: "pointer" }}> {node.label.length > 15 ? node.label.slice(0, 15) + "..." : node.label} ); })}

{filteredNodes.length} nodes, {filteredEdges.length} edges

Full interactive graph with react-force-graph-2d will be available in T8.

{/* Filter panel */} Filters

Entity Types

{ENTITY_TYPES.map((type) => (
toggleType(type)} />
{/* Node detail panel */} {selectedNode?.label || "Node"} {selectedNode && (
{selectedNode.type}

ID: {selectedNode.id}

Connected nodes

{filteredEdges .filter((e) => e.source === selectedNode.id || e.target === selectedNode.id) .map((e, i) => { const connectedId = e.source === selectedNode.id ? e.target : e.source; const connected = allNodes.find((n) => n.id === connectedId); return connected ? (
{connected.label} {e.type}
) : null; })}
)}
); } export const Route = createRoute({ getParentRoute: () => appRoute, path: "/graph", component: GraphPage, });