"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; onSelectGraph?: (graphName: string | undefined) => void; authToken?: string | null; } // 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, onSelectGraph, authToken }) => { // Create auth headers for API requests if auth token is available const createHeaders = useCallback((contentType?: string): HeadersInit => { const headers: HeadersInit = {}; if (authToken) { headers['Authorization'] = `Bearer ${authToken}`; } if (contentType) { headers['Content-Type'] = contentType; } return headers; }, [authToken]); // 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 headers = createHeaders(); const response = await fetch(`${apiBaseUrl}/graphs`, { headers }); 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, createHeaders]); // Fetch graphs on component mount useEffect(() => { fetchGraphs(); }, [fetchGraphs]); // Fetch a specific graph const fetchGraph = async (graphName: string) => { try { setLoading(true); const headers = createHeaders(); const response = await fetch( `${apiBaseUrl}/graph/${encodeURIComponent(graphName)}`, { headers } ); if (!response.ok) { throw new Error(`Failed to fetch graph: ${response.statusText}`); } const data = await response.json(); setSelectedGraph(data); // Call the callback if provided if (onSelectGraph) { onSelectGraph(graphName); } // 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 headers = createHeaders('application/json'); const response = await fetch(`${apiBaseUrl}/graph/create`, { method: 'POST', headers, 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 headers = createHeaders('application/json'); const response = await fetch(`${apiBaseUrl}/graph/${encodeURIComponent(selectedGraph.name)}/update`, { method: 'POST', headers, 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}
)}

Knowledge graphs represent relationships between entities extracted from your documents. Use them to enhance your queries with structured information and improve retrieval quality.

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 Morphik 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.