diff --git "a/frontend/src/App.js" "b/frontend/src/App.js" --- "a/frontend/src/App.js" +++ "b/frontend/src/App.js" @@ -1,1549 +1,1549 @@ -/** - * 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 = () => ( -
-
-
-

Domains

-
- {Object.entries(DOMAIN_CONFIGS).map(([key, config]) => ( - - ))} -
-
- - {processingDocs.length > 0 && ( -
-

Processing

-
- {processingDocs.map((doc, idx) => ( -
- - {doc.name} -
- ))} -
-
- )} - - {processedDocs.length > 0 && ( -
-

- Processed Documents ({processedDocs.length}) -

-
- {processedDocs.map((doc, idx) => ( -
- - {doc.name || `Document ${idx + 1}`} - -
- ))} -
-
- )} - - {messages.length > 0 && ( -
- -
- )} -
-
- ); - - const renderAppView = () => ( -
- {messages.length === 0 ? ( -
-
- {/*
- O -
-

Welcome to OrgAI

*/} -
- OrgAI Logo -

OrgAI

- {/* GlokalAI Logo */} -
-

Welcome to OrgAI

-

- Upload documents and start chatting to get intelligent responses powered by Advanced Multimodal RAG technology. -

- -
-
-
📄
-

Upload Documents

-

Support for PDF, Word, Excel, CSV and more

-
-
-
🔍
-

Ask Questions

-

Get accurate answers from your documents

-
-
-
-

Multi-Domain

-

Optimized for medical, legal, financial and more

-
-
-
-
- ) : ( -
-
- {messages.map((msg, idx) => ( -
-
-
-
- {msg.role === 'user' ? ( - // User messages: simple text -

- {msg.content} -

- ) : ( - // Assistant messages: rendered markdown -
- - {children} - - ) : ( - - {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 }) =>
    , - ol: ({ node, ...props }) =>
      , - // Custom styling for blockquotes - blockquote: ({ node, ...props }) => ( -
      - ), - // Custom styling for tables - table: ({ node, ...props }) => ( -
      - - - ), - th: ({ node, ...props }) => ( -
      - ), - td: ({ node, ...props }) => ( - - ), - }} - > - {msg.content} - - {/* {msg.streaming && ( - - )} */} - - )} - - {msg.streaming && msg.role === 'assistant' && ( -
      - Thinking - . - . - . -
      - )} - - - {/* Verification Badge - {msg.verification && !msg.streaming && ( -
      -
      - {msg.verification.passed ? ( - - ) : ( - - )} - - Verification Score: {msg.verification.score?.toFixed(1)}/10 - - - ({Math.round((msg.verification.confidence || 0) * 100)}% confident) - -
      - {msg.verification.issues && msg.verification.issues.length > 0 && ( -
      -

      Issues found:

      -
        - {msg.verification.issues.slice(0, 3).map((issue, i) => ( -
      • {issue}
      • - ))} -
      -
      - )} -
      - )} */} - - {msg.sources && msg.sources.length > 0 && ( -
      -

      Sources:

      - {msg.sources.slice(0, 3).map((source, i) => ( -
      - • {source.file_name} (score: {source.score?.toFixed(2)}) -
      - ))} -
      - )} - - - ))} -
      -
      - - )} - - {/* Bottom Input Bar */} -
      -
      -
      - - - setQuery(e.target.value)} - onKeyDown={handleKeyPress} - placeholder="Ask me anything or upload documents for context..." - className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - disabled={isQuerying} - /> - - -
      - - {/* Web Search Options */} -
      - - -
      - -

      - Press Enter to send, Shift+Enter for new line -

      -
      -
      - - ); - - const renderFilesView = () => ( -
      -
      -
      -
      -

      Document Management

      -

      Manage your uploaded and processed documents

      -
      -
      - - -
      -
      - - {processingDocs.length > 0 && ( -
      -

      Processing Documents

      -
      - {processingDocs.map((doc, idx) => ( -
      - -
      -

      {doc.name}

      -

      Processing...

      -
      -
      - ))} -
      -
      - )} - -
      -

      - Processed Documents ({processedDocs.length}) -

      - {processedDocs.length === 0 ? ( -
      - -

      No documents processed yet

      - -
      - ) : ( -
      - {processedDocs.map((doc, idx) => ( -
      -
      - - -
      -

      {doc.name || `Document ${idx + 1}`}

      -

      {DOMAIN_CONFIGS[doc.domain]?.name || selectedDomain}

      -
      - - Processed -
      -
      - ))} -
      - )} -
      -
      -
      - ); - - const renderSettingsView = () => ( -
      -
      -

      Settings

      - -
      -
      -

      Domain Configuration

      -
      -
      - - -
      -
      - -
      - {DOMAIN_CONFIGS[selectedDomain].fileTypes.map(type => ( - - {type} - - ))} -
      -
      -
      -
      - -
      -

      Performance Settings

      -
      -
      - setFastMode(e.target.checked)} - className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" - /> -
      - -

      - Use optimized parameters for 2-3x faster queries. Slightly reduced quality but much better performance. -

      -
      -
      - -
      - setEnableCache(e.target.checked)} - className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" - /> -
      - -

      - Cache query results for 5 minutes. Repeated queries return instantly (100x faster). -

      -
      -
      - -
      - { - setEnableWebSearch(e.target.checked); - if (e.target.checked && webSearchOnly) { - setWebSearchOnly(false); - } - }} - className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" - /> -
      - -

      - Augment document answers with current web search results. -

      -
      -
      - -
      - { - setWebSearchOnly(e.target.checked); - if (e.target.checked) { - setEnableWebSearch(false); - } - }} - className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" - /> -
      - -

      - Skip document retrieval and use only web search (useful when no documents uploaded). -

      -
      -
      - -
      - setEnableQueryImprovement(e.target.checked)} - className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" - /> -
      - -

      - Automatically improve and expand user queries for better results. Disable for faster responses. -

      -
      -
      - -
      - setEnableVerification(e.target.checked)} - className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" - /> -
      - -

      - Use dual-LLM verification to check answer quality and accuracy. Disable for faster responses. -

      -
      -
      - - -
      -
      - -
      -

      Actions

      -
      - -
      -
      -
      -
      -
      - ); - - // Upload Modal - const renderUploadModal = () => { - if (!showUploadModal) return null; - - return ( -
      -
      -
      -

      Upload Documents

      - -
      - - {/* Mode Toggle */} -
      - - -
      - - {uploadMode === 'file' ? ( -
      - -

      - Drop files here or click to browse -

      -

      - Supported: {DOMAIN_CONFIGS[selectedDomain].fileTypes.join(', ')} -

      - handleFileUpload(e.target.files)} - className="hidden" - /> - -
      - ) : ( -
      -
      - - setUrlInput(e.target.value)} - placeholder="https://example.com/document.pdf" - className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleUrlUpload(); - } - }} - /> -
      -
      -

      - Supported: PDF, HTML pages (converted to markdown), and other web documents -

      -
      - -
      - )} -
      -
      - ); - }; - - // Error Display - const renderError = () => { - if (!error) return null; - - return ( -
      -
      - -
      -

      {error}

      -
      - -
      -
      - ); - }; - - // ============================================================================= - // Main Render - // ============================================================================= - - return ( -
      - {renderNavigation()} - -
      - {renderSidebar()} - - {currentView === 'app' && renderAppView()} - {currentView === 'files' && renderFilesView()} - {currentView === 'settings' && renderSettingsView()} -
      - - {renderUploadModal()} - {renderError()} -
      - ); -} +/** + * 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 = () => ( +
      +
      +
      +

      Domains

      +
      + {Object.entries(DOMAIN_CONFIGS).map(([key, config]) => ( + + ))} +
      +
      + + {processingDocs.length > 0 && ( +
      +

      Processing

      +
      + {processingDocs.map((doc, idx) => ( +
      + + {doc.name} +
      + ))} +
      +
      + )} + + {processedDocs.length > 0 && ( +
      +

      + Processed Documents ({processedDocs.length}) +

      +
      + {processedDocs.map((doc, idx) => ( +
      + + {doc.name || `Document ${idx + 1}`} + +
      + ))} +
      +
      + )} + + {messages.length > 0 && ( +
      + +
      + )} +
      +
      + ); + + const renderAppView = () => ( +
      + {messages.length === 0 ? ( +
      +
      + {/*
      + O +
      +

      Welcome to OrgAI

      */} +
      + OrgAI Logo +

      OrgAI

      + {/* GlokalAI Logo */} +
      +

      Welcome to OrgAI

      +

      + Upload documents and start chatting to get intelligent responses powered by Advanced Multimodal RAG technology. +

      + +
      +
      +
      📄
      +

      Upload Documents

      +

      Support for PDF, Word, Excel, CSV and more

      +
      +
      +
      🔍
      +

      Ask Questions

      +

      Get accurate answers from your documents

      +
      +
      +
      +

      Multi-Domain

      +

      Optimized for medical, legal, financial and more

      +
      +
      +
      +
      + ) : ( +
      +
      + {messages.map((msg, idx) => ( +
      +
      +
      +
      + {msg.role === 'user' ? ( + // User messages: simple text +

      + {msg.content} +

      + ) : ( + // Assistant messages: rendered markdown +
      + + {children} + + ) : ( + + {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 }) =>
        , + ol: ({ node, ...props }) =>
          , + // Custom styling for blockquotes + blockquote: ({ node, ...props }) => ( +
          + ), + // Custom styling for tables + table: ({ node, ...props }) => ( +
          + + + ), + th: ({ node, ...props }) => ( +
          + ), + td: ({ node, ...props }) => ( + + ), + }} + > + {msg.content} + + {/* {msg.streaming && ( + + )} */} + + )} + + {msg.streaming && msg.role === 'assistant' && ( +
          + Thinking + . + . + . +
          + )} + + + {/* Verification Badge + {msg.verification && !msg.streaming && ( +
          +
          + {msg.verification.passed ? ( + + ) : ( + + )} + + Verification Score: {msg.verification.score?.toFixed(1)}/10 + + + ({Math.round((msg.verification.confidence || 0) * 100)}% confident) + +
          + {msg.verification.issues && msg.verification.issues.length > 0 && ( +
          +

          Issues found:

          +
            + {msg.verification.issues.slice(0, 3).map((issue, i) => ( +
          • {issue}
          • + ))} +
          +
          + )} +
          + )} */} + + {msg.sources && msg.sources.length > 0 && ( +
          +

          Sources:

          + {msg.sources.slice(0, 3).map((source, i) => ( +
          + • {source.file_name} (score: {source.score?.toFixed(2)}) +
          + ))} +
          + )} + + + ))} +
          +
          + + )} + + {/* Bottom Input Bar */} +
          +
          +
          + + + setQuery(e.target.value)} + onKeyDown={handleKeyPress} + placeholder="Ask me anything or upload documents for context..." + className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={isQuerying} + /> + + +
          + + {/* Web Search Options */} +
          + + +
          + +

          + Press Enter to send, Shift+Enter for new line +

          +
          +
          + + ); + + const renderFilesView = () => ( +
          +
          +
          +
          +

          Document Management

          +

          Manage your uploaded and processed documents

          +
          +
          + + +
          +
          + + {processingDocs.length > 0 && ( +
          +

          Processing Documents

          +
          + {processingDocs.map((doc, idx) => ( +
          + +
          +

          {doc.name}

          +

          Processing...

          +
          +
          + ))} +
          +
          + )} + +
          +

          + Processed Documents ({processedDocs.length}) +

          + {processedDocs.length === 0 ? ( +
          + +

          No documents processed yet

          + +
          + ) : ( +
          + {processedDocs.map((doc, idx) => ( +
          +
          + + +
          +

          {doc.name || `Document ${idx + 1}`}

          +

          {DOMAIN_CONFIGS[doc.domain]?.name || selectedDomain}

          +
          + + Processed +
          +
          + ))} +
          + )} +
          +
          +
          + ); + + const renderSettingsView = () => ( +
          +
          +

          Settings

          + +
          +
          +

          Domain Configuration

          +
          +
          + + +
          +
          + +
          + {DOMAIN_CONFIGS[selectedDomain].fileTypes.map(type => ( + + {type} + + ))} +
          +
          +
          +
          + +
          +

          Performance Settings

          +
          +
          + setFastMode(e.target.checked)} + className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" + /> +
          + +

          + Use optimized parameters for 2-3x faster queries. Slightly reduced quality but much better performance. +

          +
          +
          + +
          + setEnableCache(e.target.checked)} + className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" + /> +
          + +

          + Cache query results for 5 minutes. Repeated queries return instantly (100x faster). +

          +
          +
          + +
          + { + setEnableWebSearch(e.target.checked); + if (e.target.checked && webSearchOnly) { + setWebSearchOnly(false); + } + }} + className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" + /> +
          + +

          + Augment document answers with current web search results. +

          +
          +
          + +
          + { + setWebSearchOnly(e.target.checked); + if (e.target.checked) { + setEnableWebSearch(false); + } + }} + className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" + /> +
          + +

          + Skip document retrieval and use only web search (useful when no documents uploaded). +

          +
          +
          + +
          + setEnableQueryImprovement(e.target.checked)} + className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" + /> +
          + +

          + Automatically improve and expand user queries for better results. Disable for faster responses. +

          +
          +
          + +
          + setEnableVerification(e.target.checked)} + className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5" + /> +
          + +

          + Use dual-LLM verification to check answer quality and accuracy. Disable for faster responses. +

          +
          +
          + + +
          +
          + +
          +

          Actions

          +
          + +
          +
          +
          +
          +
          + ); + + // Upload Modal + const renderUploadModal = () => { + if (!showUploadModal) return null; + + return ( +
          +
          +
          +

          Upload Documents

          + +
          + + {/* Mode Toggle */} +
          + + +
          + + {uploadMode === 'file' ? ( +
          + +

          + Drop files here or click to browse +

          +

          + Supported: {DOMAIN_CONFIGS[selectedDomain].fileTypes.join(', ')} +

          + handleFileUpload(e.target.files)} + className="hidden" + /> + +
          + ) : ( +
          +
          + + setUrlInput(e.target.value)} + placeholder="https://example.com/document.pdf" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleUrlUpload(); + } + }} + /> +
          +
          +

          + Supported: PDF, HTML pages (converted to markdown), and other web documents +

          +
          + +
          + )} +
          +
          + ); + }; + + // Error Display + const renderError = () => { + if (!error) return null; + + return ( +
          +
          + +
          +

          {error}

          +
          + +
          +
          + ); + }; + + // ============================================================================= + // Main Render + // ============================================================================= + + return ( +
          + {renderNavigation()} + +
          + {renderSidebar()} + + {currentView === 'app' && renderAppView()} + {currentView === 'files' && renderFilesView()} + {currentView === 'settings' && renderSettingsView()} +
          + + {renderUploadModal()} + {renderError()} +
          + ); +}