'use client'; import { useState, useEffect, useMemo } from 'react'; import { Search, Filter, ZoomIn, ZoomOut, RotateCcw, Settings, Eye, EyeOff, Play, Pause, BarChart3, Network, FileText, Layers, Target, Download, RefreshCw } from 'lucide-react'; import { cn, formatTime } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Slider } from '@/components/ui/slider'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; interface RAGDocument { id: string; name: string; type: string; chunks: number; vectors: number; similarity?: number; embedding?: number[]; status: 'completed' | 'processing' | 'failed'; } interface Chunk { id: string; documentId: string; content: string; index: number; tokens: number; embedding: number[]; similarity?: number; } interface QueryResult { query: string; results: Array<{ documentId: string; chunkId: string; similarity: number; content: string; }>; method: 'vector' | 'hybrid' | 'keyword'; timestamp: Date; } interface RAGVisualizationProps { documents: RAGDocument[]; onDocumentSelect?: (documentId: string) => void; onQueryTest?: (query: string, method: string) => Promise; className?: string; } export function RAGVisualization({ documents, onDocumentSelect, onQueryTest, className = '' }: RAGVisualizationProps) { const [viewMode, setViewMode] = useState<'graph' | 'grid' | 'analysis'>('graph'); const [queryText, setQueryText] = useState(''); const [searchMethod, setSearchMethod] = useState<'vector' | 'hybrid' | 'keyword'>('hybrid'); const [queryResults, setQueryResults] = useState([]); const [isQuerying, setIsQuerying] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); const [zoomLevel, setZoomLevel] = useState(100); const [showChunks, setShowChunks] = useState(true); const [showSimilarity, setShowSimilarity] = useState(false); const [similarityThreshold, setSimilarityThreshold] = useState([0.7]); const [isPlaying, setIsPlaying] = useState(false); const [highlightedNodes, setHighlightedNodes] = useState>(new Set()); // Mock chunk data for visualization const mockChunks = useMemo(() => { const chunks: Chunk[] = []; documents.forEach(doc => { for (let i = 0; i < doc.chunks; i++) { chunks.push({ id: `${doc.id}-chunk-${i}`, documentId: doc.id, content: `Chunk ${i + 1} content from ${doc.name}`, index: i, tokens: Math.floor(Math.random() * 500) + 100, embedding: Array.from({ length: 1024 }, () => Math.random() - 0.5), similarity: Math.random() }); } }); return chunks; }, [documents]); // Calculate document similarities const documentSimilarities = useMemo(() => { const similarities: Array<{ doc1: string; doc2: string; similarity: number }> = []; for (let i = 0; i < documents.length; i++) { for (let j = i + 1; j < documents.length; j++) { similarities.push({ doc1: documents[i].id, doc2: documents[j].id, similarity: Math.random() * 0.6 + 0.2 // 0.2 to 0.8 range }); } } return similarities.filter(s => s.similarity >= similarityThreshold[0]); }, [documents, similarityThreshold]); const handleQueryTest = async () => { if (!queryText.trim()) return; setIsQuerying(true); try { // Call real search API const response = await fetch('/api/v1/search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify({ query: queryText, search_type: searchMethod, max_results: 10, min_similarity: similarityThreshold[0] }) }); if (!response.ok) { throw new Error(`Search failed: ${response.statusText}`); } const searchData = await response.json(); // Transform API response to expected format const result: QueryResult = { query: queryText, method: searchMethod, timestamp: new Date(), results: searchData.results.map((r: any) => ({ documentId: r.document_id, chunkId: r.chunk_id, similarity: r.vector_similarity, content: r.text })) }; setQueryResults(prev => [result, ...prev.slice(0, 4)]); // Highlight relevant documents const relevantDocs = new Set(result.results.map(r => r.documentId)); setHighlightedNodes(relevantDocs); // Auto-hide highlights after 5 seconds setTimeout(() => setHighlightedNodes(new Set()), 5000); // Call onQueryTest callback if provided if (onQueryTest) { await onQueryTest(queryText, searchMethod); } } catch (error) { console.error('Query test failed:', error); } finally { setIsQuerying(false); } }; const handleDocumentClick = (documentId: string) => { setSelectedDocument(selectedDocument === documentId ? null : documentId); onDocumentSelect?.(documentId); }; const resetView = () => { setZoomLevel(100); setSelectedDocument(null); setHighlightedNodes(new Set()); setQueryResults([]); }; const exportVisualization = () => { // Mock export functionality console.log('Exporting visualization data...'); const exportData = { documents, chunks: mockChunks, similarities: documentSimilarities, queries: queryResults, timestamp: new Date().toISOString() }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `rag-visualization-${Date.now()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; return (
{/* Controls Panel */}
{/* View Mode */}
{[ { id: 'graph', icon: Network, label: 'Graph' }, { id: 'grid', icon: Layers, label: 'Grid' }, { id: 'analysis', icon: BarChart3, label: 'Analysis' } ].map(({ id, icon: Icon, label }) => ( ))}
{/* Query Test */}
setQueryText(e.target?.value || '')} onKeyPress={(e) => e.key === 'Enter' && handleQueryTest()} className="text-sm" />
{/* View Controls */}
{/* Advanced Controls */}
{similarityThreshold[0].toFixed(1)}
{/* Main Visualization Area */}
{viewMode === 'graph' && ( )} {viewMode === 'grid' && ( )} {viewMode === 'analysis' && ( )}
{/* Side Panel */}
{/* Query Results */} {queryResults.length > 0 && (

Query Results

{queryResults.map((result, index) => (
{result.method} {formatTime(result.timestamp)}

{result.query}

{result.results.length} results found

))}
)} {/* Document Details */} {selectedDocument && (

Document Details

{(() => { const doc = documents.find(d => d.id === selectedDocument); if (!doc) return null; const docChunks = mockChunks.filter(c => c.documentId === selectedDocument); return (

{doc.name}

{doc.type}

Chunks

{doc.chunks.toLocaleString()}

Vectors

{doc.vectors.toLocaleString()}

Recent Chunks

{docChunks.slice(0, 3).map(chunk => (

Chunk {chunk.index + 1}

{chunk.content}

{chunk.tokens} tokens

))}
); })()}
)} {/* Statistics */}

Statistics

Total Documents {documents.length}
Total Chunks {documents.reduce((sum, doc) => sum + doc.chunks, 0).toLocaleString()}
Total Vectors {documents.reduce((sum, doc) => sum + doc.vectors, 0).toLocaleString()}
Avg Similarity {documentSimilarities.length > 0 ? (documentSimilarities.reduce((sum, s) => sum + s.similarity, 0) / documentSimilarities.length).toFixed(2) : 'N/A' }
); } // Graph View Component function GraphView({ documents, chunks, similarities, selectedDocument, highlightedNodes, zoomLevel, onDocumentClick }: { documents: RAGDocument[]; chunks: Chunk[]; similarities: Array<{ doc1: string; doc2: string; similarity: number }>; selectedDocument: string | null; highlightedNodes: Set; zoomLevel: number; onDocumentClick: (id: string) => void; }) { return (
{/* SVG Graph Visualization */} {/* Similarity Lines */} {similarities.map((sim, index) => { const doc1Index = documents.findIndex(d => d.id === sim.doc1); const doc2Index = documents.findIndex(d => d.id === sim.doc2); if (doc1Index === -1 || doc2Index === -1) return null; const x1 = (doc1Index + 1) * (800 / (documents.length + 1)); const y1 = 250 + Math.sin(doc1Index * 0.5) * 100; const x2 = (doc2Index + 1) * (800 / (documents.length + 1)); const y2 = 250 + Math.sin(doc2Index * 0.5) * 100; return ( ); })} {/* Document Nodes */} {documents.map((doc, index) => { const x = (index + 1) * (800 / (documents.length + 1)); const y = 250 + Math.sin(index * 0.5) * 100; const isSelected = selectedDocument === doc.id; const isHighlighted = highlightedNodes.has(doc.id); return ( onDocumentClick(doc.id)} /> onDocumentClick(doc.id)} > {doc.name.length > 12 ? `${doc.name.substring(0, 12)}...` : doc.name} {/* Chunk indicators */} {chunks.filter(c => c.documentId === doc.id).slice(0, 3).map((chunk, chunkIndex) => ( ))} ); })} {/* Loading/Empty State */} {documents.length === 0 && (

No documents to visualize

)}
); } // Grid View Component function GridView({ documents, chunks, selectedDocument, onDocumentClick }: { documents: RAGDocument[]; chunks: Chunk[]; selectedDocument: string | null; onDocumentClick: (id: string) => void; }) { return (
{documents.map(doc => { const docChunks = chunks.filter(c => c.documentId === doc.id); const isSelected = selectedDocument === doc.id; return (
onDocumentClick(doc.id)} className={cn( 'border rounded-lg p-4 cursor-pointer transition-all duration-200', isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' )} >

{doc.name}

{doc.chunks} chunks • {doc.vectors} vectors

{doc.type}

{/* Mini chunk visualization */}
{docChunks.slice(0, 8).map((chunk, index) => (
))}
); })}
); } // Analysis View Component function AnalysisView({ documents, chunks, similarities, queryResults }: { documents: RAGDocument[]; chunks: Chunk[]; similarities: Array<{ doc1: string; doc2: string; similarity: number }>; queryResults: QueryResult[]; }) { const avgChunksPerDoc = documents.length > 0 ? documents.reduce((sum, doc) => sum + doc.chunks, 0) / documents.length : 0; const avgVectorsPerDoc = documents.length > 0 ? documents.reduce((sum, doc) => sum + doc.vectors, 0) / documents.length : 0; return (
{/* Metrics Grid */}
{[ { label: 'Documents', value: documents.length, change: '+12%' }, { label: 'Total Chunks', value: documents.reduce((sum, doc) => sum + doc.chunks, 0), change: '+8%' }, { label: 'Avg Chunks/Doc', value: Math.round(avgChunksPerDoc), change: '-2%' }, { label: 'Query Tests', value: queryResults.length, change: 'New' } ].map((metric, index) => (
{metric.value.toLocaleString()}
{metric.label}
{metric.change}
))}
{/* Charts */}
{/* Document Distribution */}

Document Types

{(() => { const typeCount = documents.reduce((acc, doc) => { acc[doc.type] = (acc[doc.type] || 0) + 1; return acc; }, {} as Record); const totalDocs = documents.length; return Object.entries(typeCount).map(([type, count]) => (
{type}
{count} ({totalDocs > 0 ? Math.round((count / totalDocs) * 100) : 0}%)
)); })()}
{/* Similarity Distribution */}

Similarity Analysis

{similarities.slice(0, 5).map((sim, index) => { const doc1 = documents.find(d => d.id === sim.doc1); const doc2 = documents.find(d => d.id === sim.doc2); return (
{doc1?.name} ↔ {doc2?.name} {sim.similarity.toFixed(2)}
); })}
{/* Query Performance */} {queryResults.length > 0 && (

Query Performance

{queryResults.map((result, index) => (

{result.query}

{result.results.length} results • {result.method} search

{result.method}

{formatTime(result.timestamp)}

))}
)}
); }