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
- {/*

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

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