"use client"; import { useEffect, useState, useCallback } from "react"; import { fetchEntryGraph } from "@/lib/api/knowledge"; import Link from "next/link"; interface GraphNode { id: string; slug: string; title: string; summary: string | null; tags: { id: string; name: string; slug: string; color: string | null; }[]; depth: number; } interface GraphEdge { id: string; sourceId: string; targetId: string; linkText: string; } interface EntryGraphResponse { centerNode: GraphNode; nodes: GraphNode[]; edges: GraphEdge[]; stats: { totalNodes: number; totalEdges: number; maxDepth: number; }; } interface EntryGraphViewerProps { slug: string; initialDepth?: number; } export function EntryGraphViewer({ slug, initialDepth = 1, }: EntryGraphViewerProps): React.JSX.Element { const [graphData, setGraphData] = useState(null); const [depth, setDepth] = useState(initialDepth); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const loadGraph = useCallback(async () => { try { setIsLoading(true); setError(null); const data = await fetchEntryGraph(slug, depth); setGraphData(data as EntryGraphResponse); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load graph"); } finally { setIsLoading(false); } }, [slug, depth]); useEffect(() => { void loadGraph(); }, [loadGraph]); const handleDepthChange = (newDepth: number): void => { setDepth(newDepth); }; if (isLoading) { return (
); } if (error ?? !graphData) { return (
Error loading graph
{error}
); } const { centerNode, nodes, edges, stats } = graphData; // Group nodes by depth for better visualization const nodesByDepth = nodes.reduce>((acc, node) => { const d = node.depth; acc[d] ??= []; acc[d].push(node); return acc; }, {}); return (
{/* Toolbar */}

Graph View

{stats.totalNodes} nodes • {stats.totalEdges} connections
{[1, 2, 3].map((d) => ( ))}
{/* Graph Visualization - Simple List View */}
{/* Center Node */}
{ setSelectedNode(centerNode); }} isSelected={selectedNode?.id === centerNode.id} />
{/* Nodes by Depth */} {Object.entries(nodesByDepth) .filter(([d]) => d !== "0") .sort(([a], [b]) => Number(a) - Number(b)) .map(([depthLevel, depthNodes]) => (

Depth {depthLevel} ({depthNodes.length}{" "} {depthNodes.length === 1 ? "node" : "nodes"})

{depthNodes.map((node) => ( { setSelectedNode(node); }} isSelected={selectedNode?.id === node.id} connections={getNodeConnections(node.id, edges)} /> ))}
))}
{/* Selected Node Details */} {selectedNode && (

{selectedNode.title}

{selectedNode.summary && (

{selectedNode.summary}

)} {selectedNode.tags.length > 0 && (
{selectedNode.tags.map((tag) => ( {tag.name} ))}
)}
View Full Entry →
)}
); } interface NodeCardProps { node: GraphNode; isCenter?: boolean; onClick?: () => void; isSelected?: boolean; connections?: { incoming: number; outgoing: number }; } function NodeCard({ node, isCenter, onClick, isSelected, connections, }: NodeCardProps): React.JSX.Element { return (

{node.title}

{node.summary && (

{node.summary}

)} {node.tags.length > 0 && (
{node.tags.slice(0, 3).map((tag) => ( {tag.name} ))} {node.tags.length > 3 && ( +{node.tags.length - 3} )}
)} {connections && (
{connections.incoming} incoming • {connections.outgoing} outgoing
)}
); } function getNodeConnections( nodeId: string, edges: GraphEdge[] ): { incoming: number; outgoing: number } { const incoming = edges.filter((e) => e.targetId === nodeId).length; const outgoing = edges.filter((e) => e.sourceId === nodeId).length; return { incoming, outgoing }; }