/** * Enhanced Multi-Domain RAG Frontend with Professional Light Theme * * Features: * - Clean, professional light theme design * - Multi-domain document upload and querying * - Document processing status tracking * - Processed documents management * - Real-time query responses with streaming */ import React, { useState, useEffect, useRef, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; import 'highlight.js/styles/github.css'; // Code syntax highlighting theme import { Send, Upload, FileText, CheckCircle, XCircle, Menu, X, Loader2, Trash2, FolderOpen, RefreshCw } from 'lucide-react'; // ============================================================================= // Domain Configurations // ============================================================================= const DOMAIN_CONFIGS = { medical: { name: 'Medical & Healthcare', description: 'Medical documents, research papers, clinical guidelines', color: '#3b82f6', bgColor: 'bg-blue-50', borderColor: 'border-blue-200', textColor: 'text-blue-700', fileTypes: ['.pdf', '.docx', '.xml', '.txt', '.doc', '.csv', '.xlsx'], icon: '🏥' }, legal: { name: 'Legal & Compliance', description: 'Legal documents, contracts, regulations, case law', color: '#8b5cf6', bgColor: 'bg-purple-50', borderColor: 'border-purple-200', textColor: 'text-purple-700', fileTypes: ['.pdf', '.docx', '.txt', '.doc', '.csv', '.xlsx'], icon: '⚖️' }, financial: { name: 'Financial & Analytics', description: 'Financial reports, analysis, market research', color: '#10b981', bgColor: 'bg-green-50', borderColor: 'border-green-200', textColor: 'text-green-700', fileTypes: ['.pdf', '.xlsx', '.csv', '.json', '.xls'], icon: '💰' }, technical: { name: 'Technical Documentation', description: 'Technical docs, APIs, code, system architecture', color: '#f97316', bgColor: 'bg-orange-50', borderColor: 'border-orange-200', textColor: 'text-orange-700', fileTypes: ['.pdf', '.md', '.docx', '.json', '.txt', '.rst', '.csv', '.xlsx'], icon: '⚙️' }, academic: { name: 'Academic Research', description: 'Research papers, academic publications, studies', color: '#6366f1', bgColor: 'bg-indigo-50', borderColor: 'border-indigo-200', textColor: 'text-indigo-700', fileTypes: ['.pdf', '.docx', '.tex', '.bib', '.txt', '.csv', '.xlsx'], icon: '🎓' } }; const API_BASE_URL = process.env.REACT_APP_API_URL || ''; // ============================================================================= // Main Component // ============================================================================= export default function EnhancedMultiDomainRAG() { // Helper function to get from localStorage with fallback const getFromLocalStorage = (key, defaultValue) => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.error(`Error reading localStorage key "${key}":`, error); return defaultValue; } }; // State Management with localStorage persistence const [selectedDomain, setSelectedDomain] = useState(() => getFromLocalStorage('selectedDomain', 'medical') ); const [currentView, setCurrentView] = useState('app'); // 'app', 'files', 'settings' const [processingDocs, setProcessingDocs] = useState(() => getFromLocalStorage('processingDocs', []) ); const [processedDocs, setProcessedDocs] = useState([]); const [query, setQuery] = useState(''); const [messages, setMessages] = useState(() => getFromLocalStorage('chatMessages', []) ); const [isQuerying, setIsQuerying] = useState(false); const [error, setError] = useState(null); const [showUploadModal, setShowUploadModal] = useState(false); const [isDragging, setIsDragging] = useState(false); const [showSidebar, setShowSidebar] = useState(true); const [enableWebSearch, setEnableWebSearch] = useState(() => getFromLocalStorage('enableWebSearch', false) ); const [webSearchOnly, setWebSearchOnly] = useState(() => getFromLocalStorage('webSearchOnly', false) ); const [urlInput, setUrlInput] = useState(''); const [uploadMode, setUploadMode] = useState('file'); // 'file' or 'url' const [fastMode, setFastMode] = useState(() => getFromLocalStorage('fastMode', false) ); const [enableCache, setEnableCache] = useState(() => getFromLocalStorage('enableCache', true) ); const [enableQueryImprovement, setEnableQueryImprovement] = useState(() => getFromLocalStorage('enableQueryImprovement', true) ); const [enableVerification, setEnableVerification] = useState(() => getFromLocalStorage('enableVerification', true) ); const [typingSpeed] = useState(0) const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const typingQueueRef = useRef([]); const typingIntervalRef = useRef(null); // Auto-scroll to bottom of messages const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // Persist messages to localStorage whenever they change useEffect(() => { try { window.localStorage.setItem('chatMessages', JSON.stringify(messages)); } catch (error) { console.error('Error saving messages to localStorage:', error); } }, [messages]); // Persist selectedDomain to localStorage useEffect(() => { try { window.localStorage.setItem('selectedDomain', JSON.stringify(selectedDomain)); } catch (error) { console.error('Error saving domain to localStorage:', error); } }, [selectedDomain]); // Persist processingDocs to localStorage useEffect(() => { try { window.localStorage.setItem('processingDocs', JSON.stringify(processingDocs)); } catch (error) { console.error('Error saving processingDocs to localStorage:', error); } }, [processingDocs]); // Persist web search settings to localStorage useEffect(() => { try { window.localStorage.setItem('enableWebSearch', JSON.stringify(enableWebSearch)); } catch (error) { console.error('Error saving enableWebSearch to localStorage:', error); } }, [enableWebSearch]); useEffect(() => { try { window.localStorage.setItem('webSearchOnly', JSON.stringify(webSearchOnly)); } catch (error) { console.error('Error saving webSearchOnly to localStorage:', error); } }, [webSearchOnly]); // Persist fast mode setting to localStorage useEffect(() => { try { window.localStorage.setItem('fastMode', JSON.stringify(fastMode)); } catch (error) { console.error('Error saving fastMode to localStorage:', error); } }, [fastMode]); // Persist cache setting to localStorage useEffect(() => { try { window.localStorage.setItem('enableCache', JSON.stringify(enableCache)); } catch (error) { console.error('Error saving enableCache to localStorage:', error); } }, [enableCache]); // Persist query improvement setting to localStorage useEffect(() => { try { window.localStorage.setItem('enableQueryImprovement', JSON.stringify(enableQueryImprovement)); } catch (error) { console.error('Error saving enableQueryImprovement to localStorage:', error); } }, [enableQueryImprovement]); // Persist verification setting to localStorage useEffect(() => { try { window.localStorage.setItem('enableVerification', JSON.stringify(enableVerification)); } catch (error) { console.error('Error saving enableVerification to localStorage:', error); } }, [enableVerification]); // Persist typing speed setting to localStorage useEffect(() => { try { window.localStorage.setItem('typingSpeed', JSON.stringify(typingSpeed)); } catch (error) { console.error('Error saving typingSpeed to localStorage:', error); } }, [typingSpeed]); // Fetch processed documents function with useCallback const fetchProcessedDocuments = useCallback(async () => { try { const response = await fetch(`${API_BASE_URL}/documents?domain=${selectedDomain}`); if (response.ok) { const data = await response.json(); const fetchedDocs = data.documents || []; // Merge with existing docs to avoid duplicates // Keep docs that exist in both, prefer fetched version for consistency setProcessedDocs(prev => { const fetchedIds = new Set(fetchedDocs.map(d => d.id)); // Keep docs from prev that aren't in fetched (recently added via status check) const recentlyAdded = prev.filter(d => d.id && !fetchedIds.has(d.id)); // Combine with fetched docs return [...fetchedDocs, ...recentlyAdded]; }); } } catch (err) { console.error('Error fetching documents:', err); } }, [selectedDomain]); // Check processing status function with useCallback const checkProcessingStatus = useCallback(async () => { // Update processing docs status const updatedProcessing = []; for (const doc of processingDocs) { try { const response = await fetch(`${API_BASE_URL}/status/${doc.processingId}`); if (response.ok) { const status = await response.json(); if (status.status === 'completed') { // Move to processed - use processingId as id for deletion setProcessedDocs(prev => [...prev, { ...doc, id: doc.processingId, status: 'completed' }]); } else if (status.status === 'failed') { setError(`Processing failed for ${doc.name}: ${status.error}`); } else { updatedProcessing.push({ ...doc, status: status.status }); } } } catch (err) { console.error('Error checking status:', err); } } setProcessingDocs(updatedProcessing); }, [processingDocs]); // Fetch processed documents on domain change useEffect(() => { fetchProcessedDocuments(); }, [selectedDomain, fetchProcessedDocuments]); // Poll for document processing status useEffect(() => { const interval = setInterval(() => { if (processingDocs.length > 0) { checkProcessingStatus(); } }, 3000); return () => clearInterval(interval); }, [processingDocs, checkProcessingStatus]); // ============================================================================= // API Functions // ============================================================================= const handleFileUpload = async (files) => { if (!files || files.length === 0) return; setError(null); const newProcessingDocs = []; for (const file of files) { const fileExt = '.' + file.name.split('.').pop().toLowerCase(); const allowedTypes = DOMAIN_CONFIGS[selectedDomain].fileTypes; if (!allowedTypes.includes(fileExt)) { setError(`File type ${fileExt} not supported for ${selectedDomain} domain. Allowed: ${allowedTypes.join(', ')}`); continue; } const formData = new FormData(); formData.append('file', file); formData.append('domain', selectedDomain); try { const response = await fetch(`${API_BASE_URL}/upload`, { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { newProcessingDocs.push({ name: file.name, domain: selectedDomain, processingId: data.processing_id, status: 'processing', uploadedAt: new Date().toISOString() }); } else { setError(data.detail || 'Upload failed'); } } catch (err) { console.error('Upload error:', err); setError(`Failed to upload ${file.name}: ${err.message}`); } } setProcessingDocs(prev => [...prev, ...newProcessingDocs]); setShowUploadModal(false); }; const handleUrlUpload = async () => { if (!urlInput.trim()) { setError('Please enter a valid URL'); return; } setError(null); try { const response = await fetch(`${API_BASE_URL}/upload-url`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: urlInput, domain: selectedDomain, convert_to_markdown: true }) }); const data = await response.json(); if (response.ok) { setProcessingDocs(prev => [...prev, { name: urlInput, domain: selectedDomain, processingId: data.processing_id, status: 'processing', uploadedAt: new Date().toISOString() }]); setUrlInput(''); setShowUploadModal(false); } else { setError(data.detail || 'URL upload failed'); } } catch (err) { console.error('URL upload error:', err); setError(`Failed to upload URL: ${err.message}`); } }; // Typing effect function with queue-based approach const startTypingEffect = useCallback((messageIndex, targetTextRef, isStreamingRef) => { // Clear any existing typing interval if (typingIntervalRef.current) { clearInterval(typingIntervalRef.current); } let displayedLength = 0; typingIntervalRef.current = setInterval(() => { const targetText = targetTextRef.current || ''; const isStillStreaming = isStreamingRef.current; if (displayedLength < targetText.length) { // Add characters based on typing speed (higher = faster) const charsToAdd = Math.max(1, Math.floor(typingSpeed / 10)); displayedLength = Math.min(displayedLength + charsToAdd, targetText.length); setMessages(prev => { const newMessages = [...prev]; if (newMessages[messageIndex]) { newMessages[messageIndex] = { ...newMessages[messageIndex], content: targetText.substring(0, displayedLength) }; } return newMessages; }); } else if (!isStillStreaming && displayedLength >= targetText.length) { // If we've caught up and streaming is done, clear the interval clearInterval(typingIntervalRef.current); typingIntervalRef.current = null; } }, 30); // Update every 30ms for smoother animation }, [typingSpeed]); // Cleanup typing interval on unmount useEffect(() => { return () => { if (typingIntervalRef.current) { clearInterval(typingIntervalRef.current); } }; }, []); const handleQuery = async () => { if (!query.trim()) return; setError(null); setIsQuerying(true); const userMessage = { role: 'user', content: query }; setMessages(prev => [...prev, userMessage]); const currentQuery = query; setQuery(''); // Create placeholder for streaming response const assistantMessageIndex = messages.length + 1; setMessages(prev => [...prev, { role: 'assistant', content: '', streaming: true, verification: null }]); // Use ref to store the full text buffer so typing effect can access it const fullTextBufferRef = { current: '' }; const isStreamingRef = { current: true }; let typingStarted = false; try { const response = await fetch(`${API_BASE_URL}/query/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: currentQuery, domain: selectedDomain, enable_verification: true, enable_web_search: enableWebSearch, web_search_only: webSearchOnly, fast_mode: fastMode, enable_cache: enableCache, enable_query_improvement: enableQueryImprovement, enable_verification_check: enableVerification }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // Read the stream const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } // Decode chunk buffer += decoder.decode(value, { stream: true }); // Process complete SSE events const events = buffer.split('\n\n'); buffer = events.pop() || ''; // Keep incomplete event in buffer for (const event of events) { if (!event.trim()) continue; const lines = event.split('\n'); let eventType = 'message'; let eventData = ''; for (const line of lines) { if (line.startsWith('event:')) { eventType = line.substring(6).trim(); } else if (line.startsWith('data:')) { eventData = line.substring(5).trim(); } } if (eventData) { const data = JSON.parse(eventData); if (eventType === 'token') { // Add to buffer ref fullTextBufferRef.current += data.content; // Start typing effect once if speed > 0 if (!typingStarted && typingSpeed > 0) { typingStarted = true; startTypingEffect(assistantMessageIndex, fullTextBufferRef, isStreamingRef); } else if (typingSpeed === 0) { // Instant display if typing speed is 0 setMessages(prev => { const newMessages = [...prev]; newMessages[assistantMessageIndex] = { ...newMessages[assistantMessageIndex], content: fullTextBufferRef.current }; return newMessages; }); } } else if (eventType === 'verification') { // Add verification info to message setMessages(prev => { const newMessages = [...prev]; newMessages[assistantMessageIndex] = { ...newMessages[assistantMessageIndex], verification: data.content, streaming: false }; return newMessages; }); } else if (eventType === 'done') { // Mark streaming as complete isStreamingRef.current = false; // Wait a bit for typing to catch up, then ensure final text is shown setTimeout(() => { if (typingIntervalRef.current) { clearInterval(typingIntervalRef.current); typingIntervalRef.current = null; } // Set final content and mark as complete setMessages(prev => { const newMessages = [...prev]; newMessages[assistantMessageIndex] = { ...newMessages[assistantMessageIndex], streaming: false, content: fullTextBufferRef.current }; return newMessages; }); }, typingSpeed === 0 ? 0 : 500); // Wait 500ms for typing to finish } else if (eventType === 'error') { const errorMessage = data.content.message || 'An error occurred while processing your query'; const errorSuggestion = data.content.suggestion || ''; setError(errorSuggestion ? `${errorMessage}\n\n${errorSuggestion}` : errorMessage); // Mark streaming as complete isStreamingRef.current = false; // Clear typing interval if (typingIntervalRef.current) { clearInterval(typingIntervalRef.current); typingIntervalRef.current = null; } // Mark message as error with helpful message setMessages(prev => { const newMessages = [...prev]; newMessages[assistantMessageIndex] = { ...newMessages[assistantMessageIndex], content: fullTextBufferRef.current || errorMessage, streaming: false, error: true }; return newMessages; }); break; } } } } } catch (err) { console.error('Query error:', err); setError(`Query failed: ${err.message}`); // Clear typing interval if (typingIntervalRef.current) { clearInterval(typingIntervalRef.current); typingIntervalRef.current = null; } // Update message with error setMessages(prev => { const newMessages = [...prev]; if (newMessages[assistantMessageIndex]) { newMessages[assistantMessageIndex] = { ...newMessages[assistantMessageIndex], content: newMessages[assistantMessageIndex].content || '[Error occurred]', streaming: false, error: true }; } return newMessages; }); } finally { setIsQuerying(false); } }; const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleQuery(); } }; const handleDeleteDocument = async (docId, docName) => { if (!docId) { console.error('Document ID is undefined'); setError('Cannot delete document: ID is missing'); return; } // Show confirmation dialog const confirmed = window.confirm( `Are you sure you want to delete "${docName || 'this document'}"?\n\n` + `This will permanently remove:\n` + `• All text chunks and embeddings\n` + `• Knowledge graph entities and relationships\n` + `• Vector database entries\n` + `• Physical files\n\n` + `This action cannot be undone.` ); if (!confirmed) { return; } try { const response = await fetch(`${API_BASE_URL}/documents/${docId}`, { method: 'DELETE' }); const data = await response.json(); if (response.ok && data.success) { // Show success message with deletion details const report = data.report; const summary = report?.summary || {}; alert( `✓ Document deleted successfully!\n\n` + `Removed from knowledge base:\n` + `• ${summary.chunks_deleted || 0} text chunks\n` + `• ${summary.entities_deleted || 0} knowledge graph entities\n` + `• ${summary.relationships_deleted || 0} relationships\n` + `• ${summary.vectors_deleted || 0} embedding vectors\n` + `• ${summary.files_deleted || 0} physical files\n` + `• ${summary.directories_deleted || 0} directories` ); setProcessedDocs(prev => prev.filter(doc => doc.id !== docId)); // Also refresh the documents list to ensure consistency await fetchProcessedDocuments(); } else { // Show error with details if available const errorMsg = data.message || data.detail || 'Failed to delete document'; const errors = data.report?.errors || []; setError( errorMsg + (errors.length > 0 ? `\n\nErrors: ${errors.join(', ')}` : '') ); } } catch (err) { console.error('Error deleting document:', err); setError('Failed to delete document: ' + err.message); } }; const clearConversation = () => { setMessages([]); }; // ============================================================================= // Drag and Drop Handlers // ============================================================================= const handleDragOver = (e) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); setIsDragging(false); }; const handleDrop = (e) => { e.preventDefault(); setIsDragging(false); handleFileUpload(e.dataTransfer.files); }; // ============================================================================= // Render Functions // ============================================================================= const renderNavigation = () => ( ); const renderSidebar = () => (
OrgAI
{/*
*/}
Upload documents and start chatting to get intelligent responses powered by Advanced Multimodal RAG technology.
Support for PDF, Word, Excel, CSV and more
Get accurate answers from your documents
Optimized for medical, legal, financial and more
{msg.content}
) : ( // Assistant messages: rendered markdown
{children}
);
},
// Custom styling for links
a({ node, children, ...props }) {
return (
{children}
);
},
// Custom styling for headings
h1: ({ node, ...props }) => ,
h2: ({ node, ...props }) => ,
h3: ({ node, ...props }) => ,
// Custom styling for lists
ul: ({ node, ...props }) => Issues found:
Sources:
{msg.sources.slice(0, 3).map((source, i) => (Press Enter to send, Shift+Enter for new line
Manage your uploaded and processed documents
{doc.name}
Processing...
No documents processed yet
{doc.name || `Document ${idx + 1}`}
{DOMAIN_CONFIGS[doc.domain]?.name || selectedDomain}
Use optimized parameters for 2-3x faster queries. Slightly reduced quality but much better performance.
Cache query results for 5 minutes. Repeated queries return instantly (100x faster).
Augment document answers with current web search results.
Skip document retrieval and use only web search (useful when no documents uploaded).
Automatically improve and expand user queries for better results. Disable for faster responses.
Use dual-LLM verification to check answer quality and accuracy. Disable for faster responses.
Supported: {DOMAIN_CONFIGS[selectedDomain].fileTypes.join(', ')}
handleFileUpload(e.target.files)} className="hidden" />Supported: PDF, HTML pages (converted to markdown), and other web documents
{error}