"use client"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { ResearchTree } from "@/lib/types"; import * as d3 from "d3"; import React, { useEffect, useRef, useState } from "react"; interface GraphNode extends d3.SimulationNodeDatum { id: string; name: string; size: number; depth: number; url?: string; width?: number; height?: number; sources?: Record; } interface GraphLink extends d3.SimulationLinkDatum { source: string | GraphNode; target: string | GraphNode; } interface ResearchGraphProps { researchTree: ResearchTree | undefined; } const ResearchGraph: React.FC = ({ researchTree }) => { const selectedNodeRef = useRef(null); const [dialogOpen, setDialogOpen] = useState(false); const [selectedNodeContent, setSelectedNodeContent] = useState<{ title: string; sources: Record }>({ title: "", sources: {}, }); const [nodePositions, setNodePositions] = useState>({}); const [nodesState, setNodesState] = useState([]); // Store nodes for both D3 and rendering const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 1200, height: 800 }); // Resize observer to update graph size useEffect(() => { if (!containerRef.current) return; const handleResize = () => { const rect = containerRef.current?.getBoundingClientRect(); if (rect) { setDimensions({ width: rect.width, height: rect.height }); } }; handleResize(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); useEffect(() => { if (!researchTree) return; // Prepare data const nodes: GraphNode[] = []; const links: GraphLink[] = []; const processTree = (tree: ResearchTree, parentId?: string) => { const nodeId = `${tree.query}_${tree.depth}`; const textLength = tree.query.length; const nodeWidth = Math.max(120, Math.min(300, textLength * 6)); const nodeHeight = 60; nodes.push({ id: nodeId, name: tree.query, size: Object.keys(tree.sources).length + 10, depth: tree.depth, width: nodeWidth, height: nodeHeight, sources: tree.sources, }); if (parentId) { links.push({ source: parentId, target: nodeId, }); } tree.children.forEach((child) => { processTree(child, nodeId); }); }; processTree(researchTree); setNodesState([...nodes]); // Save nodes for rendering const { width, height } = dimensions; const simulation = d3 .forceSimulation(nodes) .force( "link", d3 .forceLink() .id((d: GraphNode) => d.id) .links(links) .distance(80) ) .force("charge", d3.forceManyBody().strength(-800)) .force("center", d3.forceCenter(width / 2, height / 2)) .force( "collision", d3.forceCollide().radius((d: d3.SimulationNodeDatum) => { const node = d as GraphNode; return Math.max(node.width || 0, node.height || 0) / 2 + 40; }) ) .on("tick", () => { setNodePositions((prev) => { const updated: Record = {}; nodes.forEach((n) => { // Clamp positions to container updated[n.id] = { x: Math.max((n.width || 120) / 2, Math.min(n.x || 0, width - (n.width || 120) / 2)), y: Math.max((n.height || 60) / 2, Math.min(n.y || 0, height - (n.height || 60) / 2)), }; }); return updated; }); }); return () => { simulation.stop(); }; }, [researchTree, dimensions]); const formatSourceContent = () => { return Object.entries(selectedNodeContent.sources).map(([url, content]) => (

{url}

{content}
)); }; const renderNodeCard = (node: GraphNode) => { const pos = nodePositions[node.id] || { x: 0, y: 0 }; return (
{ e.stopPropagation(); selectedNodeRef.current = node; if (node.url) { window.open(node.url, "_blank"); } else if (node.sources && Object.keys(node.sources).length > 0) { setSelectedNodeContent({ title: node.name, sources: node.sources }); setDialogOpen(true); } }} onMouseDown={(e) => e.stopPropagation()}> {node.name === "_" ? "Master Node" : node.name} {node.name !== "_" && ( Source )}
); }; return (
setDialogOpen(false)}>

Research Visualization

Click on a node to see details. Source nodes are shown in purple. Click a node to open its source or view sources.

{!researchTree ? (

No research data available yet. Start a conversation to begin research.

) : (
{/* Background for interaction */}
setDialogOpen(false)} /> {/* SVG for links only */} {(() => { if (!researchTree) return null; const nodes: GraphNode[] = []; const links: GraphLink[] = []; const processTree = (tree: ResearchTree, parentId?: string) => { const nodeId = `${tree.query}_${tree.depth}`; nodes.push({ id: nodeId, name: tree.query, size: 10, depth: tree.depth }); if (parentId) { links.push({ source: parentId, target: nodeId }); } tree.children.forEach((child) => processTree(child, nodeId)); }; processTree(researchTree); return links.map((l, i) => { const src = nodePositions[(typeof l.source === "string" ? l.source : l.source.id) as string]; const tgt = nodePositions[(typeof l.target === "string" ? l.target : l.target.id) as string]; if (!src || !tgt) return null; return ; }); })()} {/* Render node cards using nodesState */} {nodesState.map(renderNodeCard)}
)}
Sources for: {selectedNodeContent.title}
{formatSourceContent()}
); }; export default ResearchGraph;