"use client"; import React, { useState, useEffect, useRef, useCallback } from 'react'; import dynamic from 'next/dynamic'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { AlertCircle, Share2, Database, Plus, Network, Tag, Link } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; // Dynamically import ForceGraphComponent to avoid SSR issues const ForceGraphComponent = dynamic(() => import('@/components/ForceGraphComponent'), { ssr: false }); // Define interfaces interface Graph { id: string; name: string; entities: Entity[]; relationships: Relationship[]; metadata: Record; document_ids: string[]; filters?: Record; created_at: string; updated_at: string; } interface Entity { id: string; label: string; type: string; properties: Record; chunk_sources: Record; } interface Relationship { id: string; type: string; source_id: string; target_id: string; } interface GraphSectionProps { apiBaseUrl: string; } // Map entity types to colors const entityTypeColors: Record = { 'person': '#4f46e5', // Indigo 'organization': '#06b6d4', // Cyan 'location': '#10b981', // Emerald 'date': '#f59e0b', // Amber 'concept': '#8b5cf6', // Violet 'event': '#ec4899', // Pink 'product': '#ef4444', // Red 'default': '#6b7280' // Gray }; const GraphSection: React.FC = ({ apiBaseUrl }) => { // State variables const [graphs, setGraphs] = useState([]); const [selectedGraph, setSelectedGraph] = useState(null); const [graphName, setGraphName] = useState(''); const [graphDocuments, setGraphDocuments] = useState([]); const [graphFilters, setGraphFilters] = useState('{}'); const [additionalDocuments, setAdditionalDocuments] = useState([]); const [additionalFilters, setAdditionalFilters] = useState('{}'); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState('list'); const [showNodeLabels, setShowNodeLabels] = useState(true); const [showLinkLabels, setShowLinkLabels] = useState(true); // Refs for graph visualization const graphContainerRef = useRef(null); const graphInstance = useRef<{ width: (width: number) => unknown } | null>(null); // Prepare data for force-graph const prepareGraphData = useCallback((graph: Graph | null) => { if (!graph) return { nodes: [], links: [] }; const nodes = graph.entities.map(entity => ({ id: entity.id, label: entity.label, type: entity.type, properties: entity.properties, color: entityTypeColors[entity.type.toLowerCase()] || entityTypeColors.default })); // Create a Set of all entity IDs for faster lookups const nodeIdSet = new Set(graph.entities.map(entity => entity.id)); // Filter relationships to only include those where both source and target nodes exist const links = graph.relationships .filter(rel => nodeIdSet.has(rel.source_id) && nodeIdSet.has(rel.target_id)) .map(rel => ({ source: rel.source_id, target: rel.target_id, type: rel.type })); return { nodes, links }; }, []); // Initialize force-graph visualization const initializeGraph = useCallback(() => { // No need for implementation as ForceGraphComponent handles this }, []); // Handle window resize for responsive graph useEffect(() => { const handleResize = () => { if (graphContainerRef.current && graphInstance.current) { graphInstance.current.width(graphContainerRef.current.clientWidth); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Fetch all graphs const fetchGraphs = useCallback(async () => { try { setLoading(true); const response = await fetch(`${apiBaseUrl}/graphs`); if (!response.ok) { throw new Error(`Failed to fetch graphs: ${response.statusText}`); } const data = await response.json(); setGraphs(data); } catch (err: unknown) { const error = err as Error; setError(`Error fetching graphs: ${error.message}`); console.error('Error fetching graphs:', err); } finally { setLoading(false); } }, [apiBaseUrl]); // Fetch graphs on component mount useEffect(() => { fetchGraphs(); }, [fetchGraphs]); // Fetch a specific graph const fetchGraph = async (graphName: string) => { try { setLoading(true); const response = await fetch(`${apiBaseUrl}/graph/${encodeURIComponent(graphName)}`); if (!response.ok) { throw new Error(`Failed to fetch graph: ${response.statusText}`); } const data = await response.json(); setSelectedGraph(data); // Change to visualize tab if we're not on the list tab if (activeTab !== 'list' && activeTab !== 'create') { setActiveTab('visualize'); } return data; } catch (err: unknown) { const error = err as Error; setError(`Error fetching graph: ${error.message}`); console.error('Error fetching graph:', err); return null; } finally { setLoading(false); } }; // Handle graph click const handleGraphClick = (graph: Graph) => { fetchGraph(graph.name); }; // Create a new graph const handleCreateGraph = async () => { if (!graphName.trim()) { setError('Please enter a graph name'); return; } try { setLoading(true); setError(null); // Parse filters let parsedFilters = {}; try { parsedFilters = JSON.parse(graphFilters); } catch { throw new Error('Invalid JSON in filters field'); } const response = await fetch(`${apiBaseUrl}/graph/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: graphName, filters: Object.keys(parsedFilters).length > 0 ? parsedFilters : undefined, documents: graphDocuments.length > 0 ? graphDocuments : undefined, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `Failed to create graph: ${response.statusText}`); } const data = await response.json(); setSelectedGraph(data); // Refresh the graphs list await fetchGraphs(); // Reset form setGraphName(''); setGraphDocuments([]); setGraphFilters('{}'); // Switch to visualize tab setActiveTab('visualize'); } catch (err: unknown) { const error = err as Error; setError(`Error creating graph: ${error.message}`); console.error('Error creating graph:', err); } finally { setLoading(false); } }; // Update an existing graph const handleUpdateGraph = async () => { if (!selectedGraph) { setError('No graph selected for update'); return; } try { setLoading(true); setError(null); // Parse additional filters let parsedFilters = {}; try { parsedFilters = JSON.parse(additionalFilters); } catch { throw new Error('Invalid JSON in additional filters field'); } const response = await fetch(`${apiBaseUrl}/graph/${encodeURIComponent(selectedGraph.name)}/update`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ additional_filters: Object.keys(parsedFilters).length > 0 ? parsedFilters : undefined, additional_documents: additionalDocuments.length > 0 ? additionalDocuments : undefined, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `Failed to update graph: ${response.statusText}`); } const data = await response.json(); setSelectedGraph(data); // Refresh the graphs list await fetchGraphs(); // Reset form setAdditionalDocuments([]); setAdditionalFilters('{}'); // Switch to visualize tab setActiveTab('visualize'); } catch (err: unknown) { const error = err as Error; setError(`Error updating graph: ${error.message}`); console.error('Error updating graph:', err); } finally { setLoading(false); } }; // Initialize or update graph visualization when the selected graph changes useEffect(() => { if (selectedGraph && activeTab === 'visualize') { // Use setTimeout to ensure the container is rendered setTimeout(() => { initializeGraph(); }, 100); } }, [selectedGraph, activeTab, initializeGraph, showNodeLabels, showLinkLabels]); // Handle tab change const handleTabChange = (value: string) => { setActiveTab(value); // Initialize graph if switching to visualize tab and a graph is selected if (value === 'visualize' && selectedGraph) { setTimeout(() => { initializeGraph(); }, 100); } }; // Render the graph visualization tab const renderVisualization = () => { if (!selectedGraph) { return (

No Graph Selected

Select a graph from the list to visualize it here.

); } return (

{selectedGraph.name}

{selectedGraph.entities.length} entities, {selectedGraph.relationships.length} relationships

); }; return (

Knowledge Graphs

{selectedGraph && (
Current Graph: {selectedGraph.name}
)}
Available Graphs Create New Graph Update Graph Visualize Graph {/* Graph List Tab */} Available Knowledge Graphs Select a graph to view its details or visualize it. {loading ? (
) : graphs.length === 0 ? (

No graphs available.

) : (
{graphs.map((graph) => ( handleGraphClick(graph)} > {graph.name} {new Date(graph.created_at).toLocaleDateString()} {graph.entities.length} entities, {graph.relationships.length} relationships
{Array.from(new Set(graph.entities.map(e => e.type))).slice(0, 5).map(type => ( {type} ))} {Array.from(new Set(graph.entities.map(e => e.type))).length > 5 && ( +{Array.from(new Set(graph.entities.map(e => e.type))).length - 5} more )}
{graph.document_ids.length} document{graph.document_ids.length !== 1 ? 's' : ''}
))}
)}
{selectedGraph && ( {selectedGraph.name} Graph Details and Statistics

Documents

{selectedGraph.document_ids.length}
source documents

Entities

{selectedGraph.entities.length}
unique elements

Relationships

{selectedGraph.relationships.length}
connections

Created

{new Date(selectedGraph.created_at).toLocaleDateString()}
{new Date(selectedGraph.created_at).toLocaleTimeString()}

Entity Types

{Object.entries( selectedGraph.entities.reduce((acc, entity) => { acc[entity.type] = (acc[entity.type] || 0) + 1; return acc; }, {} as Record) ).map(([type, count]) => (
{type}
{count}
))}

Relationship Types

{Object.entries( selectedGraph.relationships.reduce((acc, rel) => { acc[rel.type] = (acc[rel.type] || 0) + 1; return acc; }, {} as Record) ).map(([type, count]) => (
{type} {count}
))}
)}
{/* Create Graph Tab */} Create New Knowledge Graph Create a knowledge graph from documents in your collection to enhance your queries.
setGraphName(e.target.value)} />

Give your graph a descriptive name that helps you identify its purpose.

Document Selection

Choose which documents to include in your graph. You can specify document IDs directly or use metadata filters.