OrgAI / frontend /src /App.js
smaru-4969's picture
Update frontend/src/App.js
8517e66 verified
raw
history blame
61.4 kB
/**
* 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 = () => (
<nav className="bg-white border-b border-gray-200 px-6 py-3">
<div className="flex items-center justify-between max-w-7xl mx-auto">
<div className="flex items-center space-x-8">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
<div className="h-8 flex items-center justify-center">
<img src="/logo.jpg" alt="GlokalAI Logo" className="h-full w-auto" />
</div>
<h1 className="text-xl font-bold text-gray-800">OrgAI</h1>
</div>
<span className="text-sm text-gray-500">/ {DOMAIN_CONFIGS[selectedDomain].name}</span>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => setCurrentView('app')}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
currentView === 'app'
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
}`}
>
App
</button>
<button
onClick={() => setCurrentView('files')}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
currentView === 'files'
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
}`}
>
Files
</button>
<button
onClick={() => setCurrentView('settings')}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
currentView === 'settings'
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
}`}
>
Settings
</button>
</div>
</div>
<button
onClick={() => setShowSidebar(!showSidebar)}
className="p-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md"
>
{showSidebar ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
</div>
</nav>
);
const renderSidebar = () => (
<div className={`${showSidebar ? 'w-64' : 'w-0'} transition-all duration-300 bg-gray-50 border-r border-gray-200 overflow-hidden`}>
<div className="p-4 space-y-4">
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase mb-3">Domains</h3>
<div className="space-y-1">
{Object.entries(DOMAIN_CONFIGS).map(([key, config]) => (
<button
key={key}
onClick={() => setSelectedDomain(key)}
className={`w-full flex items-center space-x-3 px-3 py-2 rounded-lg text-sm transition-colors ${
selectedDomain === key
? `${config.bgColor} ${config.textColor} font-medium`
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<span className="text-lg">{config.icon}</span>
<span className="flex-1 text-left truncate font-sans">{config.name}</span>
</button>
))}
</div>
</div>
{processingDocs.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase mb-3">Processing</h3>
<div className="space-y-2">
{processingDocs.map((doc, idx) => (
<div key={idx} className="flex items-center space-x-2 px-3 py-2 bg-yellow-50 rounded-lg">
<Loader2 className="w-4 h-4 text-yellow-600 animate-spin" />
<span className="text-xs text-yellow-800 truncate flex-1">{doc.name}</span>
</div>
))}
</div>
</div>
)}
{processedDocs.length > 0 && (
<div>
<h3 className="text-xs font-semibold text-gray-500 uppercase mb-3">
Processed Documents ({processedDocs.length})
</h3>
<div className="space-y-1 max-h-64 overflow-y-auto">
{processedDocs.map((doc, idx) => (
<div key={idx} className="flex items-center space-x-2 px-3 py-2 bg-white rounded-lg border border-gray-200 group">
<FileText className="w-4 h-4 text-gray-400" />
<span className="text-xs text-gray-700 truncate flex-1">{doc.name || `Document ${idx + 1}`}</span>
<button
onClick={() => handleDeleteDocument(doc.id, doc.name)}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
<Trash2 className="w-3 h-3 text-gray-400 hover:text-red-600" />
</button>
</div>
))}
</div>
</div>
)}
{messages.length > 0 && (
<div className="pt-4 border-t border-gray-200">
<button
onClick={() => {
if (window.confirm('Clear all chat history? This cannot be undone.')) {
setMessages([]);
window.localStorage.removeItem('chatMessages');
}
}}
className="w-full flex items-center justify-center space-x-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
<span>Clear Chat History</span>
</button>
</div>
)}
</div>
</div>
);
const renderAppView = () => (
<div className="flex-1 flex flex-col bg-white">
{messages.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center px-4">
<div className="text-center max-w-2xl">
{/* <div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-6">
<span className="text-white font-bold text-2xl">O</span>
</div>
<h2 className="text-3xl font-bold text-gray-800 mb-3">Welcome to OrgAI</h2> */}
<div className="flex items-center justify-center space-x-2 mb-6">
<img src="/org-gpt.jpg" alt="OrgAI Logo" className="w-20 h-20 rounded-full" />
<p className="text-3xl font-bold text-gray-800 font-sans">OrgAI</p>
{/* <img src="/logo.jpg" alt="GlokalAI Logo" className="h-10 w-auto" /> */}
</div>
<h3 className="text-3xl font-bold text-gray-800 mb-3 font-sans">Welcome to OrgAI</h3>
<p className="text-gray-600 mb-8">
Upload documents and start chatting to get intelligent responses powered by Advanced Multimodal RAG technology.
</p>
<div className="grid grid-cols-3 gap-4 text-left">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="text-2xl mb-2">📄</div>
<h3 className="font-semibold text-gray-800 mb-1">Upload Documents</h3>
<p className="text-xs text-gray-600">Support for PDF, Word, Excel, CSV and more</p>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<div className="text-2xl mb-2">🔍</div>
<h3 className="font-semibold text-gray-800 mb-1">Ask Questions</h3>
<p className="text-xs text-gray-600">Get accurate answers from your documents</p>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<div className="text-2xl mb-2"></div>
<h3 className="font-semibold text-gray-800 mb-1">Multi-Domain</h3>
<p className="text-xs text-gray-600">Optimized for medical, legal, financial and more</p>
</div>
</div>
</div>
</div>
) : (
<div className="flex-1 overflow-y-auto px-4 py-6">
<div className="max-w-4xl mx-auto space-y-6">
{messages.map((msg, idx) => (
<div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-2xl ${msg.role === 'user' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-800'} rounded-2xl px-4 py-3`}>
<div className="flex items-start space-x-2">
<div className="flex-1">
{msg.role === 'user' ? (
// User messages: simple text
<p className="text-sm whitespace-pre-wrap">
{msg.content}
</p>
) : (
// Assistant messages: rendered markdown
<div className="text-sm prose prose-sm max-w-none prose-headings:mt-3 prose-headings:mb-2 prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-0.5 prose-pre:my-2 prose-pre:bg-gray-800 prose-pre:text-gray-100">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
components={{
// Custom styling for code blocks
code({ node, inline, className, children, ...props }) {
return inline ? (
<code className="bg-gray-200 text-gray-800 px-1.5 py-0.5 rounded text-xs font-mono" {...props}>
{children}
</code>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
// Custom styling for links
a({ node, children, ...props }) {
return (
<a className="text-blue-600 hover:text-blue-800 underline" target="_blank" rel="noopener noreferrer" {...props}>
{children}
</a>
);
},
// Custom styling for headings
h1: ({ node, ...props }) => <h1 className="text-xl font-bold text-gray-900 mt-4 mb-2" {...props} />,
h2: ({ node, ...props }) => <h2 className="text-lg font-bold text-gray-900 mt-3 mb-2" {...props} />,
h3: ({ node, ...props }) => <h3 className="text-base font-semibold text-gray-900 mt-2 mb-1" {...props} />,
// Custom styling for lists
ul: ({ node, ...props }) => <ul className="list-disc list-inside space-y-1 my-2" {...props} />,
ol: ({ node, ...props }) => <ol className="list-decimal list-inside space-y-1 my-2" {...props} />,
// Custom styling for blockquotes
blockquote: ({ node, ...props }) => (
<blockquote className="border-l-4 border-gray-300 pl-4 italic text-gray-700 my-2" {...props} />
),
// Custom styling for tables
table: ({ node, ...props }) => (
<div className="overflow-x-auto my-2">
<table className="min-w-full divide-y divide-gray-200 border border-gray-200" {...props} />
</div>
),
th: ({ node, ...props }) => (
<th className="px-3 py-2 bg-gray-50 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider border-b" {...props} />
),
td: ({ node, ...props }) => (
<td className="px-3 py-2 text-sm text-gray-900 border-b" {...props} />
),
}}
>
{msg.content}
</ReactMarkdown>
{/* {msg.streaming && (
<span className="inline-block w-0.5 h-4 bg-blue-600 ml-1 animate-pulse"></span>
)} */}
</div>
)}
</div>
{msg.streaming && msg.role === 'assistant' && (
<div className="flex items-center space-x-0 text-gray-400 text-sm">
<span>Thinking</span>
<span className="animate-blink" style={{ animationDelay: '0s' }}>.</span>
<span className="animate-blink" style={{ animationDelay: '0.2s' }}>.</span>
<span className="animate-blink" style={{ animationDelay: '0.4s' }}>.</span>
</div>
)}
</div>
{/* Verification Badge
{msg.verification && !msg.streaming && (
<div className={`mt-3 pt-3 border-t ${msg.role === 'user' ? 'border-blue-500' : 'border-gray-300'}`}>
<div className="flex items-center space-x-2 mb-2">
{msg.verification.passed ? (
<CheckCircle className="w-4 h-4 text-green-600" />
) : (
<XCircle className="w-4 h-4 text-red-600" />
)}
<span className={`text-xs font-medium ${
msg.verification.passed ? 'text-green-700' : 'text-red-700'
}`}>
Verification Score: {msg.verification.score?.toFixed(1)}/10
</span>
<span className="text-xs text-gray-500">
({Math.round((msg.verification.confidence || 0) * 100)}% confident)
</span>
</div>
{msg.verification.issues && msg.verification.issues.length > 0 && (
<div className="mt-2">
<p className="text-xs text-gray-600 font-medium mb-1">Issues found:</p>
<ul className="text-xs text-gray-600 space-y-0.5 list-disc list-inside">
{msg.verification.issues.slice(0, 3).map((issue, i) => (
<li key={i}>{issue}</li>
))}
</ul>
</div>
)}
</div>
)} */}
{msg.sources && msg.sources.length > 0 && (
<div className={`mt-3 pt-3 border-t ${msg.role === 'user' ? 'border-blue-500' : 'border-gray-300'}`}>
<p className="text-xs text-gray-600 mb-2">Sources:</p>
{msg.sources.slice(0, 3).map((source, i) => (
<div key={i} className="text-xs text-gray-600 mb-1">
• {source.file_name} (score: {source.score?.toFixed(2)})
</div>
))}
</div>
)}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
</div>
)}
{/* Bottom Input Bar */}
<div className="border-t border-gray-200 bg-white px-4 py-4">
<div className="max-w-4xl mx-auto">
<div className="flex items-center space-x-3">
<button
onClick={() => setShowUploadModal(true)}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Upload className="w-4 h-4" />
<span className="text-sm font-medium">Upload</span>
</button>
<input
type="text"
value={query}
onChange={(e) => 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}
/>
<button
onClick={handleQuery}
disabled={isQuerying || !query.trim()}
className="p-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Send className="w-5 h-5" />
</button>
</div>
{/* Web Search Options */}
<div className="flex items-center justify-center space-x-6 mt-3">
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={enableWebSearch}
onChange={(e) => {
setEnableWebSearch(e.target.checked);
if (e.target.checked && webSearchOnly) {
setWebSearchOnly(false);
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">Enhance with Web Search</span>
</label>
<label className="flex items-center space-x-2 cursor-pointer">
<input
type="checkbox"
checked={webSearchOnly}
onChange={(e) => {
setWebSearchOnly(e.target.checked);
if (e.target.checked) {
setEnableWebSearch(false);
}
}}
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">Web Search Only</span>
</label>
</div>
<p className="text-xs text-gray-500 mt-2 text-center">
Press Enter to send, Shift+Enter for new line
</p>
</div>
</div>
</div>
);
const renderFilesView = () => (
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-5xl mx-auto">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-800">Document Management</h2>
<p className="text-gray-600">Manage your uploaded and processed documents</p>
</div>
<div className="flex space-x-3">
<button
onClick={fetchProcessedDocuments}
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<RefreshCw className="w-4 h-4" />
<span>Refresh</span>
</button>
<button
onClick={() => setShowUploadModal(true)}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Upload className="w-4 h-4" />
<span>Upload Documents</span>
</button>
</div>
</div>
{processingDocs.length > 0 && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-3">Processing Documents</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{processingDocs.map((doc, idx) => (
<div key={idx} className="flex items-center space-x-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<Loader2 className="w-8 h-8 text-yellow-600 animate-spin" />
<div className="flex-1">
<p className="font-medium text-gray-800">{doc.name}</p>
<p className="text-sm text-gray-600">Processing...</p>
</div>
</div>
))}
</div>
</div>
)}
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-3">
Processed Documents ({processedDocs.length})
</h3>
{processedDocs.length === 0 ? (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<FolderOpen className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">No documents processed yet</p>
<button
onClick={() => setShowUploadModal(true)}
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Upload Your First Document
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{processedDocs.map((doc, idx) => (
<div key={idx} className="p-4 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow group">
<div className="flex items-start justify-between mb-3">
<FileText className="w-8 h-8 text-blue-600" />
<button
onClick={() => handleDeleteDocument(doc.id, doc.name)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded"
>
<Trash2 className="w-4 h-4 text-gray-400 hover:text-red-600" />
</button>
</div>
<p className="font-medium text-gray-800 mb-1 truncate" title={doc.name}>{doc.name || `Document ${idx + 1}`}</p>
<p className="text-sm text-gray-600 mb-2">{DOMAIN_CONFIGS[doc.domain]?.name || selectedDomain}</p>
<div className="flex items-center space-x-2">
<CheckCircle className="w-4 h-4 text-green-600" />
<span className="text-xs text-gray-600">Processed</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
const renderSettingsView = () => (
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-3xl mx-auto">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Settings</h2>
<div className="space-y-6">
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Domain Configuration</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Current Domain</label>
<select
value={selectedDomain}
onChange={(e) => setSelectedDomain(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{Object.entries(DOMAIN_CONFIGS).map(([key, config]) => (
<option key={key} value={key}>{config.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Supported File Types</label>
<div className="flex flex-wrap gap-2">
{DOMAIN_CONFIGS[selectedDomain].fileTypes.map(type => (
<span key={type} className="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-full">
{type}
</span>
))}
</div>
</div>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Performance Settings</h3>
<div className="space-y-4">
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="fastMode"
checked={fastMode}
onChange={(e) => setFastMode(e.target.checked)}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
/>
<div className="flex-1">
<label htmlFor="fastMode" className="block text-sm font-medium text-gray-700 cursor-pointer">
Fast Mode
</label>
<p className="text-xs text-gray-600 mt-1">
Use optimized parameters for 2-3x faster queries. Slightly reduced quality but much better performance.
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="enableCache"
checked={enableCache}
onChange={(e) => setEnableCache(e.target.checked)}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
/>
<div className="flex-1">
<label htmlFor="enableCache" className="block text-sm font-medium text-gray-700 cursor-pointer">
Enable Query Caching
</label>
<p className="text-xs text-gray-600 mt-1">
Cache query results for 5 minutes. Repeated queries return instantly (100x faster).
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="enableWebSearch"
checked={enableWebSearch}
onChange={(e) => {
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"
/>
<div className="flex-1">
<label htmlFor="enableWebSearch" className="block text-sm font-medium text-gray-700 cursor-pointer">
Enhance with Web Search
</label>
<p className="text-xs text-gray-600 mt-1">
Augment document answers with current web search results.
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="webSearchOnly"
checked={webSearchOnly}
onChange={(e) => {
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"
/>
<div className="flex-1">
<label htmlFor="webSearchOnly" className="block text-sm font-medium text-gray-700 cursor-pointer">
Web Search Only
</label>
<p className="text-xs text-gray-600 mt-1">
Skip document retrieval and use only web search (useful when no documents uploaded).
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="enableQueryImprovement"
checked={enableQueryImprovement}
onChange={(e) => setEnableQueryImprovement(e.target.checked)}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
/>
<div className="flex-1">
<label htmlFor="enableQueryImprovement" className="block text-sm font-medium text-gray-700 cursor-pointer">
Enable Query Improvement
</label>
<p className="text-xs text-gray-600 mt-1">
Automatically improve and expand user queries for better results. Disable for faster responses.
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="enableVerification"
checked={enableVerification}
onChange={(e) => setEnableVerification(e.target.checked)}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
/>
<div className="flex-1">
<label htmlFor="enableVerification" className="block text-sm font-medium text-gray-700 cursor-pointer">
Enable Answer Verification
</label>
<p className="text-xs text-gray-600 mt-1">
Use dual-LLM verification to check answer quality and accuracy. Disable for faster responses.
</p>
</div>
</div>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Actions</h3>
<div className="space-y-3">
<button
onClick={clearConversation}
className="w-full flex items-center justify-center space-x-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<Trash2 className="w-4 h-4" />
<span>Clear Conversation</span>
</button>
</div>
</div>
</div>
</div>
</div>
);
// Upload Modal
const renderUploadModal = () => {
if (!showUploadModal) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl max-w-2xl w-full p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-800">Upload Documents</h2>
<button
onClick={() => {
setShowUploadModal(false);
setUploadMode('file');
setUrlInput('');
}}
className="p-2 hover:bg-gray-100 rounded-lg"
>
<X className="w-5 h-5 text-gray-600" />
</button>
</div>
{/* Mode Toggle */}
<div className="flex items-center space-x-2 mb-6">
<button
onClick={() => setUploadMode('file')}
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
uploadMode === 'file'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Upload File
</button>
<button
onClick={() => setUploadMode('url')}
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
uploadMode === 'url'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Upload from URL
</button>
</div>
{uploadMode === 'file' ? (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`border-2 border-dashed rounded-xl p-12 text-center transition-colors ${
isDragging
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Upload className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-800 mb-2">
Drop files here or click to browse
</h3>
<p className="text-gray-600 mb-4">
Supported: {DOMAIN_CONFIGS[selectedDomain].fileTypes.join(', ')}
</p>
<input
ref={fileInputRef}
type="file"
multiple
accept={DOMAIN_CONFIGS[selectedDomain].fileTypes.join(',')}
onChange={(e) => handleFileUpload(e.target.files)}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Select Files
</button>
</div>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Enter URL to fetch and process
</label>
<input
type="url"
value={urlInput}
onChange={(e) => 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();
}
}}
/>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>Supported:</strong> PDF, HTML pages (converted to markdown), and other web documents
</p>
</div>
<button
onClick={handleUrlUpload}
disabled={!urlInput.trim()}
className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Fetch and Process URL
</button>
</div>
)}
</div>
</div>
);
};
// Error Display
const renderError = () => {
if (!error) return null;
return (
<div className="fixed bottom-4 right-4 bg-red-50 border border-red-200 rounded-lg p-4 max-w-md shadow-lg">
<div className="flex items-start space-x-3">
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm text-red-800">{error}</p>
</div>
<button
onClick={() => setError(null)}
className="text-red-600 hover:text-red-800"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
);
};
// =============================================================================
// Main Render
// =============================================================================
return (
<div className="h-screen flex flex-col bg-gray-50">
{renderNavigation()}
<div className="flex-1 flex overflow-hidden">
{renderSidebar()}
{currentView === 'app' && renderAppView()}
{currentView === 'files' && renderFilesView()}
{currentView === 'settings' && renderSettingsView()}
</div>
{renderUploadModal()}
{renderError()}
</div>
);
}