"use client"; import React, { useState, useEffect, useRef, useCallback } from 'react'; import dynamic from 'next/dynamic'; 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 { Label } from '@/components/ui/label'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { AlertCircle, Share2, Plus, Network, Tag, Link, ArrowLeft } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Skeleton } from "@/components/ui/skeleton"; // 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'); // 'list', 'details', 'update', 'visualize' (no longer a tab, but a state) const [showCreateDialog, setShowCreateDialog] = useState(false); const [showNodeLabels, setShowNodeLabels] = useState(true); const [showLinkLabels, setShowLinkLabels] = useState(true); const [showVisualization, setShowVisualization] = useState(false); const [graphDimensions, setGraphDimensions] = useState({ width: 0, height: 0 }); // Refs for graph visualization const graphContainerRef = useRef(null); // Removed graphInstance ref as it's not needed with the dynamic component // 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 }; }, []); // Removed initializeGraph function as it's no longer needed // Observe graph container size changes useEffect(() => { if (!showVisualization || !graphContainerRef.current) return; const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { setGraphDimensions({ width: entry.contentRect.width, height: entry.contentRect.height }); } }); resizeObserver.observe(graphContainerRef.current); // Set initial size setGraphDimensions({ width: graphContainerRef.current.clientWidth, height: graphContainerRef.current.clientHeight }); const currentGraphContainer = graphContainerRef.current; // Store ref value return () => { if (currentGraphContainer) { // Use stored value in cleanup resizeObserver.unobserve(currentGraphContainer); } resizeObserver.disconnect(); }; }, [showVisualization]); // Rerun when visualization becomes active/inactive // 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); setError(null); // Clear previous errors 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); setActiveTab('details'); // Set tab to details view // Call the callback if provided if (onSelectGraph) { onSelectGraph(graphName); } return data; } catch (err: unknown) { const error = err as Error; setError(`Error fetching graph: ${error.message}`); console.error('Error fetching graph:', err); setSelectedGraph(null); // Reset selected graph on error setActiveTab('list'); // Go back to list view on error if (onSelectGraph) { onSelectGraph(undefined); } 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); setActiveTab('details'); // Switch to details tab after creation // Refresh the graphs list await fetchGraphs(); // Reset form setGraphName(''); setGraphDocuments([]); setGraphFilters('{}'); // Close dialog setShowCreateDialog(false); } catch (err: unknown) { const error = err as Error; setError(`Error creating graph: ${error.message}`); console.error('Error creating graph:', err); // Keep the dialog open on error so user can fix it } 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); // Update the selected graph data // Refresh the graphs list await fetchGraphs(); // Reset form setAdditionalDocuments([]); setAdditionalFilters('{}'); // Switch back to details tab setActiveTab('details'); } catch (err: unknown) { const error = err as Error; setError(`Error updating graph: ${error.message}`); console.error('Error updating graph:', err); // Keep the update form visible on error } finally { setLoading(false); } }; // Removed useEffect that depended on initializeGraph // Conditional rendering based on visualization state if (showVisualization && selectedGraph) { return (
{/* Visualization header */}

{selectedGraph.name} Visualization

{/* Graph visualization container */}
{graphDimensions.width > 0 && graphDimensions.height > 0 && ( )}
); } // Default view (List or Details/Update) return (
{/* Graph List View */} {activeTab === 'list' && (
{/* Removed justify-between and empty div */} 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