smaru-4969 commited on
Commit
7d103f2
·
verified ·
1 Parent(s): 84e9692

Upload App.js

Browse files
Files changed (1) hide show
  1. frontend/src/App.js +1549 -0
frontend/src/App.js ADDED
@@ -0,0 +1,1549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Enhanced Multi-Domain RAG Frontend with Professional Light Theme
3
+ *
4
+ * Features:
5
+ * - Clean, professional light theme design
6
+ * - Multi-domain document upload and querying
7
+ * - Document processing status tracking
8
+ * - Processed documents management
9
+ * - Real-time query responses with streaming
10
+ */
11
+
12
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
13
+ import ReactMarkdown from 'react-markdown';
14
+ import remarkGfm from 'remark-gfm';
15
+ import rehypeHighlight from 'rehype-highlight';
16
+ import 'highlight.js/styles/github.css'; // Code syntax highlighting theme
17
+ import {
18
+ Send,
19
+ Upload,
20
+ FileText,
21
+ CheckCircle,
22
+ XCircle,
23
+ Menu,
24
+ X,
25
+ Loader2,
26
+ Trash2,
27
+ FolderOpen,
28
+ RefreshCw
29
+ } from 'lucide-react';
30
+
31
+ // =============================================================================
32
+ // Domain Configurations
33
+ // =============================================================================
34
+
35
+ const DOMAIN_CONFIGS = {
36
+ medical: {
37
+ name: 'Medical & Healthcare',
38
+ description: 'Medical documents, research papers, clinical guidelines',
39
+ color: '#3b82f6',
40
+ bgColor: 'bg-blue-50',
41
+ borderColor: 'border-blue-200',
42
+ textColor: 'text-blue-700',
43
+ fileTypes: ['.pdf', '.docx', '.xml', '.txt', '.doc', '.csv', '.xlsx'],
44
+ icon: '🏥'
45
+ },
46
+ legal: {
47
+ name: 'Legal & Compliance',
48
+ description: 'Legal documents, contracts, regulations, case law',
49
+ color: '#8b5cf6',
50
+ bgColor: 'bg-purple-50',
51
+ borderColor: 'border-purple-200',
52
+ textColor: 'text-purple-700',
53
+ fileTypes: ['.pdf', '.docx', '.txt', '.doc', '.csv', '.xlsx'],
54
+ icon: '⚖️'
55
+ },
56
+ financial: {
57
+ name: 'Financial & Analytics',
58
+ description: 'Financial reports, analysis, market research',
59
+ color: '#10b981',
60
+ bgColor: 'bg-green-50',
61
+ borderColor: 'border-green-200',
62
+ textColor: 'text-green-700',
63
+ fileTypes: ['.pdf', '.xlsx', '.csv', '.json', '.xls'],
64
+ icon: '💰'
65
+ },
66
+ technical: {
67
+ name: 'Technical Documentation',
68
+ description: 'Technical docs, APIs, code, system architecture',
69
+ color: '#f97316',
70
+ bgColor: 'bg-orange-50',
71
+ borderColor: 'border-orange-200',
72
+ textColor: 'text-orange-700',
73
+ fileTypes: ['.pdf', '.md', '.docx', '.json', '.txt', '.rst', '.csv', '.xlsx'],
74
+ icon: '⚙️'
75
+ },
76
+ academic: {
77
+ name: 'Academic Research',
78
+ description: 'Research papers, academic publications, studies',
79
+ color: '#6366f1',
80
+ bgColor: 'bg-indigo-50',
81
+ borderColor: 'border-indigo-200',
82
+ textColor: 'text-indigo-700',
83
+ fileTypes: ['.pdf', '.docx', '.tex', '.bib', '.txt', '.csv', '.xlsx'],
84
+ icon: '🎓'
85
+ }
86
+ };
87
+
88
+ const API_BASE_URL = process.env.REACT_APP_API_URL || '';
89
+
90
+ // =============================================================================
91
+ // Main Component
92
+ // =============================================================================
93
+
94
+ export default function EnhancedMultiDomainRAG() {
95
+ // Helper function to get from localStorage with fallback
96
+ const getFromLocalStorage = (key, defaultValue) => {
97
+ try {
98
+ const item = window.localStorage.getItem(key);
99
+ return item ? JSON.parse(item) : defaultValue;
100
+ } catch (error) {
101
+ console.error(`Error reading localStorage key "${key}":`, error);
102
+ return defaultValue;
103
+ }
104
+ };
105
+
106
+ // State Management with localStorage persistence
107
+ const [selectedDomain, setSelectedDomain] = useState(() =>
108
+ getFromLocalStorage('selectedDomain', 'medical')
109
+ );
110
+ const [currentView, setCurrentView] = useState('app'); // 'app', 'files', 'settings'
111
+ const [processingDocs, setProcessingDocs] = useState(() =>
112
+ getFromLocalStorage('processingDocs', [])
113
+ );
114
+ const [processedDocs, setProcessedDocs] = useState([]);
115
+ const [query, setQuery] = useState('');
116
+ const [messages, setMessages] = useState(() =>
117
+ getFromLocalStorage('chatMessages', [])
118
+ );
119
+ const [isQuerying, setIsQuerying] = useState(false);
120
+ const [error, setError] = useState(null);
121
+ const [showUploadModal, setShowUploadModal] = useState(false);
122
+ const [isDragging, setIsDragging] = useState(false);
123
+ const [showSidebar, setShowSidebar] = useState(true);
124
+ const [enableWebSearch, setEnableWebSearch] = useState(() =>
125
+ getFromLocalStorage('enableWebSearch', false)
126
+ );
127
+ const [webSearchOnly, setWebSearchOnly] = useState(() =>
128
+ getFromLocalStorage('webSearchOnly', false)
129
+ );
130
+ const [urlInput, setUrlInput] = useState('');
131
+ const [uploadMode, setUploadMode] = useState('file'); // 'file' or 'url'
132
+ const [fastMode, setFastMode] = useState(() =>
133
+ getFromLocalStorage('fastMode', false)
134
+ );
135
+ const [enableCache, setEnableCache] = useState(() =>
136
+ getFromLocalStorage('enableCache', true)
137
+ );
138
+ const [enableQueryImprovement, setEnableQueryImprovement] = useState(() =>
139
+ getFromLocalStorage('enableQueryImprovement', true)
140
+ );
141
+ const [enableVerification, setEnableVerification] = useState(() =>
142
+ getFromLocalStorage('enableVerification', true)
143
+ );
144
+ const [typingSpeed] = useState(0)
145
+
146
+ const messagesEndRef = useRef(null);
147
+ const fileInputRef = useRef(null);
148
+ const typingQueueRef = useRef([]);
149
+ const typingIntervalRef = useRef(null);
150
+
151
+ // Auto-scroll to bottom of messages
152
+ const scrollToBottom = () => {
153
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
154
+ };
155
+
156
+ useEffect(() => {
157
+ scrollToBottom();
158
+ }, [messages]);
159
+
160
+ // Persist messages to localStorage whenever they change
161
+ useEffect(() => {
162
+ try {
163
+ window.localStorage.setItem('chatMessages', JSON.stringify(messages));
164
+ } catch (error) {
165
+ console.error('Error saving messages to localStorage:', error);
166
+ }
167
+ }, [messages]);
168
+
169
+ // Persist selectedDomain to localStorage
170
+ useEffect(() => {
171
+ try {
172
+ window.localStorage.setItem('selectedDomain', JSON.stringify(selectedDomain));
173
+ } catch (error) {
174
+ console.error('Error saving domain to localStorage:', error);
175
+ }
176
+ }, [selectedDomain]);
177
+
178
+ // Persist processingDocs to localStorage
179
+ useEffect(() => {
180
+ try {
181
+ window.localStorage.setItem('processingDocs', JSON.stringify(processingDocs));
182
+ } catch (error) {
183
+ console.error('Error saving processingDocs to localStorage:', error);
184
+ }
185
+ }, [processingDocs]);
186
+
187
+ // Persist web search settings to localStorage
188
+ useEffect(() => {
189
+ try {
190
+ window.localStorage.setItem('enableWebSearch', JSON.stringify(enableWebSearch));
191
+ } catch (error) {
192
+ console.error('Error saving enableWebSearch to localStorage:', error);
193
+ }
194
+ }, [enableWebSearch]);
195
+
196
+ useEffect(() => {
197
+ try {
198
+ window.localStorage.setItem('webSearchOnly', JSON.stringify(webSearchOnly));
199
+ } catch (error) {
200
+ console.error('Error saving webSearchOnly to localStorage:', error);
201
+ }
202
+ }, [webSearchOnly]);
203
+
204
+ // Persist fast mode setting to localStorage
205
+ useEffect(() => {
206
+ try {
207
+ window.localStorage.setItem('fastMode', JSON.stringify(fastMode));
208
+ } catch (error) {
209
+ console.error('Error saving fastMode to localStorage:', error);
210
+ }
211
+ }, [fastMode]);
212
+
213
+ // Persist cache setting to localStorage
214
+ useEffect(() => {
215
+ try {
216
+ window.localStorage.setItem('enableCache', JSON.stringify(enableCache));
217
+ } catch (error) {
218
+ console.error('Error saving enableCache to localStorage:', error);
219
+ }
220
+ }, [enableCache]);
221
+
222
+ // Persist query improvement setting to localStorage
223
+ useEffect(() => {
224
+ try {
225
+ window.localStorage.setItem('enableQueryImprovement', JSON.stringify(enableQueryImprovement));
226
+ } catch (error) {
227
+ console.error('Error saving enableQueryImprovement to localStorage:', error);
228
+ }
229
+ }, [enableQueryImprovement]);
230
+
231
+ // Persist verification setting to localStorage
232
+ useEffect(() => {
233
+ try {
234
+ window.localStorage.setItem('enableVerification', JSON.stringify(enableVerification));
235
+ } catch (error) {
236
+ console.error('Error saving enableVerification to localStorage:', error);
237
+ }
238
+ }, [enableVerification]);
239
+
240
+ // Persist typing speed setting to localStorage
241
+ useEffect(() => {
242
+ try {
243
+ window.localStorage.setItem('typingSpeed', JSON.stringify(typingSpeed));
244
+ } catch (error) {
245
+ console.error('Error saving typingSpeed to localStorage:', error);
246
+ }
247
+ }, [typingSpeed]);
248
+
249
+ // Fetch processed documents function with useCallback
250
+ const fetchProcessedDocuments = useCallback(async () => {
251
+ try {
252
+ const response = await fetch(`${API_BASE_URL}/documents?domain=${selectedDomain}`);
253
+ if (response.ok) {
254
+ const data = await response.json();
255
+ const fetchedDocs = data.documents || [];
256
+
257
+ // Merge with existing docs to avoid duplicates
258
+ // Keep docs that exist in both, prefer fetched version for consistency
259
+ setProcessedDocs(prev => {
260
+ const fetchedIds = new Set(fetchedDocs.map(d => d.id));
261
+
262
+ // Keep docs from prev that aren't in fetched (recently added via status check)
263
+ const recentlyAdded = prev.filter(d => d.id && !fetchedIds.has(d.id));
264
+
265
+ // Combine with fetched docs
266
+ return [...fetchedDocs, ...recentlyAdded];
267
+ });
268
+ }
269
+ } catch (err) {
270
+ console.error('Error fetching documents:', err);
271
+ }
272
+ }, [selectedDomain]);
273
+
274
+ // Check processing status function with useCallback
275
+ const checkProcessingStatus = useCallback(async () => {
276
+ // Update processing docs status
277
+ const updatedProcessing = [];
278
+ for (const doc of processingDocs) {
279
+ try {
280
+ const response = await fetch(`${API_BASE_URL}/status/${doc.processingId}`);
281
+ if (response.ok) {
282
+ const status = await response.json();
283
+ if (status.status === 'completed') {
284
+ // Move to processed - use processingId as id for deletion
285
+ setProcessedDocs(prev => [...prev, {
286
+ ...doc,
287
+ id: doc.processingId,
288
+ status: 'completed'
289
+ }]);
290
+ } else if (status.status === 'failed') {
291
+ setError(`Processing failed for ${doc.name}: ${status.error}`);
292
+ } else {
293
+ updatedProcessing.push({ ...doc, status: status.status });
294
+ }
295
+ }
296
+ } catch (err) {
297
+ console.error('Error checking status:', err);
298
+ }
299
+ }
300
+ setProcessingDocs(updatedProcessing);
301
+ }, [processingDocs]);
302
+
303
+ // Fetch processed documents on domain change
304
+ useEffect(() => {
305
+ fetchProcessedDocuments();
306
+ }, [selectedDomain, fetchProcessedDocuments]);
307
+
308
+ // Poll for document processing status
309
+ useEffect(() => {
310
+ const interval = setInterval(() => {
311
+ if (processingDocs.length > 0) {
312
+ checkProcessingStatus();
313
+ }
314
+ }, 3000);
315
+
316
+ return () => clearInterval(interval);
317
+ }, [processingDocs, checkProcessingStatus]);
318
+
319
+ // =============================================================================
320
+ // API Functions
321
+ // =============================================================================
322
+
323
+
324
+ const handleFileUpload = async (files) => {
325
+ if (!files || files.length === 0) return;
326
+
327
+ setError(null);
328
+ const newProcessingDocs = [];
329
+
330
+ for (const file of files) {
331
+ const fileExt = '.' + file.name.split('.').pop().toLowerCase();
332
+ const allowedTypes = DOMAIN_CONFIGS[selectedDomain].fileTypes;
333
+
334
+ if (!allowedTypes.includes(fileExt)) {
335
+ setError(`File type ${fileExt} not supported for ${selectedDomain} domain. Allowed: ${allowedTypes.join(', ')}`);
336
+ continue;
337
+ }
338
+
339
+ const formData = new FormData();
340
+ formData.append('file', file);
341
+ formData.append('domain', selectedDomain);
342
+
343
+ try {
344
+ const response = await fetch(`${API_BASE_URL}/upload`, {
345
+ method: 'POST',
346
+ body: formData
347
+ });
348
+
349
+ const data = await response.json();
350
+ if (response.ok) {
351
+ newProcessingDocs.push({
352
+ name: file.name,
353
+ domain: selectedDomain,
354
+ processingId: data.processing_id,
355
+ status: 'processing',
356
+ uploadedAt: new Date().toISOString()
357
+ });
358
+ } else {
359
+ setError(data.detail || 'Upload failed');
360
+ }
361
+ } catch (err) {
362
+ console.error('Upload error:', err);
363
+ setError(`Failed to upload ${file.name}: ${err.message}`);
364
+ }
365
+ }
366
+
367
+ setProcessingDocs(prev => [...prev, ...newProcessingDocs]);
368
+ setShowUploadModal(false);
369
+ };
370
+
371
+ const handleUrlUpload = async () => {
372
+ if (!urlInput.trim()) {
373
+ setError('Please enter a valid URL');
374
+ return;
375
+ }
376
+
377
+ setError(null);
378
+
379
+ try {
380
+ const response = await fetch(`${API_BASE_URL}/upload-url`, {
381
+ method: 'POST',
382
+ headers: { 'Content-Type': 'application/json' },
383
+ body: JSON.stringify({
384
+ url: urlInput,
385
+ domain: selectedDomain,
386
+ convert_to_markdown: true
387
+ })
388
+ });
389
+
390
+ const data = await response.json();
391
+ if (response.ok) {
392
+ setProcessingDocs(prev => [...prev, {
393
+ name: urlInput,
394
+ domain: selectedDomain,
395
+ processingId: data.processing_id,
396
+ status: 'processing',
397
+ uploadedAt: new Date().toISOString()
398
+ }]);
399
+ setUrlInput('');
400
+ setShowUploadModal(false);
401
+ } else {
402
+ setError(data.detail || 'URL upload failed');
403
+ }
404
+ } catch (err) {
405
+ console.error('URL upload error:', err);
406
+ setError(`Failed to upload URL: ${err.message}`);
407
+ }
408
+ };
409
+
410
+ // Typing effect function with queue-based approach
411
+ const startTypingEffect = useCallback((messageIndex, targetTextRef, isStreamingRef) => {
412
+ // Clear any existing typing interval
413
+ if (typingIntervalRef.current) {
414
+ clearInterval(typingIntervalRef.current);
415
+ }
416
+
417
+ let displayedLength = 0;
418
+
419
+ typingIntervalRef.current = setInterval(() => {
420
+ const targetText = targetTextRef.current || '';
421
+ const isStillStreaming = isStreamingRef.current;
422
+
423
+ if (displayedLength < targetText.length) {
424
+ // Add characters based on typing speed (higher = faster)
425
+ const charsToAdd = Math.max(1, Math.floor(typingSpeed / 10));
426
+ displayedLength = Math.min(displayedLength + charsToAdd, targetText.length);
427
+
428
+ setMessages(prev => {
429
+ const newMessages = [...prev];
430
+ if (newMessages[messageIndex]) {
431
+ newMessages[messageIndex] = {
432
+ ...newMessages[messageIndex],
433
+ content: targetText.substring(0, displayedLength)
434
+ };
435
+ }
436
+ return newMessages;
437
+ });
438
+ } else if (!isStillStreaming && displayedLength >= targetText.length) {
439
+ // If we've caught up and streaming is done, clear the interval
440
+ clearInterval(typingIntervalRef.current);
441
+ typingIntervalRef.current = null;
442
+ }
443
+ }, 30); // Update every 30ms for smoother animation
444
+ }, [typingSpeed]);
445
+
446
+ // Cleanup typing interval on unmount
447
+ useEffect(() => {
448
+ return () => {
449
+ if (typingIntervalRef.current) {
450
+ clearInterval(typingIntervalRef.current);
451
+ }
452
+ };
453
+ }, []);
454
+
455
+ const handleQuery = async () => {
456
+ if (!query.trim()) return;
457
+
458
+ setError(null);
459
+ setIsQuerying(true);
460
+
461
+ const userMessage = { role: 'user', content: query };
462
+ setMessages(prev => [...prev, userMessage]);
463
+ const currentQuery = query;
464
+ setQuery('');
465
+
466
+ // Create placeholder for streaming response
467
+ const assistantMessageIndex = messages.length + 1;
468
+ setMessages(prev => [...prev, {
469
+ role: 'assistant',
470
+ content: '',
471
+ streaming: true,
472
+ verification: null
473
+ }]);
474
+
475
+ // Use ref to store the full text buffer so typing effect can access it
476
+ const fullTextBufferRef = { current: '' };
477
+ const isStreamingRef = { current: true };
478
+ let typingStarted = false;
479
+
480
+ try {
481
+ const response = await fetch(`${API_BASE_URL}/query/stream`, {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'application/json' },
484
+ body: JSON.stringify({
485
+ query: currentQuery,
486
+ domain: selectedDomain,
487
+ enable_verification: true,
488
+ enable_web_search: enableWebSearch,
489
+ web_search_only: webSearchOnly,
490
+ fast_mode: fastMode,
491
+ enable_cache: enableCache,
492
+ enable_query_improvement: enableQueryImprovement,
493
+ enable_verification_check: enableVerification
494
+ })
495
+ });
496
+
497
+ if (!response.ok) {
498
+ throw new Error(`HTTP error! status: ${response.status}`);
499
+ }
500
+
501
+ // Read the stream
502
+ const reader = response.body.getReader();
503
+ const decoder = new TextDecoder();
504
+ let buffer = '';
505
+
506
+ while (true) {
507
+ const { done, value } = await reader.read();
508
+
509
+ if (done) {
510
+ break;
511
+ }
512
+
513
+ // Decode chunk
514
+ buffer += decoder.decode(value, { stream: true });
515
+
516
+ // Process complete SSE events
517
+ const events = buffer.split('\n\n');
518
+ buffer = events.pop() || ''; // Keep incomplete event in buffer
519
+
520
+ for (const event of events) {
521
+ if (!event.trim()) continue;
522
+
523
+ const lines = event.split('\n');
524
+ let eventType = 'message';
525
+ let eventData = '';
526
+
527
+ for (const line of lines) {
528
+ if (line.startsWith('event:')) {
529
+ eventType = line.substring(6).trim();
530
+ } else if (line.startsWith('data:')) {
531
+ eventData = line.substring(5).trim();
532
+ }
533
+ }
534
+
535
+ if (eventData) {
536
+ const data = JSON.parse(eventData);
537
+
538
+ if (eventType === 'token') {
539
+ // Add to buffer ref
540
+ fullTextBufferRef.current += data.content;
541
+
542
+ // Start typing effect once if speed > 0
543
+ if (!typingStarted && typingSpeed > 0) {
544
+ typingStarted = true;
545
+ startTypingEffect(assistantMessageIndex, fullTextBufferRef, isStreamingRef);
546
+ } else if (typingSpeed === 0) {
547
+ // Instant display if typing speed is 0
548
+ setMessages(prev => {
549
+ const newMessages = [...prev];
550
+ newMessages[assistantMessageIndex] = {
551
+ ...newMessages[assistantMessageIndex],
552
+ content: fullTextBufferRef.current
553
+ };
554
+ return newMessages;
555
+ });
556
+ }
557
+
558
+ } else if (eventType === 'verification') {
559
+ // Add verification info to message
560
+ setMessages(prev => {
561
+ const newMessages = [...prev];
562
+ newMessages[assistantMessageIndex] = {
563
+ ...newMessages[assistantMessageIndex],
564
+ verification: data.content,
565
+ streaming: false
566
+ };
567
+ return newMessages;
568
+ });
569
+
570
+ } else if (eventType === 'done') {
571
+ // Mark streaming as complete
572
+ isStreamingRef.current = false;
573
+
574
+ // Wait a bit for typing to catch up, then ensure final text is shown
575
+ setTimeout(() => {
576
+ if (typingIntervalRef.current) {
577
+ clearInterval(typingIntervalRef.current);
578
+ typingIntervalRef.current = null;
579
+ }
580
+
581
+ // Set final content and mark as complete
582
+ setMessages(prev => {
583
+ const newMessages = [...prev];
584
+ newMessages[assistantMessageIndex] = {
585
+ ...newMessages[assistantMessageIndex],
586
+ streaming: false,
587
+ content: fullTextBufferRef.current
588
+ };
589
+ return newMessages;
590
+ });
591
+ }, typingSpeed === 0 ? 0 : 500); // Wait 500ms for typing to finish
592
+
593
+ } else if (eventType === 'error') {
594
+ const errorMessage = data.content.message || 'An error occurred while processing your query';
595
+ const errorSuggestion = data.content.suggestion || '';
596
+ setError(errorSuggestion ? `${errorMessage}\n\n${errorSuggestion}` : errorMessage);
597
+
598
+ // Mark streaming as complete
599
+ isStreamingRef.current = false;
600
+
601
+ // Clear typing interval
602
+ if (typingIntervalRef.current) {
603
+ clearInterval(typingIntervalRef.current);
604
+ typingIntervalRef.current = null;
605
+ }
606
+
607
+ // Mark message as error with helpful message
608
+ setMessages(prev => {
609
+ const newMessages = [...prev];
610
+ newMessages[assistantMessageIndex] = {
611
+ ...newMessages[assistantMessageIndex],
612
+ content: fullTextBufferRef.current || errorMessage,
613
+ streaming: false,
614
+ error: true
615
+ };
616
+ return newMessages;
617
+ });
618
+ break;
619
+ }
620
+ }
621
+ }
622
+ }
623
+
624
+ } catch (err) {
625
+ console.error('Query error:', err);
626
+ setError(`Query failed: ${err.message}`);
627
+
628
+ // Clear typing interval
629
+ if (typingIntervalRef.current) {
630
+ clearInterval(typingIntervalRef.current);
631
+ typingIntervalRef.current = null;
632
+ }
633
+
634
+ // Update message with error
635
+ setMessages(prev => {
636
+ const newMessages = [...prev];
637
+ if (newMessages[assistantMessageIndex]) {
638
+ newMessages[assistantMessageIndex] = {
639
+ ...newMessages[assistantMessageIndex],
640
+ content: newMessages[assistantMessageIndex].content || '[Error occurred]',
641
+ streaming: false,
642
+ error: true
643
+ };
644
+ }
645
+ return newMessages;
646
+ });
647
+ } finally {
648
+ setIsQuerying(false);
649
+ }
650
+ };
651
+
652
+ const handleKeyPress = (e) => {
653
+ if (e.key === 'Enter' && !e.shiftKey) {
654
+ e.preventDefault();
655
+ handleQuery();
656
+ }
657
+ };
658
+
659
+ const handleDeleteDocument = async (docId, docName) => {
660
+ if (!docId) {
661
+ console.error('Document ID is undefined');
662
+ setError('Cannot delete document: ID is missing');
663
+ return;
664
+ }
665
+
666
+ // Show confirmation dialog
667
+ const confirmed = window.confirm(
668
+ `Are you sure you want to delete "${docName || 'this document'}"?\n\n` +
669
+ `This will permanently remove:\n` +
670
+ `• All text chunks and embeddings\n` +
671
+ `• Knowledge graph entities and relationships\n` +
672
+ `• Vector database entries\n` +
673
+ `• Physical files\n\n` +
674
+ `This action cannot be undone.`
675
+ );
676
+
677
+ if (!confirmed) {
678
+ return;
679
+ }
680
+
681
+ try {
682
+ const response = await fetch(`${API_BASE_URL}/documents/${docId}`, {
683
+ method: 'DELETE'
684
+ });
685
+
686
+ const data = await response.json();
687
+
688
+ if (response.ok && data.success) {
689
+ // Show success message with deletion details
690
+ const report = data.report;
691
+ const summary = report?.summary || {};
692
+
693
+ alert(
694
+ `✓ Document deleted successfully!\n\n` +
695
+ `Removed from knowledge base:\n` +
696
+ `• ${summary.chunks_deleted || 0} text chunks\n` +
697
+ `• ${summary.entities_deleted || 0} knowledge graph entities\n` +
698
+ `• ${summary.relationships_deleted || 0} relationships\n` +
699
+ `• ${summary.vectors_deleted || 0} embedding vectors\n` +
700
+ `• ${summary.files_deleted || 0} physical files\n` +
701
+ `• ${summary.directories_deleted || 0} directories`
702
+ );
703
+
704
+ setProcessedDocs(prev => prev.filter(doc => doc.id !== docId));
705
+ // Also refresh the documents list to ensure consistency
706
+ await fetchProcessedDocuments();
707
+ } else {
708
+ // Show error with details if available
709
+ const errorMsg = data.message || data.detail || 'Failed to delete document';
710
+ const errors = data.report?.errors || [];
711
+
712
+ setError(
713
+ errorMsg +
714
+ (errors.length > 0 ? `\n\nErrors: ${errors.join(', ')}` : '')
715
+ );
716
+ }
717
+ } catch (err) {
718
+ console.error('Error deleting document:', err);
719
+ setError('Failed to delete document: ' + err.message);
720
+ }
721
+ };
722
+
723
+ const clearConversation = () => {
724
+ setMessages([]);
725
+ };
726
+
727
+ // =============================================================================
728
+ // Drag and Drop Handlers
729
+ // =============================================================================
730
+
731
+ const handleDragOver = (e) => {
732
+ e.preventDefault();
733
+ setIsDragging(true);
734
+ };
735
+
736
+ const handleDragLeave = (e) => {
737
+ e.preventDefault();
738
+ setIsDragging(false);
739
+ };
740
+
741
+ const handleDrop = (e) => {
742
+ e.preventDefault();
743
+ setIsDragging(false);
744
+ handleFileUpload(e.dataTransfer.files);
745
+ };
746
+
747
+ // =============================================================================
748
+ // Render Functions
749
+ // =============================================================================
750
+
751
+ const renderNavigation = () => (
752
+ <nav className="bg-white border-b border-gray-200 px-6 py-3">
753
+ <div className="flex items-center justify-between max-w-7xl mx-auto">
754
+ <div className="flex items-center space-x-8">
755
+ <div className="flex items-center space-x-3">
756
+ <div className="flex items-center space-x-2">
757
+ <div className="h-8 flex items-center justify-center">
758
+ <img src="/logo.jpg" alt="GlokalAI Logo" className="h-full w-auto" />
759
+ </div>
760
+ <h1 className="text-xl font-bold text-gray-800">OrgAI</h1>
761
+ </div>
762
+ <span className="text-sm text-gray-500">/ {DOMAIN_CONFIGS[selectedDomain].name}</span>
763
+ </div>
764
+
765
+ <div className="flex items-center space-x-1">
766
+ <button
767
+ onClick={() => setCurrentView('app')}
768
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
769
+ currentView === 'app'
770
+ ? 'text-blue-600 bg-blue-50'
771
+ : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
772
+ }`}
773
+ >
774
+ App
775
+ </button>
776
+ <button
777
+ onClick={() => setCurrentView('files')}
778
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
779
+ currentView === 'files'
780
+ ? 'text-blue-600 bg-blue-50'
781
+ : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
782
+ }`}
783
+ >
784
+ Files
785
+ </button>
786
+ <button
787
+ onClick={() => setCurrentView('settings')}
788
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
789
+ currentView === 'settings'
790
+ ? 'text-blue-600 bg-blue-50'
791
+ : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50'
792
+ }`}
793
+ >
794
+ Settings
795
+ </button>
796
+ </div>
797
+ </div>
798
+
799
+ <button
800
+ onClick={() => setShowSidebar(!showSidebar)}
801
+ className="p-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md"
802
+ >
803
+ {showSidebar ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
804
+ </button>
805
+ </div>
806
+ </nav>
807
+ );
808
+
809
+ const renderSidebar = () => (
810
+ <div className={`${showSidebar ? 'w-64' : 'w-0'} transition-all duration-300 bg-gray-50 border-r border-gray-200 overflow-hidden`}>
811
+ <div className="p-4 space-y-4">
812
+ <div>
813
+ <h3 className="text-xs font-semibold text-gray-500 uppercase mb-3">Domains</h3>
814
+ <div className="space-y-1">
815
+ {Object.entries(DOMAIN_CONFIGS).map(([key, config]) => (
816
+ <button
817
+ key={key}
818
+ onClick={() => setSelectedDomain(key)}
819
+ className={`w-full flex items-center space-x-3 px-3 py-2 rounded-lg text-sm transition-colors ${
820
+ selectedDomain === key
821
+ ? `${config.bgColor} ${config.textColor} font-medium`
822
+ : 'text-gray-700 hover:bg-gray-100'
823
+ }`}
824
+ >
825
+ <span className="text-lg">{config.icon}</span>
826
+ <span className="flex-1 text-left truncate font-sans">{config.name}</span>
827
+ </button>
828
+ ))}
829
+ </div>
830
+ </div>
831
+
832
+ {processingDocs.length > 0 && (
833
+ <div>
834
+ <h3 className="text-xs font-semibold text-gray-500 uppercase mb-3">Processing</h3>
835
+ <div className="space-y-2">
836
+ {processingDocs.map((doc, idx) => (
837
+ <div key={idx} className="flex items-center space-x-2 px-3 py-2 bg-yellow-50 rounded-lg">
838
+ <Loader2 className="w-4 h-4 text-yellow-600 animate-spin" />
839
+ <span className="text-xs text-yellow-800 truncate flex-1">{doc.name}</span>
840
+ </div>
841
+ ))}
842
+ </div>
843
+ </div>
844
+ )}
845
+
846
+ {processedDocs.length > 0 && (
847
+ <div>
848
+ <h3 className="text-xs font-semibold text-gray-500 uppercase mb-3">
849
+ Processed Documents ({processedDocs.length})
850
+ </h3>
851
+ <div className="space-y-1 max-h-64 overflow-y-auto">
852
+ {processedDocs.map((doc, idx) => (
853
+ <div key={idx} className="flex items-center space-x-2 px-3 py-2 bg-white rounded-lg border border-gray-200 group">
854
+ <FileText className="w-4 h-4 text-gray-400" />
855
+ <span className="text-xs text-gray-700 truncate flex-1">{doc.name || `Document ${idx + 1}`}</span>
856
+ <button
857
+ onClick={() => handleDeleteDocument(doc.id, doc.name)}
858
+ className="opacity-0 group-hover:opacity-100 transition-opacity"
859
+ >
860
+ <Trash2 className="w-3 h-3 text-gray-400 hover:text-red-600" />
861
+ </button>
862
+ </div>
863
+ ))}
864
+ </div>
865
+ </div>
866
+ )}
867
+
868
+ {messages.length > 0 && (
869
+ <div className="pt-4 border-t border-gray-200">
870
+ <button
871
+ onClick={() => {
872
+ if (window.confirm('Clear all chat history? This cannot be undone.')) {
873
+ setMessages([]);
874
+ window.localStorage.removeItem('chatMessages');
875
+ }
876
+ }}
877
+ 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"
878
+ >
879
+ <Trash2 className="w-4 h-4" />
880
+ <span>Clear Chat History</span>
881
+ </button>
882
+ </div>
883
+ )}
884
+ </div>
885
+ </div>
886
+ );
887
+
888
+ const renderAppView = () => (
889
+ <div className="flex-1 flex flex-col bg-white">
890
+ {messages.length === 0 ? (
891
+ <div className="flex-1 flex flex-col items-center justify-center px-4">
892
+ <div className="text-center max-w-2xl">
893
+ {/* <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">
894
+ <span className="text-white font-bold text-2xl">O</span>
895
+ </div>
896
+ <h2 className="text-3xl font-bold text-gray-800 mb-3">Welcome to OrgAI</h2> */}
897
+ <div className="flex items-center justify-center space-x-2 mb-6">
898
+ <img src="/org-gpt.png" alt="OrgAI Logo" className="w-20 h-20 rounded-full" />
899
+ <p className="text-3xl font-bold text-gray-800 font-sans">OrgAI</p>
900
+ {/* <img src="/logo.jpg" alt="GlokalAI Logo" className="h-10 w-auto" /> */}
901
+ </div>
902
+ <h3 className="text-3xl font-bold text-gray-800 mb-3 font-sans">Welcome to OrgAI</h3>
903
+ <p className="text-gray-600 mb-8">
904
+ Upload documents and start chatting to get intelligent responses powered by Advanced Multimodal RAG technology.
905
+ </p>
906
+
907
+ <div className="grid grid-cols-3 gap-4 text-left">
908
+ <div className="p-4 bg-gray-50 rounded-lg">
909
+ <div className="text-2xl mb-2">📄</div>
910
+ <h3 className="font-semibold text-gray-800 mb-1">Upload Documents</h3>
911
+ <p className="text-xs text-gray-600">Support for PDF, Word, Excel, CSV and more</p>
912
+ </div>
913
+ <div className="p-4 bg-gray-50 rounded-lg">
914
+ <div className="text-2xl mb-2">🔍</div>
915
+ <h3 className="font-semibold text-gray-800 mb-1">Ask Questions</h3>
916
+ <p className="text-xs text-gray-600">Get accurate answers from your documents</p>
917
+ </div>
918
+ <div className="p-4 bg-gray-50 rounded-lg">
919
+ <div className="text-2xl mb-2">⚡</div>
920
+ <h3 className="font-semibold text-gray-800 mb-1">Multi-Domain</h3>
921
+ <p className="text-xs text-gray-600">Optimized for medical, legal, financial and more</p>
922
+ </div>
923
+ </div>
924
+ </div>
925
+ </div>
926
+ ) : (
927
+ <div className="flex-1 overflow-y-auto px-4 py-6">
928
+ <div className="max-w-4xl mx-auto space-y-6">
929
+ {messages.map((msg, idx) => (
930
+ <div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
931
+ <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`}>
932
+ <div className="flex items-start space-x-2">
933
+ <div className="flex-1">
934
+ {msg.role === 'user' ? (
935
+ // User messages: simple text
936
+ <p className="text-sm whitespace-pre-wrap">
937
+ {msg.content}
938
+ </p>
939
+ ) : (
940
+ // Assistant messages: rendered markdown
941
+ <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">
942
+ <ReactMarkdown
943
+ remarkPlugins={[remarkGfm]}
944
+ rehypePlugins={[rehypeHighlight]}
945
+ components={{
946
+ // Custom styling for code blocks
947
+ code({ node, inline, className, children, ...props }) {
948
+ return inline ? (
949
+ <code className="bg-gray-200 text-gray-800 px-1.5 py-0.5 rounded text-xs font-mono" {...props}>
950
+ {children}
951
+ </code>
952
+ ) : (
953
+ <code className={className} {...props}>
954
+ {children}
955
+ </code>
956
+ );
957
+ },
958
+ // Custom styling for links
959
+ a({ node, children, ...props }) {
960
+ return (
961
+ <a className="text-blue-600 hover:text-blue-800 underline" target="_blank" rel="noopener noreferrer" {...props}>
962
+ {children}
963
+ </a>
964
+ );
965
+ },
966
+ // Custom styling for headings
967
+ h1: ({ node, ...props }) => <h1 className="text-xl font-bold text-gray-900 mt-4 mb-2" {...props} />,
968
+ h2: ({ node, ...props }) => <h2 className="text-lg font-bold text-gray-900 mt-3 mb-2" {...props} />,
969
+ h3: ({ node, ...props }) => <h3 className="text-base font-semibold text-gray-900 mt-2 mb-1" {...props} />,
970
+ // Custom styling for lists
971
+ ul: ({ node, ...props }) => <ul className="list-disc list-inside space-y-1 my-2" {...props} />,
972
+ ol: ({ node, ...props }) => <ol className="list-decimal list-inside space-y-1 my-2" {...props} />,
973
+ // Custom styling for blockquotes
974
+ blockquote: ({ node, ...props }) => (
975
+ <blockquote className="border-l-4 border-gray-300 pl-4 italic text-gray-700 my-2" {...props} />
976
+ ),
977
+ // Custom styling for tables
978
+ table: ({ node, ...props }) => (
979
+ <div className="overflow-x-auto my-2">
980
+ <table className="min-w-full divide-y divide-gray-200 border border-gray-200" {...props} />
981
+ </div>
982
+ ),
983
+ th: ({ node, ...props }) => (
984
+ <th className="px-3 py-2 bg-gray-50 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider border-b" {...props} />
985
+ ),
986
+ td: ({ node, ...props }) => (
987
+ <td className="px-3 py-2 text-sm text-gray-900 border-b" {...props} />
988
+ ),
989
+ }}
990
+ >
991
+ {msg.content}
992
+ </ReactMarkdown>
993
+ {/* {msg.streaming && (
994
+ <span className="inline-block w-0.5 h-4 bg-blue-600 ml-1 animate-pulse"></span>
995
+ )} */}
996
+ </div>
997
+ )}
998
+ </div>
999
+ {msg.streaming && msg.role === 'assistant' && (
1000
+ <div className="flex items-center space-x-0 text-gray-400 text-sm">
1001
+ <span>Thinking</span>
1002
+ <span className="animate-blink" style={{ animationDelay: '0s' }}>.</span>
1003
+ <span className="animate-blink" style={{ animationDelay: '0.2s' }}>.</span>
1004
+ <span className="animate-blink" style={{ animationDelay: '0.4s' }}>.</span>
1005
+ </div>
1006
+ )}
1007
+ </div>
1008
+
1009
+ {/* Verification Badge
1010
+ {msg.verification && !msg.streaming && (
1011
+ <div className={`mt-3 pt-3 border-t ${msg.role === 'user' ? 'border-blue-500' : 'border-gray-300'}`}>
1012
+ <div className="flex items-center space-x-2 mb-2">
1013
+ {msg.verification.passed ? (
1014
+ <CheckCircle className="w-4 h-4 text-green-600" />
1015
+ ) : (
1016
+ <XCircle className="w-4 h-4 text-red-600" />
1017
+ )}
1018
+ <span className={`text-xs font-medium ${
1019
+ msg.verification.passed ? 'text-green-700' : 'text-red-700'
1020
+ }`}>
1021
+ Verification Score: {msg.verification.score?.toFixed(1)}/10
1022
+ </span>
1023
+ <span className="text-xs text-gray-500">
1024
+ ({Math.round((msg.verification.confidence || 0) * 100)}% confident)
1025
+ </span>
1026
+ </div>
1027
+ {msg.verification.issues && msg.verification.issues.length > 0 && (
1028
+ <div className="mt-2">
1029
+ <p className="text-xs text-gray-600 font-medium mb-1">Issues found:</p>
1030
+ <ul className="text-xs text-gray-600 space-y-0.5 list-disc list-inside">
1031
+ {msg.verification.issues.slice(0, 3).map((issue, i) => (
1032
+ <li key={i}>{issue}</li>
1033
+ ))}
1034
+ </ul>
1035
+ </div>
1036
+ )}
1037
+ </div>
1038
+ )} */}
1039
+
1040
+ {msg.sources && msg.sources.length > 0 && (
1041
+ <div className={`mt-3 pt-3 border-t ${msg.role === 'user' ? 'border-blue-500' : 'border-gray-300'}`}>
1042
+ <p className="text-xs text-gray-600 mb-2">Sources:</p>
1043
+ {msg.sources.slice(0, 3).map((source, i) => (
1044
+ <div key={i} className="text-xs text-gray-600 mb-1">
1045
+ • {source.file_name} (score: {source.score?.toFixed(2)})
1046
+ </div>
1047
+ ))}
1048
+ </div>
1049
+ )}
1050
+ </div>
1051
+ </div>
1052
+ ))}
1053
+ <div ref={messagesEndRef} />
1054
+ </div>
1055
+ </div>
1056
+ )}
1057
+
1058
+ {/* Bottom Input Bar */}
1059
+ <div className="border-t border-gray-200 bg-white px-4 py-4">
1060
+ <div className="max-w-4xl mx-auto">
1061
+ <div className="flex items-center space-x-3">
1062
+ <button
1063
+ onClick={() => setShowUploadModal(true)}
1064
+ className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1065
+ >
1066
+ <Upload className="w-4 h-4" />
1067
+ <span className="text-sm font-medium">Upload</span>
1068
+ </button>
1069
+
1070
+ <input
1071
+ type="text"
1072
+ value={query}
1073
+ onChange={(e) => setQuery(e.target.value)}
1074
+ onKeyDown={handleKeyPress}
1075
+ placeholder="Ask me anything or upload documents for context..."
1076
+ 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"
1077
+ disabled={isQuerying}
1078
+ />
1079
+
1080
+ <button
1081
+ onClick={handleQuery}
1082
+ disabled={isQuerying || !query.trim()}
1083
+ className="p-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1084
+ >
1085
+ <Send className="w-5 h-5" />
1086
+ </button>
1087
+ </div>
1088
+
1089
+ {/* Web Search Options */}
1090
+ <div className="flex items-center justify-center space-x-6 mt-3">
1091
+ <label className="flex items-center space-x-2 cursor-pointer">
1092
+ <input
1093
+ type="checkbox"
1094
+ checked={enableWebSearch}
1095
+ onChange={(e) => {
1096
+ setEnableWebSearch(e.target.checked);
1097
+ if (e.target.checked && webSearchOnly) {
1098
+ setWebSearchOnly(false);
1099
+ }
1100
+ }}
1101
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1102
+ />
1103
+ <span className="text-sm text-gray-700">Enhance with Web Search</span>
1104
+ </label>
1105
+ <label className="flex items-center space-x-2 cursor-pointer">
1106
+ <input
1107
+ type="checkbox"
1108
+ checked={webSearchOnly}
1109
+ onChange={(e) => {
1110
+ setWebSearchOnly(e.target.checked);
1111
+ if (e.target.checked) {
1112
+ setEnableWebSearch(false);
1113
+ }
1114
+ }}
1115
+ className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1116
+ />
1117
+ <span className="text-sm text-gray-700">Web Search Only</span>
1118
+ </label>
1119
+ </div>
1120
+
1121
+ <p className="text-xs text-gray-500 mt-2 text-center">
1122
+ Press Enter to send, Shift+Enter for new line
1123
+ </p>
1124
+ </div>
1125
+ </div>
1126
+ </div>
1127
+ );
1128
+
1129
+ const renderFilesView = () => (
1130
+ <div className="flex-1 overflow-y-auto p-6">
1131
+ <div className="max-w-5xl mx-auto">
1132
+ <div className="flex items-center justify-between mb-6">
1133
+ <div>
1134
+ <h2 className="text-2xl font-bold text-gray-800">Document Management</h2>
1135
+ <p className="text-gray-600">Manage your uploaded and processed documents</p>
1136
+ </div>
1137
+ <div className="flex space-x-3">
1138
+ <button
1139
+ onClick={fetchProcessedDocuments}
1140
+ 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"
1141
+ >
1142
+ <RefreshCw className="w-4 h-4" />
1143
+ <span>Refresh</span>
1144
+ </button>
1145
+ <button
1146
+ onClick={() => setShowUploadModal(true)}
1147
+ className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1148
+ >
1149
+ <Upload className="w-4 h-4" />
1150
+ <span>Upload Documents</span>
1151
+ </button>
1152
+ </div>
1153
+ </div>
1154
+
1155
+ {processingDocs.length > 0 && (
1156
+ <div className="mb-6">
1157
+ <h3 className="text-lg font-semibold text-gray-800 mb-3">Processing Documents</h3>
1158
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1159
+ {processingDocs.map((doc, idx) => (
1160
+ <div key={idx} className="flex items-center space-x-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
1161
+ <Loader2 className="w-8 h-8 text-yellow-600 animate-spin" />
1162
+ <div className="flex-1">
1163
+ <p className="font-medium text-gray-800">{doc.name}</p>
1164
+ <p className="text-sm text-gray-600">Processing...</p>
1165
+ </div>
1166
+ </div>
1167
+ ))}
1168
+ </div>
1169
+ </div>
1170
+ )}
1171
+
1172
+ <div>
1173
+ <h3 className="text-lg font-semibold text-gray-800 mb-3">
1174
+ Processed Documents ({processedDocs.length})
1175
+ </h3>
1176
+ {processedDocs.length === 0 ? (
1177
+ <div className="text-center py-12 bg-gray-50 rounded-lg">
1178
+ <FolderOpen className="w-16 h-16 text-gray-400 mx-auto mb-4" />
1179
+ <p className="text-gray-600">No documents processed yet</p>
1180
+ <button
1181
+ onClick={() => setShowUploadModal(true)}
1182
+ className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1183
+ >
1184
+ Upload Your First Document
1185
+ </button>
1186
+ </div>
1187
+ ) : (
1188
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1189
+ {processedDocs.map((doc, idx) => (
1190
+ <div key={idx} className="p-4 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow group">
1191
+ <div className="flex items-start justify-between mb-3">
1192
+ <FileText className="w-8 h-8 text-blue-600" />
1193
+ <button
1194
+ onClick={() => handleDeleteDocument(doc.id, doc.name)}
1195
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded"
1196
+ >
1197
+ <Trash2 className="w-4 h-4 text-gray-400 hover:text-red-600" />
1198
+ </button>
1199
+ </div>
1200
+ <p className="font-medium text-gray-800 mb-1 truncate" title={doc.name}>{doc.name || `Document ${idx + 1}`}</p>
1201
+ <p className="text-sm text-gray-600 mb-2">{DOMAIN_CONFIGS[doc.domain]?.name || selectedDomain}</p>
1202
+ <div className="flex items-center space-x-2">
1203
+ <CheckCircle className="w-4 h-4 text-green-600" />
1204
+ <span className="text-xs text-gray-600">Processed</span>
1205
+ </div>
1206
+ </div>
1207
+ ))}
1208
+ </div>
1209
+ )}
1210
+ </div>
1211
+ </div>
1212
+ </div>
1213
+ );
1214
+
1215
+ const renderSettingsView = () => (
1216
+ <div className="flex-1 overflow-y-auto p-6">
1217
+ <div className="max-w-3xl mx-auto">
1218
+ <h2 className="text-2xl font-bold text-gray-800 mb-6">Settings</h2>
1219
+
1220
+ <div className="space-y-6">
1221
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
1222
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Domain Configuration</h3>
1223
+ <div className="space-y-3">
1224
+ <div>
1225
+ <label className="block text-sm font-medium text-gray-700 mb-2">Current Domain</label>
1226
+ <select
1227
+ value={selectedDomain}
1228
+ onChange={(e) => setSelectedDomain(e.target.value)}
1229
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
1230
+ >
1231
+ {Object.entries(DOMAIN_CONFIGS).map(([key, config]) => (
1232
+ <option key={key} value={key}>{config.name}</option>
1233
+ ))}
1234
+ </select>
1235
+ </div>
1236
+ <div>
1237
+ <label className="block text-sm font-medium text-gray-700 mb-2">Supported File Types</label>
1238
+ <div className="flex flex-wrap gap-2">
1239
+ {DOMAIN_CONFIGS[selectedDomain].fileTypes.map(type => (
1240
+ <span key={type} className="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-full">
1241
+ {type}
1242
+ </span>
1243
+ ))}
1244
+ </div>
1245
+ </div>
1246
+ </div>
1247
+ </div>
1248
+
1249
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
1250
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Performance Settings</h3>
1251
+ <div className="space-y-4">
1252
+ <div className="flex items-start space-x-3">
1253
+ <input
1254
+ type="checkbox"
1255
+ id="fastMode"
1256
+ checked={fastMode}
1257
+ onChange={(e) => setFastMode(e.target.checked)}
1258
+ className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
1259
+ />
1260
+ <div className="flex-1">
1261
+ <label htmlFor="fastMode" className="block text-sm font-medium text-gray-700 cursor-pointer">
1262
+ Fast Mode
1263
+ </label>
1264
+ <p className="text-xs text-gray-600 mt-1">
1265
+ Use optimized parameters for 2-3x faster queries. Slightly reduced quality but much better performance.
1266
+ </p>
1267
+ </div>
1268
+ </div>
1269
+
1270
+ <div className="flex items-start space-x-3">
1271
+ <input
1272
+ type="checkbox"
1273
+ id="enableCache"
1274
+ checked={enableCache}
1275
+ onChange={(e) => setEnableCache(e.target.checked)}
1276
+ className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
1277
+ />
1278
+ <div className="flex-1">
1279
+ <label htmlFor="enableCache" className="block text-sm font-medium text-gray-700 cursor-pointer">
1280
+ Enable Query Caching
1281
+ </label>
1282
+ <p className="text-xs text-gray-600 mt-1">
1283
+ Cache query results for 5 minutes. Repeated queries return instantly (100x faster).
1284
+ </p>
1285
+ </div>
1286
+ </div>
1287
+
1288
+ <div className="flex items-start space-x-3">
1289
+ <input
1290
+ type="checkbox"
1291
+ id="enableWebSearch"
1292
+ checked={enableWebSearch}
1293
+ onChange={(e) => {
1294
+ setEnableWebSearch(e.target.checked);
1295
+ if (e.target.checked && webSearchOnly) {
1296
+ setWebSearchOnly(false);
1297
+ }
1298
+ }}
1299
+ className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
1300
+ />
1301
+ <div className="flex-1">
1302
+ <label htmlFor="enableWebSearch" className="block text-sm font-medium text-gray-700 cursor-pointer">
1303
+ Enhance with Web Search
1304
+ </label>
1305
+ <p className="text-xs text-gray-600 mt-1">
1306
+ Augment document answers with current web search results.
1307
+ </p>
1308
+ </div>
1309
+ </div>
1310
+
1311
+ <div className="flex items-start space-x-3">
1312
+ <input
1313
+ type="checkbox"
1314
+ id="webSearchOnly"
1315
+ checked={webSearchOnly}
1316
+ onChange={(e) => {
1317
+ setWebSearchOnly(e.target.checked);
1318
+ if (e.target.checked) {
1319
+ setEnableWebSearch(false);
1320
+ }
1321
+ }}
1322
+ className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
1323
+ />
1324
+ <div className="flex-1">
1325
+ <label htmlFor="webSearchOnly" className="block text-sm font-medium text-gray-700 cursor-pointer">
1326
+ Web Search Only
1327
+ </label>
1328
+ <p className="text-xs text-gray-600 mt-1">
1329
+ Skip document retrieval and use only web search (useful when no documents uploaded).
1330
+ </p>
1331
+ </div>
1332
+ </div>
1333
+
1334
+ <div className="flex items-start space-x-3">
1335
+ <input
1336
+ type="checkbox"
1337
+ id="enableQueryImprovement"
1338
+ checked={enableQueryImprovement}
1339
+ onChange={(e) => setEnableQueryImprovement(e.target.checked)}
1340
+ className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
1341
+ />
1342
+ <div className="flex-1">
1343
+ <label htmlFor="enableQueryImprovement" className="block text-sm font-medium text-gray-700 cursor-pointer">
1344
+ Enable Query Improvement
1345
+ </label>
1346
+ <p className="text-xs text-gray-600 mt-1">
1347
+ Automatically improve and expand user queries for better results. Disable for faster responses.
1348
+ </p>
1349
+ </div>
1350
+ </div>
1351
+
1352
+ <div className="flex items-start space-x-3">
1353
+ <input
1354
+ type="checkbox"
1355
+ id="enableVerification"
1356
+ checked={enableVerification}
1357
+ onChange={(e) => setEnableVerification(e.target.checked)}
1358
+ className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mt-0.5"
1359
+ />
1360
+ <div className="flex-1">
1361
+ <label htmlFor="enableVerification" className="block text-sm font-medium text-gray-700 cursor-pointer">
1362
+ Enable Answer Verification
1363
+ </label>
1364
+ <p className="text-xs text-gray-600 mt-1">
1365
+ Use dual-LLM verification to check answer quality and accuracy. Disable for faster responses.
1366
+ </p>
1367
+ </div>
1368
+ </div>
1369
+
1370
+
1371
+ </div>
1372
+ </div>
1373
+
1374
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
1375
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Actions</h3>
1376
+ <div className="space-y-3">
1377
+ <button
1378
+ onClick={clearConversation}
1379
+ 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"
1380
+ >
1381
+ <Trash2 className="w-4 h-4" />
1382
+ <span>Clear Conversation</span>
1383
+ </button>
1384
+ </div>
1385
+ </div>
1386
+ </div>
1387
+ </div>
1388
+ </div>
1389
+ );
1390
+
1391
+ // Upload Modal
1392
+ const renderUploadModal = () => {
1393
+ if (!showUploadModal) return null;
1394
+
1395
+ return (
1396
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
1397
+ <div className="bg-white rounded-xl max-w-2xl w-full p-6">
1398
+ <div className="flex items-center justify-between mb-6">
1399
+ <h2 className="text-2xl font-bold text-gray-800">Upload Documents</h2>
1400
+ <button
1401
+ onClick={() => {
1402
+ setShowUploadModal(false);
1403
+ setUploadMode('file');
1404
+ setUrlInput('');
1405
+ }}
1406
+ className="p-2 hover:bg-gray-100 rounded-lg"
1407
+ >
1408
+ <X className="w-5 h-5 text-gray-600" />
1409
+ </button>
1410
+ </div>
1411
+
1412
+ {/* Mode Toggle */}
1413
+ <div className="flex items-center space-x-2 mb-6">
1414
+ <button
1415
+ onClick={() => setUploadMode('file')}
1416
+ className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
1417
+ uploadMode === 'file'
1418
+ ? 'bg-blue-600 text-white'
1419
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
1420
+ }`}
1421
+ >
1422
+ Upload File
1423
+ </button>
1424
+ <button
1425
+ onClick={() => setUploadMode('url')}
1426
+ className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
1427
+ uploadMode === 'url'
1428
+ ? 'bg-blue-600 text-white'
1429
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
1430
+ }`}
1431
+ >
1432
+ Upload from URL
1433
+ </button>
1434
+ </div>
1435
+
1436
+ {uploadMode === 'file' ? (
1437
+ <div
1438
+ onDragOver={handleDragOver}
1439
+ onDragLeave={handleDragLeave}
1440
+ onDrop={handleDrop}
1441
+ className={`border-2 border-dashed rounded-xl p-12 text-center transition-colors ${
1442
+ isDragging
1443
+ ? 'border-blue-500 bg-blue-50'
1444
+ : 'border-gray-300 hover:border-gray-400'
1445
+ }`}
1446
+ >
1447
+ <Upload className="w-16 h-16 text-gray-400 mx-auto mb-4" />
1448
+ <h3 className="text-lg font-semibold text-gray-800 mb-2">
1449
+ Drop files here or click to browse
1450
+ </h3>
1451
+ <p className="text-gray-600 mb-4">
1452
+ Supported: {DOMAIN_CONFIGS[selectedDomain].fileTypes.join(', ')}
1453
+ </p>
1454
+ <input
1455
+ ref={fileInputRef}
1456
+ type="file"
1457
+ multiple
1458
+ accept={DOMAIN_CONFIGS[selectedDomain].fileTypes.join(',')}
1459
+ onChange={(e) => handleFileUpload(e.target.files)}
1460
+ className="hidden"
1461
+ />
1462
+ <button
1463
+ onClick={() => fileInputRef.current?.click()}
1464
+ className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1465
+ >
1466
+ Select Files
1467
+ </button>
1468
+ </div>
1469
+ ) : (
1470
+ <div className="space-y-4">
1471
+ <div>
1472
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1473
+ Enter URL to fetch and process
1474
+ </label>
1475
+ <input
1476
+ type="url"
1477
+ value={urlInput}
1478
+ onChange={(e) => setUrlInput(e.target.value)}
1479
+ placeholder="https://example.com/document.pdf"
1480
+ 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"
1481
+ onKeyDown={(e) => {
1482
+ if (e.key === 'Enter') {
1483
+ handleUrlUpload();
1484
+ }
1485
+ }}
1486
+ />
1487
+ </div>
1488
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
1489
+ <p className="text-sm text-blue-800">
1490
+ <strong>Supported:</strong> PDF, HTML pages (converted to markdown), and other web documents
1491
+ </p>
1492
+ </div>
1493
+ <button
1494
+ onClick={handleUrlUpload}
1495
+ disabled={!urlInput.trim()}
1496
+ 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"
1497
+ >
1498
+ Fetch and Process URL
1499
+ </button>
1500
+ </div>
1501
+ )}
1502
+ </div>
1503
+ </div>
1504
+ );
1505
+ };
1506
+
1507
+ // Error Display
1508
+ const renderError = () => {
1509
+ if (!error) return null;
1510
+
1511
+ return (
1512
+ <div className="fixed bottom-4 right-4 bg-red-50 border border-red-200 rounded-lg p-4 max-w-md shadow-lg">
1513
+ <div className="flex items-start space-x-3">
1514
+ <XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
1515
+ <div className="flex-1">
1516
+ <p className="text-sm text-red-800">{error}</p>
1517
+ </div>
1518
+ <button
1519
+ onClick={() => setError(null)}
1520
+ className="text-red-600 hover:text-red-800"
1521
+ >
1522
+ <X className="w-4 h-4" />
1523
+ </button>
1524
+ </div>
1525
+ </div>
1526
+ );
1527
+ };
1528
+
1529
+ // =============================================================================
1530
+ // Main Render
1531
+ // =============================================================================
1532
+
1533
+ return (
1534
+ <div className="h-screen flex flex-col bg-gray-50">
1535
+ {renderNavigation()}
1536
+
1537
+ <div className="flex-1 flex overflow-hidden">
1538
+ {renderSidebar()}
1539
+
1540
+ {currentView === 'app' && renderAppView()}
1541
+ {currentView === 'files' && renderFilesView()}
1542
+ {currentView === 'settings' && renderSettingsView()}
1543
+ </div>
1544
+
1545
+ {renderUploadModal()}
1546
+ {renderError()}
1547
+ </div>
1548
+ );
1549
+ }