fazeel007's picture
initial commit
7c012de
import { useEffect, useRef, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Network, GitBranch, Zap, Eye, Play, Pause, Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import ForceGraph2D from "react-force-graph-2d";
import ForceGraph3D from "react-force-graph-3d";
import * as d3 from "d3";
interface GraphNode {
id: string;
label: string;
type: "document" | "concept" | "author" | "topic" | "query";
size: number;
color: string;
metadata?: any;
x?: number;
y?: number;
z?: number;
}
interface GraphLink {
source: string | GraphNode;
target: string | GraphNode;
relationship: string;
strength: number;
color: string;
}
interface GraphData {
nodes: GraphNode[];
links: GraphLink[];
}
interface GraphStats {
totalDocuments: number;
totalConcepts: number;
totalResearchTeams: number;
totalSourceTypes: number;
}
const NODE_COLORS = {
document: "#3b82f6", // blue
concept: "#10b981", // emerald
author: "#f59e0b", // amber
topic: "#8b5cf6", // violet
query: "#ef4444", // red
};
const RELATIONSHIP_COLORS = {
"cites": "#64748b",
"authored_by": "#f59e0b",
"contains_concept": "#10b981",
"related_to": "#8b5cf6",
"searched_for": "#ef4444",
"similar_to": "#06b6d4",
};
export function KnowledgeGraph() {
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], links: [] });
const [view3D, setView3D] = useState(false);
const [isAnimating, setIsAnimating] = useState(true);
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
const [linkDistance, setLinkDistance] = useState([150]);
const [chargeStrength, setChargeStrength] = useState([-300]);
const [showLabels, setShowLabels] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const [stats, setStats] = useState<GraphStats>({ totalDocuments: 0, totalConcepts: 0, totalResearchTeams: 0, totalSourceTypes: 0 });
const [searchTerm, setSearchTerm] = useState("");
const [filteredGraphData, setFilteredGraphData] = useState<GraphData>({ nodes: [], links: [] });
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
const [highlightNodes, setHighlightNodes] = useState(new Set<string>());
const [highlightLinks, setHighlightLinks] = useState(new Set<string>());
const fgRef = useRef<any>();
useEffect(() => {
fetchKnowledgeGraph();
}, []);
useEffect(() => {
filterGraphData();
}, [searchTerm, graphData]);
const filterGraphData = () => {
if (!searchTerm) {
setFilteredGraphData({ nodes: [], links: [] });
return;
}
const term = searchTerm.toLowerCase();
const filteredNodes = graphData.nodes.filter(node =>
node.label.toLowerCase().includes(term) ||
node.type.toLowerCase().includes(term) ||
(node.metadata?.concept && node.metadata.concept.toLowerCase().includes(term)) ||
(node.metadata?.title && node.metadata.title.toLowerCase().includes(term))
);
const nodeIds = new Set(filteredNodes.map(n => n.id));
const filteredLinks = graphData.links.filter(link => {
const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
return nodeIds.has(sourceId) && nodeIds.has(targetId);
});
setFilteredGraphData({ nodes: filteredNodes, links: filteredLinks });
};
const fetchKnowledgeGraph = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/knowledge-graph');
const data = await response.json();
if (data.nodes && data.links) {
setGraphData({ nodes: data.nodes, links: data.links });
setStats(data.stats || { totalDocuments: 0, totalConcepts: 0, totalResearchTeams: 0, totalSourceTypes: 0 });
} else {
// Fallback to sample data if API fails
generateKnowledgeGraph();
}
} catch (error) {
console.error('Failed to fetch knowledge graph:', error);
// Fallback to sample data
generateKnowledgeGraph();
} finally {
setIsLoading(false);
}
};
const generateKnowledgeGraph = () => {
// Extract concepts from KnowledgeBridge sample documents
const documents = [
{ id: "deepseek-r1", title: "DeepSeek-R1: Advanced Reasoning Model", concepts: ["reasoning", "thinking", "chain-of-thought"], authors: ["DeepSeek Team"], year: 2024, sourceType: "arxiv" },
{ id: "nebius-ai", title: "Nebius AI Platform", concepts: ["cloud-ai", "scaling", "embeddings"], authors: ["Nebius Team"], year: 2024, sourceType: "github" },
{ id: "semantic-search", title: "Semantic Search with BGE Models", concepts: ["embeddings", "similarity", "retrieval"], authors: ["BAAI Team"], year: 2024, sourceType: "arxiv" },
{ id: "modal-compute", title: "Modal Distributed Computing", concepts: ["serverless", "scaling", "distributed"], authors: ["Modal Team"], year: 2024, sourceType: "github" },
{ id: "url-validation", title: "Smart URL Validation Systems", concepts: ["validation", "filtering", "content-verification"], authors: ["Web Research Group"], year: 2024, sourceType: "web" },
{ id: "multi-source", title: "Multi-Source Information Retrieval", concepts: ["search", "aggregation", "ranking"], authors: ["IR Conference"], year: 2024, sourceType: "academic" },
{ id: "ai-security", title: "AI Application Security", concepts: ["rate-limiting", "validation", "middleware"], authors: ["Security Researchers"], year: 2024, sourceType: "github" },
{ id: "react-query", title: "TanStack Query for Data Fetching", concepts: ["caching", "state-management", "performance"], authors: ["TanStack Team"], year: 2024, sourceType: "github" },
{ id: "typescript-safety", title: "TypeScript for Type Safety", concepts: ["type-safety", "compilation", "validation"], authors: ["TypeScript Team"], year: 2024, sourceType: "web" },
{ id: "knowledge-graphs", title: "Interactive Knowledge Graphs", concepts: ["visualization", "relationships", "d3js"], authors: ["DataViz Community"], year: 2024, sourceType: "github" },
{ id: "github-api", title: "GitHub API for Repository Search", concepts: ["api-integration", "search", "filtering"], authors: ["GitHub Team"], year: 2024, sourceType: "web" },
{ id: "arxiv-papers", title: "ArXiv Academic Paper Search", concepts: ["academic-search", "paper-validation", "research"], authors: ["arXiv Team"], year: 2024, sourceType: "academic" },
];
const nodes: GraphNode[] = [];
const links: GraphLink[] = [];
// Add document nodes
documents.forEach(doc => {
nodes.push({
id: doc.id,
label: doc.title,
type: "document",
size: 12,
color: NODE_COLORS.document,
metadata: doc
});
});
// Extract unique concepts and create concept nodes
const allConcepts = new Set<string>();
documents.forEach(doc => doc.concepts.forEach(concept => allConcepts.add(concept)));
allConcepts.forEach(concept => {
const relatedDocs = documents.filter(doc => doc.concepts.includes(concept));
nodes.push({
id: `concept_${concept}`,
label: concept,
type: "concept",
size: 8 + relatedDocs.length * 2,
color: NODE_COLORS.concept,
metadata: { relatedDocuments: relatedDocs.length }
});
// Link concepts to documents
relatedDocs.forEach(doc => {
links.push({
source: doc.id,
target: `concept_${concept}`,
relationship: "contains_concept",
strength: 1,
color: RELATIONSHIP_COLORS.contains_concept
});
});
});
// Extract unique authors and create author nodes
const allAuthors = new Set<string>();
documents.forEach(doc => doc.authors.forEach(author => allAuthors.add(author)));
allAuthors.forEach(author => {
const authoredDocs = documents.filter(doc => doc.authors.includes(author));
nodes.push({
id: `author_${author}`,
label: author,
type: "author",
size: 6 + authoredDocs.length,
color: NODE_COLORS.author,
metadata: { publications: authoredDocs.length }
});
// Link authors to documents
authoredDocs.forEach(doc => {
links.push({
source: `author_${author}`,
target: doc.id,
relationship: "authored_by",
strength: 0.8,
color: RELATIONSHIP_COLORS.authored_by
});
});
});
// Create topic clusters for KnowledgeBridge features
const topics = [
{ id: "ai_models", name: "AI Models & Processing", docs: ["deepseek-r1", "nebius-ai", "semantic-search"] },
{ id: "infrastructure", name: "Infrastructure & Scaling", docs: ["modal-compute", "ai-security", "react-query"] },
{ id: "search_systems", name: "Search & Retrieval", docs: ["multi-source", "url-validation", "github-api"] },
{ id: "data_visualization", name: "Data & Visualization", docs: ["knowledge-graphs", "typescript-safety"] },
{ id: "academic_integration", name: "Academic Integration", docs: ["arxiv-papers", "semantic-search"] },
{ id: "web_technologies", name: "Web Technologies", docs: ["react-query", "typescript-safety", "ai-security"] }
];
topics.forEach(topic => {
nodes.push({
id: topic.id,
label: topic.name,
type: "topic",
size: 10,
color: NODE_COLORS.topic,
metadata: { documentCount: topic.docs.length }
});
// Link topics to documents
topic.docs.forEach(docId => {
if (documents.find(d => d.id === docId)) {
links.push({
source: topic.id,
target: docId,
relationship: "related_to",
strength: 0.6,
color: RELATIONSHIP_COLORS.related_to
});
}
});
});
// Add similarity links between documents with shared concepts
documents.forEach(doc1 => {
documents.forEach(doc2 => {
if (doc1.id !== doc2.id) {
const sharedConcepts = doc1.concepts.filter(c => doc2.concepts.includes(c));
if (sharedConcepts.length >= 2) {
links.push({
source: doc1.id,
target: doc2.id,
relationship: "similar_to",
strength: sharedConcepts.length * 0.3,
color: RELATIONSHIP_COLORS.similar_to
});
}
}
});
});
// Add sample search queries for KnowledgeBridge
const sampleQueries = [
{ id: "query_semantic", text: "semantic search with embeddings", relatedDocs: ["semantic-search", "nebius-ai"] },
{ id: "query_security", text: "AI application security middleware", relatedDocs: ["ai-security", "url-validation"] },
{ id: "query_distributed", text: "distributed AI processing", relatedDocs: ["modal-compute", "nebius-ai"] }
];
sampleQueries.forEach(query => {
nodes.push({
id: query.id,
label: query.text,
type: "query",
size: 6,
color: NODE_COLORS.query,
metadata: { searchText: query.text }
});
query.relatedDocs.forEach(docId => {
links.push({
source: query.id,
target: docId,
relationship: "searched_for",
strength: 0.4,
color: RELATIONSHIP_COLORS.searched_for
});
});
});
setGraphData({ nodes, links });
};
const handleNodeClick = (node: any) => {
setSelectedNode(node);
if (fgRef.current) {
// Center the camera on the clicked node
if (view3D) {
fgRef.current.cameraPosition(
{ x: node.x, y: node.y, z: node.z + 100 },
node,
3000
);
} else {
fgRef.current.centerAt(node.x, node.y, 1000);
fgRef.current.zoom(2, 1000);
}
}
};
const updateHighlight = () => {
const currentData = filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData;
setHighlightNodes(highlightNodes);
setHighlightLinks(highlightLinks);
};
const handleNodeHover = (node: any) => {
if (!node) {
setHoveredNode(null);
setHighlightNodes(new Set());
setHighlightLinks(new Set());
return;
}
setHoveredNode(node.id);
const newHighlightNodes = new Set<string>();
const newHighlightLinks = new Set<string>();
newHighlightNodes.add(node.id);
const currentData = filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData;
currentData.links.forEach(link => {
const sourceId = typeof link.source === 'string' ? link.source : link.source.id;
const targetId = typeof link.target === 'string' ? link.target : link.target.id;
if (sourceId === node.id || targetId === node.id) {
newHighlightLinks.add(`${sourceId}-${targetId}`);
newHighlightNodes.add(sourceId);
newHighlightNodes.add(targetId);
}
});
setHighlightNodes(newHighlightNodes);
setHighlightLinks(newHighlightLinks);
};
const resetView = () => {
if (fgRef.current) {
if (view3D) {
fgRef.current.cameraPosition({ x: 0, y: 0, z: 300 }, { x: 0, y: 0, z: 0 }, 2000);
} else {
fgRef.current.zoomToFit(2000);
}
}
setSelectedNode(null);
};
const toggleAnimation = () => {
setIsAnimating(!isAnimating);
if (fgRef.current) {
if (isAnimating) {
fgRef.current.pauseAnimation();
} else {
fgRef.current.resumeAnimation();
}
}
};
const getNodeLabel = (node: any) => {
if (!showLabels) return "";
return node.label.length > 20 ? node.label.substring(0, 20) + "..." : node.label;
};
const getNodeSize = (node: any) => {
return node.size || 8;
};
const getNodeColor = (node: any) => {
if (hoveredNode === node.id) return "#ff6b6b"; // Red for hovered node
if (highlightNodes.has(node.id)) return "#4ecdc4"; // Teal for connected nodes
return node.color; // Original color
};
const getLinkWidth = (link: any) => {
const baseWidth = Math.max(0.5, link.strength * 3);
const linkId = `${link.source}-${link.target}`;
return highlightLinks.has(linkId) ? baseWidth * 2 : baseWidth;
};
const getLinkColor = (link: any) => {
const linkId = `${link.source}-${link.target}`;
return highlightLinks.has(linkId) ? "#ff6b6b" : link.color;
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Network className="h-6 w-6" />
KnowledgeBridge Knowledge Graph
</h2>
<p className="text-muted-foreground">
Explore relationships between technologies, research sources, and concepts in KnowledgeBridge
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={fetchKnowledgeGraph} disabled={isLoading}>
<GitBranch className="h-4 w-4 mr-2" />
Refresh Graph
</Button>
<Button variant="outline" size="sm" onClick={resetView}>
<Eye className="h-4 w-4 mr-2" />
Reset View
</Button>
<Button variant="outline" size="sm" onClick={toggleAnimation}>
{isAnimating ? <Pause className="h-4 w-4 mr-2" /> : <Play className="h-4 w-4 mr-2" />}
{isAnimating ? "Pause" : "Resume"}
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Controls Panel */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="text-lg">Graph Controls</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Search Filter */}
<div className="space-y-2">
<Label className="text-sm font-medium">Search Graph</Label>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Filter nodes..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
{filteredGraphData.nodes.length > 0 && (
<div className="text-xs text-muted-foreground">
Showing {filteredGraphData.nodes.length} of {graphData.nodes.length} nodes
</div>
)}
</div>
{/* View Toggle */}
<div className="flex items-center justify-between">
<Label htmlFor="view-toggle" className="text-sm font-medium">
3D View
</Label>
<Switch
id="view-toggle"
checked={view3D}
onCheckedChange={setView3D}
/>
</div>
{/* Show Labels Toggle */}
<div className="flex items-center justify-between">
<Label htmlFor="labels-toggle" className="text-sm font-medium">
Show Labels
</Label>
<Switch
id="labels-toggle"
checked={showLabels}
onCheckedChange={setShowLabels}
/>
</div>
{/* Link Distance */}
<div className="space-y-2">
<Label className="text-sm font-medium">Link Distance</Label>
<Slider
value={linkDistance}
onValueChange={setLinkDistance}
max={300}
min={50}
step={10}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{linkDistance[0]}px
</div>
</div>
{/* Charge Strength */}
<div className="space-y-2">
<Label className="text-sm font-medium">Node Repulsion</Label>
<Slider
value={chargeStrength}
onValueChange={setChargeStrength}
max={-50}
min={-500}
step={10}
className="w-full"
/>
<div className="text-xs text-muted-foreground text-center">
{chargeStrength[0]}
</div>
</div>
{/* Legend */}
<div className="space-y-2">
<Label className="text-sm font-medium">Node Types</Label>
<div className="grid grid-cols-2 gap-1 text-xs">
{Object.entries(NODE_COLORS).map(([type, color]) => (
<div key={type} className="flex items-center gap-1">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="capitalize">{type}</span>
</div>
))}
</div>
</div>
{/* Graph Stats */}
<div className="pt-4 border-t space-y-1 text-sm">
<div className="flex justify-between">
<span>Nodes:</span>
<span className="font-medium">{graphData.nodes.length}</span>
</div>
<div className="flex justify-between">
<span>Links:</span>
<span className="font-medium">{graphData.links.length}</span>
</div>
</div>
</CardContent>
</Card>
{/* Graph Visualization */}
<Card className="lg:col-span-2">
<CardContent className="p-0">
<div className="h-[600px] w-full bg-background rounded-lg overflow-hidden relative flex items-center justify-center">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
<p className="text-sm text-muted-foreground">Building knowledge graph...</p>
</div>
</div>
)}
{view3D ? (
<ForceGraph3D
ref={fgRef}
graphData={filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData}
nodeLabel={getNodeLabel}
nodeColor={getNodeColor}
nodeVal={getNodeSize}
linkColor={getLinkColor}
linkWidth={getLinkWidth}
linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.01}
onNodeClick={handleNodeClick}
onNodeHover={handleNodeHover}
d3AlphaDecay={0.01}
d3VelocityDecay={0.1}
enableNodeDrag={true}
enableNavigationControls={true}
controlType="orbit"
backgroundColor="rgba(0,0,0,0)"
onEngineStop={() => {
if (fgRef.current) {
// Center the 3D graph with proper camera positioning
setTimeout(() => {
const distance = Math.max(400, graphData.nodes.length * 20);
fgRef.current.cameraPosition({ x: 0, y: 0, z: distance }, { x: 0, y: 0, z: 0 }, 1500);
}, 100);
}
}}
/>
) : (
<ForceGraph2D
ref={fgRef}
graphData={filteredGraphData.nodes.length > 0 ? filteredGraphData : graphData}
nodeLabel={getNodeLabel}
nodeColor={getNodeColor}
nodeVal={getNodeSize}
linkColor={getLinkColor}
linkWidth={getLinkWidth}
linkDirectionalParticles={1}
linkDirectionalParticleSpeed={0.005}
onNodeClick={handleNodeClick}
onNodeHover={handleNodeHover}
d3AlphaDecay={0.01}
d3VelocityDecay={0.1}
enableNodeDrag={true}
enableZoomInteraction={true}
backgroundColor="rgba(0,0,0,0)"
linkDirectionalArrowLength={3}
linkDirectionalArrowRelPos={1}
onEngineStop={() => {
if (fgRef.current) {
// Center the graph with proper zoom
setTimeout(() => {
fgRef.current.zoomToFit(400, 80);
}, 100);
}
}}
/>
)}
</div>
</CardContent>
</Card>
{/* Node Details Panel */}
<Card className="lg:col-span-1">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Node Details</CardTitle>
<CardDescription>
Click on a node to view details
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{selectedNode ? (
<div className="space-y-4">
<div>
<Badge variant="outline" className="mb-2">
{selectedNode.type}
</Badge>
<h3 className="font-semibold text-lg">{selectedNode.label}</h3>
</div>
{selectedNode.metadata && (
<div className="space-y-2 text-sm">
{selectedNode.type === "document" && (
<>
{selectedNode.metadata.year && (
<div>
<span className="font-medium">Year:</span> {selectedNode.metadata.year}
</div>
)}
{selectedNode.metadata.authors && Array.isArray(selectedNode.metadata.authors) && (
<div>
<span className="font-medium">Authors:</span>
<div className="flex flex-wrap gap-1 mt-1">
{selectedNode.metadata.authors.map((author: string) => (
<Badge key={author} variant="secondary" className="text-xs">
{author}
</Badge>
))}
</div>
</div>
)}
{selectedNode.metadata.venue && (
<div>
<span className="font-medium">Venue:</span> {selectedNode.metadata.venue}
</div>
)}
{selectedNode.metadata.sourceType && (
<div>
<span className="font-medium">Source Type:</span> {selectedNode.metadata.sourceType}
</div>
)}
{selectedNode.metadata.title && selectedNode.metadata.title !== selectedNode.label && (
<div>
<span className="font-medium">Full Title:</span>
<div className="text-xs text-muted-foreground mt-1">
{selectedNode.metadata.title}
</div>
</div>
)}
</>
)}
{selectedNode.type === "concept" && (
<div>
<span className="font-medium">Related Documents:</span> {selectedNode.metadata.relatedDocuments}
</div>
)}
{selectedNode.type === "author" && (
<>
{selectedNode.metadata.teamName && (
<div>
<span className="font-medium">Team:</span> {selectedNode.metadata.teamName}
</div>
)}
{selectedNode.metadata.publicationCount && (
<div>
<span className="font-medium">Publications:</span> {selectedNode.metadata.publicationCount}
</div>
)}
{selectedNode.metadata.publications && (
<div>
<span className="font-medium">Publications:</span> {selectedNode.metadata.publications}
</div>
)}
</>
)}
{selectedNode.type === "topic" && (
<div>
<span className="font-medium">Documents:</span> {selectedNode.metadata.documentCount}
</div>
)}
{selectedNode.type === "query" && (
<div>
<span className="font-medium">Search Text:</span> {selectedNode.metadata.searchText}
</div>
)}
</div>
)}
{/* Connected Nodes */}
<div>
<h4 className="font-medium mb-2">Connected Nodes</h4>
<div className="space-y-1 max-h-32 overflow-y-auto">
{graphData.links
.filter(link => {
// Handle both string and object references for source/target
const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
return sourceId === selectedNode.id || targetId === selectedNode.id;
})
.map((link, idx) => {
const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
const connectedNodeId = sourceId === selectedNode.id ? targetId : sourceId;
const connectedNode = graphData.nodes.find(n => n.id === connectedNodeId);
return connectedNode ? (
<div key={idx} className="text-xs p-2 bg-muted rounded cursor-pointer hover:bg-muted/80"
onClick={() => setSelectedNode(connectedNode)}>
<div className="font-medium">{connectedNode.label}</div>
<div className="text-muted-foreground">
{link.relationship?.replace(/_/g, ' ') || 'related'}
</div>
</div>
) : null;
})}
{graphData.links.filter(link => {
const sourceId = typeof link.source === 'string' ? link.source : (link.source as GraphNode).id;
const targetId = typeof link.target === 'string' ? link.target : (link.target as GraphNode).id;
return sourceId === selectedNode.id || targetId === selectedNode.id;
}).length === 0 && (
<div className="text-xs text-muted-foreground p-2">
No connected nodes found
</div>
)}
</div>
</div>
</div>
) : (
<div className="text-center text-muted-foreground py-8">
<GitBranch className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Select a node to view its details and connections</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Insights Panel */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5" />
Graph Insights
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">
{stats.totalDocuments || graphData.nodes.filter(n => n.type === "document").length}
</div>
<div className="text-sm text-muted-foreground">Knowledge Sources</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-emerald-600">
{stats.totalConcepts || graphData.nodes.filter(n => n.type === "concept").length}
</div>
<div className="text-sm text-muted-foreground">Technical Concepts</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-amber-600">
{stats.totalResearchTeams || graphData.nodes.filter(n => n.type === "author").length}
</div>
<div className="text-sm text-muted-foreground">Technology Teams</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-violet-600">
{stats.totalSourceTypes || graphData.nodes.filter(n => n.type === "topic").length}
</div>
<div className="text-sm text-muted-foreground">Technology Areas</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}