| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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';
|
| import {
|
| Send,
|
| Upload,
|
| FileText,
|
| CheckCircle,
|
| XCircle,
|
| Menu,
|
| X,
|
| Loader2,
|
| Trash2,
|
| FolderOpen,
|
| RefreshCw
|
| } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
|
| 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 || '';
|
|
|
|
|
|
|
|
|
|
|
| export default function EnhancedMultiDomainRAG() {
|
|
|
| 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;
|
| }
|
| };
|
|
|
|
|
| const [selectedDomain, setSelectedDomain] = useState(() =>
|
| getFromLocalStorage('selectedDomain', 'medical')
|
| );
|
| const [currentView, setCurrentView] = useState('app');
|
| 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');
|
| 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);
|
|
|
|
|
| const scrollToBottom = () => {
|
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| };
|
|
|
| useEffect(() => {
|
| scrollToBottom();
|
| }, [messages]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('chatMessages', JSON.stringify(messages));
|
| } catch (error) {
|
| console.error('Error saving messages to localStorage:', error);
|
| }
|
| }, [messages]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('selectedDomain', JSON.stringify(selectedDomain));
|
| } catch (error) {
|
| console.error('Error saving domain to localStorage:', error);
|
| }
|
| }, [selectedDomain]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('processingDocs', JSON.stringify(processingDocs));
|
| } catch (error) {
|
| console.error('Error saving processingDocs to localStorage:', error);
|
| }
|
| }, [processingDocs]);
|
|
|
|
|
| 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]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('fastMode', JSON.stringify(fastMode));
|
| } catch (error) {
|
| console.error('Error saving fastMode to localStorage:', error);
|
| }
|
| }, [fastMode]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('enableCache', JSON.stringify(enableCache));
|
| } catch (error) {
|
| console.error('Error saving enableCache to localStorage:', error);
|
| }
|
| }, [enableCache]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('enableQueryImprovement', JSON.stringify(enableQueryImprovement));
|
| } catch (error) {
|
| console.error('Error saving enableQueryImprovement to localStorage:', error);
|
| }
|
| }, [enableQueryImprovement]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('enableVerification', JSON.stringify(enableVerification));
|
| } catch (error) {
|
| console.error('Error saving enableVerification to localStorage:', error);
|
| }
|
| }, [enableVerification]);
|
|
|
|
|
| useEffect(() => {
|
| try {
|
| window.localStorage.setItem('typingSpeed', JSON.stringify(typingSpeed));
|
| } catch (error) {
|
| console.error('Error saving typingSpeed to localStorage:', error);
|
| }
|
| }, [typingSpeed]);
|
|
|
|
|
| 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 || [];
|
|
|
|
|
|
|
| setProcessedDocs(prev => {
|
| const fetchedIds = new Set(fetchedDocs.map(d => d.id));
|
|
|
|
|
| const recentlyAdded = prev.filter(d => d.id && !fetchedIds.has(d.id));
|
|
|
|
|
| return [...fetchedDocs, ...recentlyAdded];
|
| });
|
| }
|
| } catch (err) {
|
| console.error('Error fetching documents:', err);
|
| }
|
| }, [selectedDomain]);
|
|
|
|
|
| const checkProcessingStatus = useCallback(async () => {
|
|
|
| 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') {
|
|
|
| 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]);
|
|
|
|
|
| useEffect(() => {
|
| fetchProcessedDocuments();
|
| }, [selectedDomain, fetchProcessedDocuments]);
|
|
|
|
|
| useEffect(() => {
|
| const interval = setInterval(() => {
|
| if (processingDocs.length > 0) {
|
| checkProcessingStatus();
|
| }
|
| }, 3000);
|
|
|
| return () => clearInterval(interval);
|
| }, [processingDocs, checkProcessingStatus]);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}`);
|
| }
|
| };
|
|
|
|
|
| const startTypingEffect = useCallback((messageIndex, targetTextRef, isStreamingRef) => {
|
|
|
| if (typingIntervalRef.current) {
|
| clearInterval(typingIntervalRef.current);
|
| }
|
|
|
| let displayedLength = 0;
|
|
|
| typingIntervalRef.current = setInterval(() => {
|
| const targetText = targetTextRef.current || '';
|
| const isStillStreaming = isStreamingRef.current;
|
|
|
| if (displayedLength < targetText.length) {
|
|
|
| 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) {
|
|
|
| clearInterval(typingIntervalRef.current);
|
| typingIntervalRef.current = null;
|
| }
|
| }, 30);
|
| }, [typingSpeed]);
|
|
|
|
|
| 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('');
|
|
|
|
|
| const assistantMessageIndex = messages.length + 1;
|
| setMessages(prev => [...prev, {
|
| role: 'assistant',
|
| content: '',
|
| streaming: true,
|
| verification: null
|
| }]);
|
|
|
|
|
| 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}`);
|
| }
|
|
|
|
|
| const reader = response.body.getReader();
|
| const decoder = new TextDecoder();
|
| let buffer = '';
|
|
|
| while (true) {
|
| const { done, value } = await reader.read();
|
|
|
| if (done) {
|
| break;
|
| }
|
|
|
|
|
| buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
| const events = buffer.split('\n\n');
|
| buffer = events.pop() || '';
|
|
|
| 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') {
|
|
|
| fullTextBufferRef.current += data.content;
|
|
|
|
|
| if (!typingStarted && typingSpeed > 0) {
|
| typingStarted = true;
|
| startTypingEffect(assistantMessageIndex, fullTextBufferRef, isStreamingRef);
|
| } else if (typingSpeed === 0) {
|
|
|
| setMessages(prev => {
|
| const newMessages = [...prev];
|
| newMessages[assistantMessageIndex] = {
|
| ...newMessages[assistantMessageIndex],
|
| content: fullTextBufferRef.current
|
| };
|
| return newMessages;
|
| });
|
| }
|
|
|
| } else if (eventType === 'verification') {
|
|
|
| setMessages(prev => {
|
| const newMessages = [...prev];
|
| newMessages[assistantMessageIndex] = {
|
| ...newMessages[assistantMessageIndex],
|
| verification: data.content,
|
| streaming: false
|
| };
|
| return newMessages;
|
| });
|
|
|
| } else if (eventType === 'done') {
|
|
|
| isStreamingRef.current = false;
|
|
|
|
|
| setTimeout(() => {
|
| if (typingIntervalRef.current) {
|
| clearInterval(typingIntervalRef.current);
|
| typingIntervalRef.current = null;
|
| }
|
|
|
|
|
| setMessages(prev => {
|
| const newMessages = [...prev];
|
| newMessages[assistantMessageIndex] = {
|
| ...newMessages[assistantMessageIndex],
|
| streaming: false,
|
| content: fullTextBufferRef.current
|
| };
|
| return newMessages;
|
| });
|
| }, typingSpeed === 0 ? 0 : 500);
|
|
|
| } 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);
|
|
|
|
|
| isStreamingRef.current = false;
|
|
|
|
|
| if (typingIntervalRef.current) {
|
| clearInterval(typingIntervalRef.current);
|
| typingIntervalRef.current = null;
|
| }
|
|
|
|
|
| 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}`);
|
|
|
|
|
| if (typingIntervalRef.current) {
|
| clearInterval(typingIntervalRef.current);
|
| typingIntervalRef.current = null;
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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) {
|
|
|
| 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));
|
|
|
| await fetchProcessedDocuments();
|
| } else {
|
|
|
| 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([]);
|
| };
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| };
|
|
|
|
|
|
|
|
|
|
|
| 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.png" 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>
|
| );
|
|
|
|
|
| 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>
|
| );
|
| };
|
|
|
|
|
| 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>
|
| );
|
| };
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
| );
|
| }
|
|
|