/** * 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()}
      ); }