fazeel007 commited on
Commit
10ac46e
Β·
1 Parent(s): 39781c3

Implement complete document upload and processing pipeline with Modal integration

Browse files

βœ… Real Document Upload:
- Created SQLite storage replacing in-memory storage with full document metadata
- Added file upload API with multer supporting PDFs, images, text files (50MB limit)
- Implemented document processing pipeline with Modal integration for heavy workloads

βœ… PDF/Image Processing Pipeline:
- Modal integration for OCR text extraction using PyPDF2 and Tesseract
- Batch processing capabilities for multiple documents concurrently
- Document status tracking (pending -> processing -> completed/failed)

βœ… Vector Embeddings Storage & Search:
- Real embeddings generation using Nebius AI and storage in SQLite
- Vector index building using Modal's FAISS implementation
- High-performance vector search with relevance scoring

βœ… Database Storage:
- SQLite storage with proper schema for file metadata, processing status
- Document lifecycle management with file path tracking
- Search history and results persistence

βœ… Batch Processing:
- Modal distributed computing for processing large document collections
- Index building from processed documents with configurable parameters
- Concurrent document processing with proper error handling

βœ… Frontend Components:
- Document upload UI with drag-and-drop, progress tracking, file validation
- Vector search interface with comparison mode vs traditional search
- Document management with processing status and batch operations
- Integration into main knowledge base with new tabs

πŸ”§ Heavy Workloads Now Supported:
- OCR processing for PDFs and images via Modal
- FAISS vector index building for large document collections
- Distributed embedding generation and similarity search
- Batch document processing with 2-4GB memory allocation per task

The Modal integration is now actively used for real computational workloads instead of being theoretical.

client/src/components/knowledge-base/document-upload.tsx ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Upload, FileText, Image, FileCode, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Progress } from '@/components/ui/progress';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Alert, AlertDescription } from '@/components/ui/alert';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Label } from '@/components/ui/label';
10
+
11
+ interface UploadedFile {
12
+ id: string;
13
+ file: File;
14
+ status: 'pending' | 'uploading' | 'processing' | 'completed' | 'failed';
15
+ progress: number;
16
+ documentId?: number;
17
+ error?: string;
18
+ }
19
+
20
+ interface Document {
21
+ id: number;
22
+ title: string;
23
+ fileName: string;
24
+ fileSize: number;
25
+ mimeType: string;
26
+ processingStatus: string;
27
+ createdAt: string;
28
+ }
29
+
30
+ export default function DocumentUpload() {
31
+ const [files, setFiles] = useState<UploadedFile[]>([]);
32
+ const [isDragging, setIsDragging] = useState(false);
33
+ const [uploadTitle, setUploadTitle] = useState('');
34
+ const [uploadSource, setUploadSource] = useState('');
35
+ const [documents, setDocuments] = useState<Document[]>([]);
36
+ const [isProcessing, setIsProcessing] = useState(false);
37
+
38
+ const acceptedTypes = [
39
+ 'application/pdf',
40
+ 'image/jpeg',
41
+ 'image/png',
42
+ 'image/gif',
43
+ 'image/webp',
44
+ 'text/plain',
45
+ 'text/markdown',
46
+ 'application/json',
47
+ 'application/msword',
48
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
49
+ ];
50
+
51
+ const getFileIcon = (mimeType: string) => {
52
+ if (mimeType.startsWith('image/')) return <Image className="w-5 h-5" />;
53
+ if (mimeType === 'application/pdf') return <FileText className="w-5 h-5" />;
54
+ if (mimeType.includes('text') || mimeType.includes('json')) return <FileCode className="w-5 h-5" />;
55
+ return <FileText className="w-5 h-5" />;
56
+ };
57
+
58
+ const getStatusIcon = (status: string) => {
59
+ switch (status) {
60
+ case 'completed':
61
+ return <CheckCircle className="w-4 h-4 text-green-500" />;
62
+ case 'failed':
63
+ return <XCircle className="w-4 h-4 text-red-500" />;
64
+ case 'processing':
65
+ case 'uploading':
66
+ return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
67
+ default:
68
+ return <AlertCircle className="w-4 h-4 text-yellow-500" />;
69
+ }
70
+ };
71
+
72
+ const formatFileSize = (bytes: number): string => {
73
+ if (bytes === 0) return '0 Bytes';
74
+ const k = 1024;
75
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
76
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
77
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
78
+ };
79
+
80
+ const handleDragOver = useCallback((e: React.DragEvent) => {
81
+ e.preventDefault();
82
+ setIsDragging(true);
83
+ }, []);
84
+
85
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
86
+ e.preventDefault();
87
+ setIsDragging(false);
88
+ }, []);
89
+
90
+ const handleDrop = useCallback((e: React.DragEvent) => {
91
+ e.preventDefault();
92
+ setIsDragging(false);
93
+
94
+ const droppedFiles = Array.from(e.dataTransfer.files);
95
+ handleFiles(droppedFiles);
96
+ }, []);
97
+
98
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
99
+ if (e.target.files) {
100
+ const selectedFiles = Array.from(e.target.files);
101
+ handleFiles(selectedFiles);
102
+ }
103
+ };
104
+
105
+ const handleFiles = (newFiles: File[]) => {
106
+ const validFiles = newFiles.filter(file => {
107
+ if (!acceptedTypes.includes(file.type)) {
108
+ alert(`File type ${file.type} is not supported`);
109
+ return false;
110
+ }
111
+ if (file.size > 50 * 1024 * 1024) { // 50MB limit
112
+ alert(`File ${file.name} is too large. Maximum size is 50MB`);
113
+ return false;
114
+ }
115
+ return true;
116
+ });
117
+
118
+ const uploadFiles: UploadedFile[] = validFiles.map(file => ({
119
+ id: `${Date.now()}-${Math.random()}`,
120
+ file,
121
+ status: 'pending',
122
+ progress: 0
123
+ }));
124
+
125
+ setFiles(prev => [...prev, ...uploadFiles]);
126
+ };
127
+
128
+ const uploadFiles = async () => {
129
+ const pendingFiles = files.filter(f => f.status === 'pending');
130
+ if (pendingFiles.length === 0) return;
131
+
132
+ for (const uploadFile of pendingFiles) {
133
+ try {
134
+ // Update status to uploading
135
+ setFiles(prev => prev.map(f =>
136
+ f.id === uploadFile.id
137
+ ? { ...f, status: 'uploading', progress: 0 }
138
+ : f
139
+ ));
140
+
141
+ const formData = new FormData();
142
+ formData.append('files', uploadFile.file);
143
+ if (uploadTitle) formData.append('title', uploadTitle);
144
+ if (uploadSource) formData.append('source', uploadSource);
145
+
146
+ const response = await fetch('/api/documents/upload', {
147
+ method: 'POST',
148
+ body: formData,
149
+ });
150
+
151
+ if (!response.ok) {
152
+ throw new Error(`Upload failed: ${response.statusText}`);
153
+ }
154
+
155
+ const result = await response.json();
156
+
157
+ if (result.success && result.documents?.length > 0) {
158
+ const document = result.documents[0];
159
+
160
+ // Update file status to processing
161
+ setFiles(prev => prev.map(f =>
162
+ f.id === uploadFile.id
163
+ ? {
164
+ ...f,
165
+ status: 'processing',
166
+ progress: 100,
167
+ documentId: document.id
168
+ }
169
+ : f
170
+ ));
171
+
172
+ // Add to documents list
173
+ setDocuments(prev => [document, ...prev]);
174
+
175
+ // If file requires processing, start processing
176
+ if (document.processingStatus === 'pending') {
177
+ await processDocument(document.id, uploadFile.id);
178
+ } else {
179
+ // Mark as completed if no processing needed
180
+ setFiles(prev => prev.map(f =>
181
+ f.id === uploadFile.id
182
+ ? { ...f, status: 'completed' }
183
+ : f
184
+ ));
185
+ }
186
+ } else {
187
+ throw new Error(result.message || 'Upload failed');
188
+ }
189
+
190
+ } catch (error) {
191
+ console.error('Upload error:', error);
192
+ setFiles(prev => prev.map(f =>
193
+ f.id === uploadFile.id
194
+ ? {
195
+ ...f,
196
+ status: 'failed',
197
+ error: error instanceof Error ? error.message : 'Upload failed'
198
+ }
199
+ : f
200
+ ));
201
+ }
202
+ }
203
+ };
204
+
205
+ const processDocument = async (documentId: number, fileId: string) => {
206
+ try {
207
+ const response = await fetch(`/api/documents/process/${documentId}`, {
208
+ method: 'POST',
209
+ headers: {
210
+ 'Content-Type': 'application/json'
211
+ },
212
+ body: JSON.stringify({
213
+ operations: ['extract_text', 'generate_embedding']
214
+ })
215
+ });
216
+
217
+ const result = await response.json();
218
+
219
+ if (result.success) {
220
+ // Update file status to completed
221
+ setFiles(prev => prev.map(f =>
222
+ f.id === fileId
223
+ ? { ...f, status: 'completed' }
224
+ : f
225
+ ));
226
+
227
+ // Update document in list
228
+ setDocuments(prev => prev.map(doc =>
229
+ doc.id === documentId
230
+ ? { ...doc, processingStatus: 'completed' }
231
+ : doc
232
+ ));
233
+ } else {
234
+ throw new Error(result.message || 'Processing failed');
235
+ }
236
+
237
+ } catch (error) {
238
+ console.error('Processing error:', error);
239
+ setFiles(prev => prev.map(f =>
240
+ f.id === fileId
241
+ ? {
242
+ ...f,
243
+ status: 'failed',
244
+ error: error instanceof Error ? error.message : 'Processing failed'
245
+ }
246
+ : f
247
+ ));
248
+ }
249
+ };
250
+
251
+ const batchProcessDocuments = async () => {
252
+ const completedDocuments = documents.filter(doc => doc.processingStatus === 'completed');
253
+ if (completedDocuments.length === 0) {
254
+ alert('No processed documents available for batch operations');
255
+ return;
256
+ }
257
+
258
+ setIsProcessing(true);
259
+
260
+ try {
261
+ // Build vector index
262
+ const response = await fetch('/api/documents/index/build', {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json'
266
+ },
267
+ body: JSON.stringify({
268
+ documentIds: completedDocuments.map(doc => doc.id),
269
+ indexName: 'main_index'
270
+ })
271
+ });
272
+
273
+ const result = await response.json();
274
+
275
+ if (result.success) {
276
+ alert(`Vector index built successfully with ${result.documentCount} documents`);
277
+ } else {
278
+ throw new Error(result.message || 'Index building failed');
279
+ }
280
+
281
+ } catch (error) {
282
+ console.error('Batch processing error:', error);
283
+ alert(`Batch processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
284
+ } finally {
285
+ setIsProcessing(false);
286
+ }
287
+ };
288
+
289
+ const removeFile = (fileId: string) => {
290
+ setFiles(prev => prev.filter(f => f.id !== fileId));
291
+ };
292
+
293
+ const clearCompleted = () => {
294
+ setFiles(prev => prev.filter(f => f.status !== 'completed' && f.status !== 'failed'));
295
+ };
296
+
297
+ return (
298
+ <div className="space-y-6">
299
+ {/* Upload Area */}
300
+ <Card>
301
+ <CardHeader>
302
+ <CardTitle className="flex items-center gap-2">
303
+ <Upload className="w-5 h-5" />
304
+ Document Upload
305
+ </CardTitle>
306
+ </CardHeader>
307
+ <CardContent className="space-y-4">
308
+ {/* Optional metadata */}
309
+ <div className="grid grid-cols-2 gap-4">
310
+ <div>
311
+ <Label htmlFor="title">Title (optional)</Label>
312
+ <Input
313
+ id="title"
314
+ placeholder="Document title"
315
+ value={uploadTitle}
316
+ onChange={(e) => setUploadTitle(e.target.value)}
317
+ />
318
+ </div>
319
+ <div>
320
+ <Label htmlFor="source">Source (optional)</Label>
321
+ <Input
322
+ id="source"
323
+ placeholder="Document source"
324
+ value={uploadSource}
325
+ onChange={(e) => setUploadSource(e.target.value)}
326
+ />
327
+ </div>
328
+ </div>
329
+
330
+ {/* Drag and drop area */}
331
+ <div
332
+ className={`
333
+ border-2 border-dashed rounded-lg p-8 text-center transition-colors
334
+ ${isDragging
335
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
336
+ : 'border-gray-300 hover:border-gray-400 dark:border-gray-600'
337
+ }
338
+ `}
339
+ onDragOver={handleDragOver}
340
+ onDragLeave={handleDragLeave}
341
+ onDrop={handleDrop}
342
+ >
343
+ <Upload className="w-12 h-12 mx-auto text-gray-400 mb-4" />
344
+ <p className="text-lg font-medium mb-2">
345
+ Drop files here or click to browse
346
+ </p>
347
+ <p className="text-sm text-gray-500 mb-4">
348
+ Supports PDF, images, text files, Word documents (max 50MB each)
349
+ </p>
350
+ <input
351
+ type="file"
352
+ multiple
353
+ accept={acceptedTypes.join(',')}
354
+ onChange={handleFileSelect}
355
+ className="hidden"
356
+ id="file-upload"
357
+ />
358
+ <Button asChild variant="outline">
359
+ <label htmlFor="file-upload" className="cursor-pointer">
360
+ Browse Files
361
+ </label>
362
+ </Button>
363
+ </div>
364
+
365
+ {/* File list */}
366
+ {files.length > 0 && (
367
+ <div className="space-y-2">
368
+ <div className="flex justify-between items-center">
369
+ <h3 className="font-medium">Files ({files.length})</h3>
370
+ <div className="space-x-2">
371
+ <Button onClick={uploadFiles} size="sm" disabled={!files.some(f => f.status === 'pending')}>
372
+ Upload All
373
+ </Button>
374
+ <Button onClick={clearCompleted} variant="outline" size="sm">
375
+ Clear Completed
376
+ </Button>
377
+ </div>
378
+ </div>
379
+
380
+ {files.map((file) => (
381
+ <div key={file.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
382
+ <div className="flex items-center gap-3 flex-1">
383
+ {getFileIcon(file.file.type)}
384
+ <div className="flex-1 min-w-0">
385
+ <p className="font-medium truncate">{file.file.name}</p>
386
+ <p className="text-sm text-gray-500">
387
+ {formatFileSize(file.file.size)}
388
+ </p>
389
+ </div>
390
+ </div>
391
+
392
+ <div className="flex items-center gap-3">
393
+ <Badge variant={
394
+ file.status === 'completed' ? 'default' :
395
+ file.status === 'failed' ? 'destructive' :
396
+ 'secondary'
397
+ }>
398
+ {file.status}
399
+ </Badge>
400
+
401
+ {getStatusIcon(file.status)}
402
+
403
+ {(file.status === 'uploading' || file.status === 'processing') && (
404
+ <div className="w-24">
405
+ <Progress value={file.progress} className="h-2" />
406
+ </div>
407
+ )}
408
+
409
+ {(file.status === 'pending' || file.status === 'failed') && (
410
+ <Button
411
+ variant="ghost"
412
+ size="sm"
413
+ onClick={() => removeFile(file.id)}
414
+ >
415
+ <XCircle className="w-4 h-4" />
416
+ </Button>
417
+ )}
418
+ </div>
419
+ </div>
420
+ ))}
421
+ </div>
422
+ )}
423
+
424
+ {/* Error alerts */}
425
+ {files.some(f => f.error) && (
426
+ <Alert variant="destructive">
427
+ <AlertCircle className="w-4 h-4" />
428
+ <AlertDescription>
429
+ Some files failed to upload. Check individual file status above.
430
+ </AlertDescription>
431
+ </Alert>
432
+ )}
433
+ </CardContent>
434
+ </Card>
435
+
436
+ {/* Document Management */}
437
+ {documents.length > 0 && (
438
+ <Card>
439
+ <CardHeader>
440
+ <CardTitle className="flex items-center justify-between">
441
+ <span>Uploaded Documents ({documents.length})</span>
442
+ <Button
443
+ onClick={batchProcessDocuments}
444
+ disabled={isProcessing || documents.filter(d => d.processingStatus === 'completed').length === 0}
445
+ >
446
+ {isProcessing ? (
447
+ <>
448
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
449
+ Building Index...
450
+ </>
451
+ ) : (
452
+ 'Build Vector Index'
453
+ )}
454
+ </Button>
455
+ </CardTitle>
456
+ </CardHeader>
457
+ <CardContent>
458
+ <div className="space-y-2">
459
+ {documents.map((doc) => (
460
+ <div key={doc.id} className="flex items-center justify-between p-3 border rounded-lg">
461
+ <div className="flex items-center gap-3 flex-1">
462
+ {getFileIcon(doc.mimeType)}
463
+ <div className="flex-1 min-w-0">
464
+ <p className="font-medium truncate">{doc.title}</p>
465
+ <p className="text-sm text-gray-500">
466
+ {doc.fileName} β€’ {formatFileSize(doc.fileSize)}
467
+ </p>
468
+ </div>
469
+ </div>
470
+
471
+ <div className="flex items-center gap-3">
472
+ <Badge variant={
473
+ doc.processingStatus === 'completed' ? 'default' :
474
+ doc.processingStatus === 'failed' ? 'destructive' :
475
+ 'secondary'
476
+ }>
477
+ {doc.processingStatus}
478
+ </Badge>
479
+ {getStatusIcon(doc.processingStatus)}
480
+ </div>
481
+ </div>
482
+ ))}
483
+ </div>
484
+ </CardContent>
485
+ </Card>
486
+ )}
487
+ </div>
488
+ );
489
+ }
client/src/components/knowledge-base/vector-search.tsx ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Search, Zap, Database, Loader2, ArrowRight } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Input } from '@/components/ui/input';
6
+ import { Label } from '@/components/ui/label';
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { Alert, AlertDescription } from '@/components/ui/alert';
10
+ import { Separator } from '@/components/ui/separator';
11
+
12
+ interface VectorSearchResult {
13
+ id: string;
14
+ title: string;
15
+ content: string;
16
+ source: string;
17
+ relevanceScore: number;
18
+ rank: number;
19
+ snippet: string;
20
+ }
21
+
22
+ interface SearchResults {
23
+ success: boolean;
24
+ query: string;
25
+ indexName: string;
26
+ results: VectorSearchResult[];
27
+ totalFound: number;
28
+ searchTime?: number;
29
+ error?: string;
30
+ }
31
+
32
+ export default function VectorSearch() {
33
+ const [query, setQuery] = useState('');
34
+ const [indexName, setIndexName] = useState('main_index');
35
+ const [maxResults, setMaxResults] = useState(10);
36
+ const [isSearching, setIsSearching] = useState(false);
37
+ const [searchResults, setSearchResults] = useState<SearchResults | null>(null);
38
+ const [comparisonMode, setComparisonMode] = useState(false);
39
+ const [traditionalResults, setTraditionalResults] = useState<any>(null);
40
+
41
+ const handleVectorSearch = async () => {
42
+ if (!query.trim()) return;
43
+
44
+ setIsSearching(true);
45
+ try {
46
+ const response = await fetch('/api/documents/search/vector', {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ },
51
+ body: JSON.stringify({
52
+ query: query.trim(),
53
+ indexName,
54
+ maxResults
55
+ })
56
+ });
57
+
58
+ const result = await response.json();
59
+ setSearchResults(result);
60
+
61
+ // If comparison mode is enabled, also run traditional search
62
+ if (comparisonMode) {
63
+ await runTraditionalSearch();
64
+ }
65
+
66
+ } catch (error) {
67
+ console.error('Vector search error:', error);
68
+ setSearchResults({
69
+ success: false,
70
+ query: query.trim(),
71
+ indexName,
72
+ results: [],
73
+ totalFound: 0,
74
+ error: error instanceof Error ? error.message : 'Search failed'
75
+ });
76
+ } finally {
77
+ setIsSearching(false);
78
+ }
79
+ };
80
+
81
+ const runTraditionalSearch = async () => {
82
+ try {
83
+ const response = await fetch('/api/search', {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json'
87
+ },
88
+ body: JSON.stringify({
89
+ query: query.trim(),
90
+ searchType: 'keyword',
91
+ limit: maxResults,
92
+ offset: 0
93
+ })
94
+ });
95
+
96
+ const result = await response.json();
97
+ setTraditionalResults(result);
98
+
99
+ } catch (error) {
100
+ console.error('Traditional search error:', error);
101
+ setTraditionalResults(null);
102
+ }
103
+ };
104
+
105
+ const handleKeyPress = (e: React.KeyboardEvent) => {
106
+ if (e.key === 'Enter' && !isSearching) {
107
+ handleVectorSearch();
108
+ }
109
+ };
110
+
111
+ const formatRelevanceScore = (score: number): string => {
112
+ return (score * 100).toFixed(1) + '%';
113
+ };
114
+
115
+ const getScoreColor = (score: number): string => {
116
+ if (score >= 0.8) return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
117
+ if (score >= 0.6) return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
118
+ if (score >= 0.4) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
119
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
120
+ };
121
+
122
+ return (
123
+ <div className="space-y-6">
124
+ {/* Search Interface */}
125
+ <Card>
126
+ <CardHeader>
127
+ <CardTitle className="flex items-center gap-2">
128
+ <Zap className="w-5 h-5 text-blue-500" />
129
+ Vector Search (Modal + FAISS)
130
+ </CardTitle>
131
+ </CardHeader>
132
+ <CardContent className="space-y-4">
133
+ {/* Search Input */}
134
+ <div className="space-y-2">
135
+ <Label htmlFor="vector-query">Search Query</Label>
136
+ <div className="flex gap-2">
137
+ <Input
138
+ id="vector-query"
139
+ placeholder="Enter your search query for semantic similarity matching..."
140
+ value={query}
141
+ onChange={(e) => setQuery(e.target.value)}
142
+ onKeyPress={handleKeyPress}
143
+ className="flex-1"
144
+ />
145
+ <Button
146
+ onClick={handleVectorSearch}
147
+ disabled={isSearching || !query.trim()}
148
+ >
149
+ {isSearching ? (
150
+ <Loader2 className="w-4 h-4 animate-spin" />
151
+ ) : (
152
+ <Search className="w-4 h-4" />
153
+ )}
154
+ </Button>
155
+ </div>
156
+ </div>
157
+
158
+ {/* Search Options */}
159
+ <div className="grid grid-cols-3 gap-4">
160
+ <div>
161
+ <Label htmlFor="index-name">Vector Index</Label>
162
+ <Select value={indexName} onValueChange={setIndexName}>
163
+ <SelectTrigger>
164
+ <SelectValue />
165
+ </SelectTrigger>
166
+ <SelectContent>
167
+ <SelectItem value="main_index">Main Index</SelectItem>
168
+ <SelectItem value="test_index">Test Index</SelectItem>
169
+ <SelectItem value="academic_index">Academic Papers</SelectItem>
170
+ </SelectContent>
171
+ </Select>
172
+ </div>
173
+
174
+ <div>
175
+ <Label htmlFor="max-results">Max Results</Label>
176
+ <Select value={maxResults.toString()} onValueChange={(value) => setMaxResults(parseInt(value))}>
177
+ <SelectTrigger>
178
+ <SelectValue />
179
+ </SelectTrigger>
180
+ <SelectContent>
181
+ <SelectItem value="5">5 results</SelectItem>
182
+ <SelectItem value="10">10 results</SelectItem>
183
+ <SelectItem value="20">20 results</SelectItem>
184
+ <SelectItem value="50">50 results</SelectItem>
185
+ </SelectContent>
186
+ </Select>
187
+ </div>
188
+
189
+ <div className="flex items-end">
190
+ <Button
191
+ variant="outline"
192
+ onClick={() => setComparisonMode(!comparisonMode)}
193
+ className="w-full"
194
+ >
195
+ <Database className="w-4 h-4 mr-2" />
196
+ {comparisonMode ? 'Comparison: ON' : 'Compare with Keyword'}
197
+ </Button>
198
+ </div>
199
+ </div>
200
+
201
+ {/* Search Info */}
202
+ <Alert>
203
+ <Database className="w-4 h-4" />
204
+ <AlertDescription>
205
+ Vector search uses Modal.com's distributed FAISS implementation for high-performance semantic similarity matching.
206
+ Enable comparison mode to see differences between vector and traditional keyword search.
207
+ </AlertDescription>
208
+ </Alert>
209
+ </CardContent>
210
+ </Card>
211
+
212
+ {/* Search Results */}
213
+ {searchResults && (
214
+ <div className="grid grid-cols-1 gap-6">
215
+ {/* Vector Search Results */}
216
+ <Card>
217
+ <CardHeader>
218
+ <div className="flex items-center justify-between">
219
+ <CardTitle className="flex items-center gap-2">
220
+ <Zap className="w-5 h-5 text-blue-500" />
221
+ Vector Search Results
222
+ </CardTitle>
223
+ <div className="flex items-center gap-4 text-sm text-gray-500">
224
+ {searchResults.searchTime && (
225
+ <span>Search time: {(searchResults.searchTime * 1000).toFixed(0)}ms</span>
226
+ )}
227
+ <Badge variant="outline">
228
+ {searchResults.totalFound} results found
229
+ </Badge>
230
+ </div>
231
+ </div>
232
+ </CardHeader>
233
+ <CardContent>
234
+ {searchResults.success ? (
235
+ <div className="space-y-4">
236
+ {searchResults.results.length === 0 ? (
237
+ <div className="text-center py-8 text-gray-500">
238
+ <Database className="w-12 h-12 mx-auto mb-4 opacity-50" />
239
+ <p>No results found in vector index.</p>
240
+ <p className="text-sm">Try uploading and processing some documents first.</p>
241
+ </div>
242
+ ) : (
243
+ searchResults.results.map((result, index) => (
244
+ <div key={result.id} className="border rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
245
+ <div className="flex items-start justify-between mb-2">
246
+ <div className="flex items-center gap-3">
247
+ <Badge variant="outline" className="text-xs">
248
+ #{result.rank}
249
+ </Badge>
250
+ <Badge className={getScoreColor(result.relevanceScore)}>
251
+ {formatRelevanceScore(result.relevanceScore)}
252
+ </Badge>
253
+ </div>
254
+ <span className="text-xs text-gray-500">ID: {result.id}</span>
255
+ </div>
256
+
257
+ <h3 className="font-semibold text-lg mb-2">{result.title}</h3>
258
+
259
+ <p className="text-gray-600 dark:text-gray-300 mb-3 leading-relaxed">
260
+ {result.snippet}
261
+ </p>
262
+
263
+ <div className="flex items-center justify-between text-sm text-gray-500">
264
+ <span>{result.source}</span>
265
+ <ArrowRight className="w-4 h-4" />
266
+ </div>
267
+ </div>
268
+ ))
269
+ )}
270
+ </div>
271
+ ) : (
272
+ <Alert variant="destructive">
273
+ <AlertDescription>
274
+ {searchResults.error || 'Vector search failed'}
275
+ </AlertDescription>
276
+ </Alert>
277
+ )}
278
+ </CardContent>
279
+ </Card>
280
+
281
+ {/* Traditional Search Results (if comparison mode) */}
282
+ {comparisonMode && traditionalResults && (
283
+ <Card>
284
+ <CardHeader>
285
+ <CardTitle className="flex items-center gap-2">
286
+ <Search className="w-5 h-5 text-gray-500" />
287
+ Traditional Search Results (for comparison)
288
+ </CardTitle>
289
+ </CardHeader>
290
+ <CardContent>
291
+ {traditionalResults.results && traditionalResults.results.length > 0 ? (
292
+ <div className="space-y-4">
293
+ {traditionalResults.results.slice(0, maxResults).map((result: any, index: number) => (
294
+ <div key={result.id} className="border rounded-lg p-4 opacity-75">
295
+ <div className="flex items-start justify-between mb-2">
296
+ <Badge variant="outline" className="text-xs">
297
+ #{index + 1}
298
+ </Badge>
299
+ <Badge variant="secondary">
300
+ {formatRelevanceScore(result.relevanceScore || 0)}
301
+ </Badge>
302
+ </div>
303
+
304
+ <h3 className="font-semibold text-lg mb-2">{result.title}</h3>
305
+
306
+ <p className="text-gray-600 dark:text-gray-300 mb-3 leading-relaxed">
307
+ {result.snippet}
308
+ </p>
309
+
310
+ <div className="text-sm text-gray-500">
311
+ {result.source}
312
+ </div>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ ) : (
317
+ <div className="text-center py-4 text-gray-500">
318
+ <p>No traditional search results found.</p>
319
+ </div>
320
+ )}
321
+ </CardContent>
322
+ </Card>
323
+ )}
324
+ </div>
325
+ )}
326
+
327
+ {/* Help Text */}
328
+ {!searchResults && (
329
+ <Card>
330
+ <CardContent className="pt-6">
331
+ <div className="text-center text-gray-500 space-y-2">
332
+ <Database className="w-16 h-16 mx-auto opacity-50" />
333
+ <h3 className="text-lg font-medium">Advanced Vector Search</h3>
334
+ <p className="text-sm max-w-md mx-auto">
335
+ Search through your documents using semantic similarity powered by Modal.com's distributed FAISS implementation.
336
+ Upload documents first to build your vector index.
337
+ </p>
338
+ </div>
339
+ </CardContent>
340
+ </Card>
341
+ )}
342
+ </div>
343
+ );
344
+ }
client/src/pages/knowledge-base.tsx CHANGED
@@ -5,6 +5,8 @@ import SearchResults from "@/components/knowledge-base/search-results";
5
  import CitationPanel from "@/components/knowledge-base/citation-panel";
6
  import SystemFlowDiagram from "@/components/knowledge-base/system-flow-diagram";
7
  import { KnowledgeGraph } from "@/components/knowledge-base/knowledge-graph";
 
 
8
  import { ThemeToggle } from "@/components/theme-toggle";
9
  import { type SearchRequest, type SearchResponse, type Citation } from "@shared/schema";
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -180,9 +182,11 @@ export default function KnowledgeBase() {
180
  </div>
181
 
182
  <Tabs defaultValue="search" className="w-full">
183
- <TabsList className="grid w-full grid-cols-3 mb-6">
184
  <TabsTrigger value="search">πŸ” AI-Enhanced Search</TabsTrigger>
185
- <TabsTrigger value="flow">⚑ System Flow</TabsTrigger>
 
 
186
  <TabsTrigger value="graph">πŸ•ΈοΈ Knowledge Graph</TabsTrigger>
187
  </TabsList>
188
 
@@ -212,6 +216,15 @@ export default function KnowledgeBase() {
212
  />
213
  </TabsContent>
214
 
 
 
 
 
 
 
 
 
 
215
 
216
  <TabsContent value="flow">
217
  <SystemFlowDiagram />
 
5
  import CitationPanel from "@/components/knowledge-base/citation-panel";
6
  import SystemFlowDiagram from "@/components/knowledge-base/system-flow-diagram";
7
  import { KnowledgeGraph } from "@/components/knowledge-base/knowledge-graph";
8
+ import DocumentUpload from "@/components/knowledge-base/document-upload";
9
+ import VectorSearch from "@/components/knowledge-base/vector-search";
10
  import { ThemeToggle } from "@/components/theme-toggle";
11
  import { type SearchRequest, type SearchResponse, type Citation } from "@shared/schema";
12
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 
182
  </div>
183
 
184
  <Tabs defaultValue="search" className="w-full">
185
+ <TabsList className="grid w-full grid-cols-5 mb-6">
186
  <TabsTrigger value="search">πŸ” AI-Enhanced Search</TabsTrigger>
187
+ <TabsTrigger value="upload">πŸ“„ Document Upload</TabsTrigger>
188
+ <TabsTrigger value="vector">⚑ Vector Search</TabsTrigger>
189
+ <TabsTrigger value="flow">πŸ”§ System Flow</TabsTrigger>
190
  <TabsTrigger value="graph">πŸ•ΈοΈ Knowledge Graph</TabsTrigger>
191
  </TabsList>
192
 
 
216
  />
217
  </TabsContent>
218
 
219
+ {/* Document Upload */}
220
+ <TabsContent value="upload">
221
+ <DocumentUpload />
222
+ </TabsContent>
223
+
224
+ {/* Vector Search */}
225
+ <TabsContent value="vector">
226
+ <VectorSearch />
227
+ </TabsContent>
228
 
229
  <TabsContent value="flow">
230
  <SystemFlowDiagram />
data/knowledgebridge.db ADDED
Binary file (45.1 kB). View file
 
package-lock.json CHANGED
@@ -41,9 +41,12 @@
41
  "@radix-ui/react-tooltip": "^1.2.0",
42
  "@tailwindcss/typography": "^0.5.15",
43
  "@tanstack/react-query": "^5.60.5",
 
44
  "@types/d3": "^7.4.3",
 
45
  "@vitejs/plugin-react": "^4.3.2",
46
  "autoprefixer": "^10.4.20",
 
47
  "class-variance-authority": "^0.7.1",
48
  "clsx": "^2.1.1",
49
  "cmdk": "^1.1.1",
@@ -64,6 +67,7 @@
64
  "input-otp": "^1.4.2",
65
  "lucide-react": "^0.453.0",
66
  "memorystore": "^1.6.7",
 
67
  "next-themes": "^0.4.6",
68
  "openai": "^5.1.0",
69
  "passport": "^0.7.0",
@@ -3266,11 +3270,18 @@
3266
  "@babel/types": "^7.20.7"
3267
  }
3268
  },
 
 
 
 
 
 
 
 
3269
  "node_modules/@types/body-parser": {
3270
  "version": "1.19.5",
3271
  "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
3272
  "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
3273
- "dev": true,
3274
  "license": "MIT",
3275
  "dependencies": {
3276
  "@types/connect": "*",
@@ -3281,7 +3292,6 @@
3281
  "version": "3.4.38",
3282
  "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
3283
  "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
3284
- "dev": true,
3285
  "license": "MIT",
3286
  "dependencies": {
3287
  "@types/node": "*"
@@ -3562,7 +3572,6 @@
3562
  "version": "4.17.21",
3563
  "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
3564
  "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
3565
- "dev": true,
3566
  "license": "MIT",
3567
  "dependencies": {
3568
  "@types/body-parser": "*",
@@ -3575,7 +3584,6 @@
3575
  "version": "4.19.6",
3576
  "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
3577
  "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
3578
- "dev": true,
3579
  "license": "MIT",
3580
  "dependencies": {
3581
  "@types/node": "*",
@@ -3604,16 +3612,22 @@
3604
  "version": "2.0.4",
3605
  "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
3606
  "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
3607
- "dev": true,
3608
  "license": "MIT"
3609
  },
3610
  "node_modules/@types/mime": {
3611
  "version": "1.3.5",
3612
  "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
3613
  "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
3614
- "dev": true,
3615
  "license": "MIT"
3616
  },
 
 
 
 
 
 
 
 
3617
  "node_modules/@types/node": {
3618
  "version": "20.16.11",
3619
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
@@ -3678,14 +3692,12 @@
3678
  "version": "6.9.16",
3679
  "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
3680
  "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
3681
- "dev": true,
3682
  "license": "MIT"
3683
  },
3684
  "node_modules/@types/range-parser": {
3685
  "version": "1.2.7",
3686
  "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
3687
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
3688
- "dev": true,
3689
  "license": "MIT"
3690
  },
3691
  "node_modules/@types/react": {
@@ -3713,7 +3725,6 @@
3713
  "version": "0.17.4",
3714
  "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
3715
  "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
3716
- "dev": true,
3717
  "license": "MIT",
3718
  "dependencies": {
3719
  "@types/mime": "^1",
@@ -3724,7 +3735,6 @@
3724
  "version": "1.15.7",
3725
  "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
3726
  "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
3727
- "dev": true,
3728
  "license": "MIT",
3729
  "dependencies": {
3730
  "@types/http-errors": "*",
@@ -3842,6 +3852,11 @@
3842
  "node": ">= 8"
3843
  }
3844
  },
 
 
 
 
 
3845
  "node_modules/arg": {
3846
  "version": "5.0.2",
3847
  "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -3909,6 +3924,35 @@
3909
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
3910
  "license": "MIT"
3911
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3912
  "node_modules/bezier-js": {
3913
  "version": "6.1.4",
3914
  "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
@@ -3931,6 +3975,24 @@
3931
  "url": "https://github.com/sponsors/sindresorhus"
3932
  }
3933
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3934
  "node_modules/body-parser": {
3935
  "version": "1.20.3",
3936
  "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -4023,11 +4085,33 @@
4023
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
4024
  }
4025
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4026
  "node_modules/buffer-from": {
4027
  "version": "1.1.2",
4028
  "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
4029
  "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
4030
- "dev": true,
4031
  "license": "MIT"
4032
  },
4033
  "node_modules/bufferutil": {
@@ -4044,6 +4128,17 @@
4044
  "node": ">=6.14.2"
4045
  }
4046
  },
 
 
 
 
 
 
 
 
 
 
 
4047
  "node_modules/bytes": {
4048
  "version": "3.1.2",
4049
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -4148,6 +4243,11 @@
4148
  "node": ">= 6"
4149
  }
4150
  },
 
 
 
 
 
4151
  "node_modules/class-variance-authority": {
4152
  "version": "0.7.1",
4153
  "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -4209,6 +4309,20 @@
4209
  "node": ">= 6"
4210
  }
4211
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4212
  "node_modules/connect-pg-simple": {
4213
  "version": "10.0.0",
4214
  "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
@@ -4788,6 +4902,28 @@
4788
  "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
4789
  "license": "MIT"
4790
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4791
  "node_modules/define-data-property": {
4792
  "version": "1.1.4",
4793
  "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -4837,7 +4973,6 @@
4837
  "version": "2.0.3",
4838
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
4839
  "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
4840
- "devOptional": true,
4841
  "engines": {
4842
  "node": ">=8"
4843
  }
@@ -5542,6 +5677,14 @@
5542
  "node": ">= 0.8"
5543
  }
5544
  },
 
 
 
 
 
 
 
 
5545
  "node_modules/enhanced-resolve": {
5546
  "version": "5.18.1",
5547
  "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -5659,6 +5802,14 @@
5659
  "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
5660
  "license": "MIT"
5661
  },
 
 
 
 
 
 
 
 
5662
  "node_modules/express": {
5663
  "version": "4.21.2",
5664
  "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -5840,6 +5991,11 @@
5840
  "reusify": "^1.0.4"
5841
  }
5842
  },
 
 
 
 
 
5843
  "node_modules/fill-range": {
5844
  "version": "7.1.1",
5845
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -5999,6 +6155,11 @@
5999
  "node": ">= 0.6"
6000
  }
6001
  },
 
 
 
 
 
6002
  "node_modules/fsevents": {
6003
  "version": "2.3.3",
6004
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -6071,6 +6232,11 @@
6071
  "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
6072
  }
6073
  },
 
 
 
 
 
6074
  "node_modules/glob": {
6075
  "version": "10.4.5",
6076
  "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -6214,6 +6380,25 @@
6214
  "node": ">=0.10.0"
6215
  }
6216
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6217
  "node_modules/index-array-by": {
6218
  "version": "1.4.2",
6219
  "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
@@ -6229,6 +6414,11 @@
6229
  "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
6230
  "license": "ISC"
6231
  },
 
 
 
 
 
6232
  "node_modules/input-otp": {
6233
  "version": "1.4.2",
6234
  "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
@@ -6827,6 +7017,17 @@
6827
  "node": ">= 0.6"
6828
  }
6829
  },
 
 
 
 
 
 
 
 
 
 
 
6830
  "node_modules/minimatch": {
6831
  "version": "9.0.5",
6832
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -6846,7 +7047,6 @@
6846
  "version": "1.2.8",
6847
  "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
6848
  "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
6849
- "dev": true,
6850
  "funding": {
6851
  "url": "https://github.com/sponsors/ljharb"
6852
  }
@@ -6866,6 +7066,22 @@
6866
  "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
6867
  "license": "MIT"
6868
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6869
  "node_modules/modern-screenshot": {
6870
  "version": "4.6.0",
6871
  "resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz",
@@ -6888,6 +7104,23 @@
6888
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
6889
  "license": "MIT"
6890
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6891
  "node_modules/mz": {
6892
  "version": "2.7.0",
6893
  "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -6916,6 +7149,11 @@
6916
  "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
6917
  }
6918
  },
 
 
 
 
 
6919
  "node_modules/negotiator": {
6920
  "version": "0.6.3",
6921
  "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -6972,6 +7210,28 @@
6972
  "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==",
6973
  "license": "BSD-3-Clause"
6974
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6975
  "node_modules/node-gyp-build": {
6976
  "version": "4.8.3",
6977
  "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
@@ -7065,6 +7325,14 @@
7065
  "node": ">= 0.8"
7066
  }
7067
  },
 
 
 
 
 
 
 
 
7068
  "node_modules/openai": {
7069
  "version": "5.1.0",
7070
  "resolved": "https://registry.npmjs.org/openai/-/openai-5.1.0.tgz",
@@ -7587,6 +7855,31 @@
7587
  "url": "https://opencollective.com/preact"
7588
  }
7589
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7590
  "node_modules/prop-types": {
7591
  "version": "15.8.1",
7592
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7621,6 +7914,15 @@
7621
  "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
7622
  "license": "ISC"
7623
  },
 
 
 
 
 
 
 
 
 
7624
  "node_modules/qs": {
7625
  "version": "6.13.0",
7626
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -7689,6 +7991,20 @@
7689
  "node": ">= 0.8"
7690
  }
7691
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7692
  "node_modules/react": {
7693
  "version": "18.3.1",
7694
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -7926,6 +8242,19 @@
7926
  "pify": "^2.3.0"
7927
  }
7928
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
7929
  "node_modules/readdirp": {
7930
  "version": "3.6.0",
7931
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -8273,6 +8602,49 @@
8273
  "url": "https://github.com/sponsors/isaacs"
8274
  }
8275
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8276
  "node_modules/source-map": {
8277
  "version": "0.6.1",
8278
  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -8321,6 +8693,22 @@
8321
  "node": ">= 0.8"
8322
  }
8323
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8324
  "node_modules/string-width": {
8325
  "version": "5.1.2",
8326
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -8417,6 +8805,14 @@
8417
  "node": ">=8"
8418
  }
8419
  },
 
 
 
 
 
 
 
 
8420
  "node_modules/sucrase": {
8421
  "version": "3.35.0",
8422
  "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -8513,6 +8909,32 @@
8513
  "node": ">=6"
8514
  }
8515
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8516
  "node_modules/thenify": {
8517
  "version": "3.3.1",
8518
  "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -9096,6 +9518,17 @@
9096
  "@esbuild/win32-x64": "0.23.1"
9097
  }
9098
  },
 
 
 
 
 
 
 
 
 
 
 
9099
  "node_modules/tw-animate-css": {
9100
  "version": "1.2.5",
9101
  "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
@@ -9117,6 +9550,11 @@
9117
  "node": ">= 0.6"
9118
  }
9119
  },
 
 
 
 
 
9120
  "node_modules/typescript": {
9121
  "version": "5.6.3",
9122
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@@ -9889,6 +10327,11 @@
9889
  "node": ">=8"
9890
  }
9891
  },
 
 
 
 
 
9892
  "node_modules/ws": {
9893
  "version": "8.18.0",
9894
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
 
41
  "@radix-ui/react-tooltip": "^1.2.0",
42
  "@tailwindcss/typography": "^0.5.15",
43
  "@tanstack/react-query": "^5.60.5",
44
+ "@types/better-sqlite3": "^7.6.13",
45
  "@types/d3": "^7.4.3",
46
+ "@types/multer": "^1.4.13",
47
  "@vitejs/plugin-react": "^4.3.2",
48
  "autoprefixer": "^10.4.20",
49
+ "better-sqlite3": "^11.10.0",
50
  "class-variance-authority": "^0.7.1",
51
  "clsx": "^2.1.1",
52
  "cmdk": "^1.1.1",
 
67
  "input-otp": "^1.4.2",
68
  "lucide-react": "^0.453.0",
69
  "memorystore": "^1.6.7",
70
+ "multer": "^2.0.1",
71
  "next-themes": "^0.4.6",
72
  "openai": "^5.1.0",
73
  "passport": "^0.7.0",
 
3270
  "@babel/types": "^7.20.7"
3271
  }
3272
  },
3273
+ "node_modules/@types/better-sqlite3": {
3274
+ "version": "7.6.13",
3275
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
3276
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
3277
+ "dependencies": {
3278
+ "@types/node": "*"
3279
+ }
3280
+ },
3281
  "node_modules/@types/body-parser": {
3282
  "version": "1.19.5",
3283
  "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
3284
  "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
 
3285
  "license": "MIT",
3286
  "dependencies": {
3287
  "@types/connect": "*",
 
3292
  "version": "3.4.38",
3293
  "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
3294
  "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
 
3295
  "license": "MIT",
3296
  "dependencies": {
3297
  "@types/node": "*"
 
3572
  "version": "4.17.21",
3573
  "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
3574
  "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
 
3575
  "license": "MIT",
3576
  "dependencies": {
3577
  "@types/body-parser": "*",
 
3584
  "version": "4.19.6",
3585
  "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
3586
  "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
 
3587
  "license": "MIT",
3588
  "dependencies": {
3589
  "@types/node": "*",
 
3612
  "version": "2.0.4",
3613
  "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
3614
  "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
 
3615
  "license": "MIT"
3616
  },
3617
  "node_modules/@types/mime": {
3618
  "version": "1.3.5",
3619
  "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
3620
  "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
 
3621
  "license": "MIT"
3622
  },
3623
+ "node_modules/@types/multer": {
3624
+ "version": "1.4.13",
3625
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
3626
+ "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
3627
+ "dependencies": {
3628
+ "@types/express": "*"
3629
+ }
3630
+ },
3631
  "node_modules/@types/node": {
3632
  "version": "20.16.11",
3633
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
 
3692
  "version": "6.9.16",
3693
  "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
3694
  "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
 
3695
  "license": "MIT"
3696
  },
3697
  "node_modules/@types/range-parser": {
3698
  "version": "1.2.7",
3699
  "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
3700
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
 
3701
  "license": "MIT"
3702
  },
3703
  "node_modules/@types/react": {
 
3725
  "version": "0.17.4",
3726
  "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
3727
  "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
 
3728
  "license": "MIT",
3729
  "dependencies": {
3730
  "@types/mime": "^1",
 
3735
  "version": "1.15.7",
3736
  "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
3737
  "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
 
3738
  "license": "MIT",
3739
  "dependencies": {
3740
  "@types/http-errors": "*",
 
3852
  "node": ">= 8"
3853
  }
3854
  },
3855
+ "node_modules/append-field": {
3856
+ "version": "1.0.0",
3857
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
3858
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
3859
+ },
3860
  "node_modules/arg": {
3861
  "version": "5.0.2",
3862
  "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
 
3924
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
3925
  "license": "MIT"
3926
  },
3927
+ "node_modules/base64-js": {
3928
+ "version": "1.5.1",
3929
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
3930
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
3931
+ "funding": [
3932
+ {
3933
+ "type": "github",
3934
+ "url": "https://github.com/sponsors/feross"
3935
+ },
3936
+ {
3937
+ "type": "patreon",
3938
+ "url": "https://www.patreon.com/feross"
3939
+ },
3940
+ {
3941
+ "type": "consulting",
3942
+ "url": "https://feross.org/support"
3943
+ }
3944
+ ]
3945
+ },
3946
+ "node_modules/better-sqlite3": {
3947
+ "version": "11.10.0",
3948
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
3949
+ "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
3950
+ "hasInstallScript": true,
3951
+ "dependencies": {
3952
+ "bindings": "^1.5.0",
3953
+ "prebuild-install": "^7.1.1"
3954
+ }
3955
+ },
3956
  "node_modules/bezier-js": {
3957
  "version": "6.1.4",
3958
  "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
 
3975
  "url": "https://github.com/sponsors/sindresorhus"
3976
  }
3977
  },
3978
+ "node_modules/bindings": {
3979
+ "version": "1.5.0",
3980
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
3981
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
3982
+ "dependencies": {
3983
+ "file-uri-to-path": "1.0.0"
3984
+ }
3985
+ },
3986
+ "node_modules/bl": {
3987
+ "version": "4.1.0",
3988
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
3989
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
3990
+ "dependencies": {
3991
+ "buffer": "^5.5.0",
3992
+ "inherits": "^2.0.4",
3993
+ "readable-stream": "^3.4.0"
3994
+ }
3995
+ },
3996
  "node_modules/body-parser": {
3997
  "version": "1.20.3",
3998
  "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
 
4085
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
4086
  }
4087
  },
4088
+ "node_modules/buffer": {
4089
+ "version": "5.7.1",
4090
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
4091
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
4092
+ "funding": [
4093
+ {
4094
+ "type": "github",
4095
+ "url": "https://github.com/sponsors/feross"
4096
+ },
4097
+ {
4098
+ "type": "patreon",
4099
+ "url": "https://www.patreon.com/feross"
4100
+ },
4101
+ {
4102
+ "type": "consulting",
4103
+ "url": "https://feross.org/support"
4104
+ }
4105
+ ],
4106
+ "dependencies": {
4107
+ "base64-js": "^1.3.1",
4108
+ "ieee754": "^1.1.13"
4109
+ }
4110
+ },
4111
  "node_modules/buffer-from": {
4112
  "version": "1.1.2",
4113
  "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
4114
  "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
 
4115
  "license": "MIT"
4116
  },
4117
  "node_modules/bufferutil": {
 
4128
  "node": ">=6.14.2"
4129
  }
4130
  },
4131
+ "node_modules/busboy": {
4132
+ "version": "1.6.0",
4133
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
4134
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
4135
+ "dependencies": {
4136
+ "streamsearch": "^1.1.0"
4137
+ },
4138
+ "engines": {
4139
+ "node": ">=10.16.0"
4140
+ }
4141
+ },
4142
  "node_modules/bytes": {
4143
  "version": "3.1.2",
4144
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
 
4243
  "node": ">= 6"
4244
  }
4245
  },
4246
+ "node_modules/chownr": {
4247
+ "version": "1.1.4",
4248
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
4249
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
4250
+ },
4251
  "node_modules/class-variance-authority": {
4252
  "version": "0.7.1",
4253
  "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
 
4309
  "node": ">= 6"
4310
  }
4311
  },
4312
+ "node_modules/concat-stream": {
4313
+ "version": "2.0.0",
4314
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
4315
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
4316
+ "engines": [
4317
+ "node >= 6.0"
4318
+ ],
4319
+ "dependencies": {
4320
+ "buffer-from": "^1.0.0",
4321
+ "inherits": "^2.0.3",
4322
+ "readable-stream": "^3.0.2",
4323
+ "typedarray": "^0.0.6"
4324
+ }
4325
+ },
4326
  "node_modules/connect-pg-simple": {
4327
  "version": "10.0.0",
4328
  "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
 
4902
  "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
4903
  "license": "MIT"
4904
  },
4905
+ "node_modules/decompress-response": {
4906
+ "version": "6.0.0",
4907
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
4908
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
4909
+ "dependencies": {
4910
+ "mimic-response": "^3.1.0"
4911
+ },
4912
+ "engines": {
4913
+ "node": ">=10"
4914
+ },
4915
+ "funding": {
4916
+ "url": "https://github.com/sponsors/sindresorhus"
4917
+ }
4918
+ },
4919
+ "node_modules/deep-extend": {
4920
+ "version": "0.6.0",
4921
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
4922
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
4923
+ "engines": {
4924
+ "node": ">=4.0.0"
4925
+ }
4926
+ },
4927
  "node_modules/define-data-property": {
4928
  "version": "1.1.4",
4929
  "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
 
4973
  "version": "2.0.3",
4974
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
4975
  "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
 
4976
  "engines": {
4977
  "node": ">=8"
4978
  }
 
5677
  "node": ">= 0.8"
5678
  }
5679
  },
5680
+ "node_modules/end-of-stream": {
5681
+ "version": "1.4.4",
5682
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
5683
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
5684
+ "dependencies": {
5685
+ "once": "^1.4.0"
5686
+ }
5687
+ },
5688
  "node_modules/enhanced-resolve": {
5689
  "version": "5.18.1",
5690
  "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
 
5802
  "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
5803
  "license": "MIT"
5804
  },
5805
+ "node_modules/expand-template": {
5806
+ "version": "2.0.3",
5807
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
5808
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
5809
+ "engines": {
5810
+ "node": ">=6"
5811
+ }
5812
+ },
5813
  "node_modules/express": {
5814
  "version": "4.21.2",
5815
  "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
 
5991
  "reusify": "^1.0.4"
5992
  }
5993
  },
5994
+ "node_modules/file-uri-to-path": {
5995
+ "version": "1.0.0",
5996
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
5997
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
5998
+ },
5999
  "node_modules/fill-range": {
6000
  "version": "7.1.1",
6001
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
 
6155
  "node": ">= 0.6"
6156
  }
6157
  },
6158
+ "node_modules/fs-constants": {
6159
+ "version": "1.0.0",
6160
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
6161
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
6162
+ },
6163
  "node_modules/fsevents": {
6164
  "version": "2.3.3",
6165
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
6232
  "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
6233
  }
6234
  },
6235
+ "node_modules/github-from-package": {
6236
+ "version": "0.0.0",
6237
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
6238
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
6239
+ },
6240
  "node_modules/glob": {
6241
  "version": "10.4.5",
6242
  "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
 
6380
  "node": ">=0.10.0"
6381
  }
6382
  },
6383
+ "node_modules/ieee754": {
6384
+ "version": "1.2.1",
6385
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
6386
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
6387
+ "funding": [
6388
+ {
6389
+ "type": "github",
6390
+ "url": "https://github.com/sponsors/feross"
6391
+ },
6392
+ {
6393
+ "type": "patreon",
6394
+ "url": "https://www.patreon.com/feross"
6395
+ },
6396
+ {
6397
+ "type": "consulting",
6398
+ "url": "https://feross.org/support"
6399
+ }
6400
+ ]
6401
+ },
6402
  "node_modules/index-array-by": {
6403
  "version": "1.4.2",
6404
  "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
 
6414
  "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
6415
  "license": "ISC"
6416
  },
6417
+ "node_modules/ini": {
6418
+ "version": "1.3.8",
6419
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
6420
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
6421
+ },
6422
  "node_modules/input-otp": {
6423
  "version": "1.4.2",
6424
  "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
 
7017
  "node": ">= 0.6"
7018
  }
7019
  },
7020
+ "node_modules/mimic-response": {
7021
+ "version": "3.1.0",
7022
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
7023
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
7024
+ "engines": {
7025
+ "node": ">=10"
7026
+ },
7027
+ "funding": {
7028
+ "url": "https://github.com/sponsors/sindresorhus"
7029
+ }
7030
+ },
7031
  "node_modules/minimatch": {
7032
  "version": "9.0.5",
7033
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
 
7047
  "version": "1.2.8",
7048
  "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
7049
  "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
 
7050
  "funding": {
7051
  "url": "https://github.com/sponsors/ljharb"
7052
  }
 
7066
  "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
7067
  "license": "MIT"
7068
  },
7069
+ "node_modules/mkdirp": {
7070
+ "version": "0.5.6",
7071
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
7072
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
7073
+ "dependencies": {
7074
+ "minimist": "^1.2.6"
7075
+ },
7076
+ "bin": {
7077
+ "mkdirp": "bin/cmd.js"
7078
+ }
7079
+ },
7080
+ "node_modules/mkdirp-classic": {
7081
+ "version": "0.5.3",
7082
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
7083
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
7084
+ },
7085
  "node_modules/modern-screenshot": {
7086
  "version": "4.6.0",
7087
  "resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz",
 
7104
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
7105
  "license": "MIT"
7106
  },
7107
+ "node_modules/multer": {
7108
+ "version": "2.0.1",
7109
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz",
7110
+ "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==",
7111
+ "dependencies": {
7112
+ "append-field": "^1.0.0",
7113
+ "busboy": "^1.6.0",
7114
+ "concat-stream": "^2.0.0",
7115
+ "mkdirp": "^0.5.6",
7116
+ "object-assign": "^4.1.1",
7117
+ "type-is": "^1.6.18",
7118
+ "xtend": "^4.0.2"
7119
+ },
7120
+ "engines": {
7121
+ "node": ">= 10.16.0"
7122
+ }
7123
+ },
7124
  "node_modules/mz": {
7125
  "version": "2.7.0",
7126
  "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
 
7149
  "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
7150
  }
7151
  },
7152
+ "node_modules/napi-build-utils": {
7153
+ "version": "2.0.0",
7154
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
7155
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
7156
+ },
7157
  "node_modules/negotiator": {
7158
  "version": "0.6.3",
7159
  "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
 
7210
  "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==",
7211
  "license": "BSD-3-Clause"
7212
  },
7213
+ "node_modules/node-abi": {
7214
+ "version": "3.75.0",
7215
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
7216
+ "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
7217
+ "dependencies": {
7218
+ "semver": "^7.3.5"
7219
+ },
7220
+ "engines": {
7221
+ "node": ">=10"
7222
+ }
7223
+ },
7224
+ "node_modules/node-abi/node_modules/semver": {
7225
+ "version": "7.7.2",
7226
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
7227
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
7228
+ "bin": {
7229
+ "semver": "bin/semver.js"
7230
+ },
7231
+ "engines": {
7232
+ "node": ">=10"
7233
+ }
7234
+ },
7235
  "node_modules/node-gyp-build": {
7236
  "version": "4.8.3",
7237
  "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
 
7325
  "node": ">= 0.8"
7326
  }
7327
  },
7328
+ "node_modules/once": {
7329
+ "version": "1.4.0",
7330
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
7331
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
7332
+ "dependencies": {
7333
+ "wrappy": "1"
7334
+ }
7335
+ },
7336
  "node_modules/openai": {
7337
  "version": "5.1.0",
7338
  "resolved": "https://registry.npmjs.org/openai/-/openai-5.1.0.tgz",
 
7855
  "url": "https://opencollective.com/preact"
7856
  }
7857
  },
7858
+ "node_modules/prebuild-install": {
7859
+ "version": "7.1.3",
7860
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
7861
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
7862
+ "dependencies": {
7863
+ "detect-libc": "^2.0.0",
7864
+ "expand-template": "^2.0.3",
7865
+ "github-from-package": "0.0.0",
7866
+ "minimist": "^1.2.3",
7867
+ "mkdirp-classic": "^0.5.3",
7868
+ "napi-build-utils": "^2.0.0",
7869
+ "node-abi": "^3.3.0",
7870
+ "pump": "^3.0.0",
7871
+ "rc": "^1.2.7",
7872
+ "simple-get": "^4.0.0",
7873
+ "tar-fs": "^2.0.0",
7874
+ "tunnel-agent": "^0.6.0"
7875
+ },
7876
+ "bin": {
7877
+ "prebuild-install": "bin.js"
7878
+ },
7879
+ "engines": {
7880
+ "node": ">=10"
7881
+ }
7882
+ },
7883
  "node_modules/prop-types": {
7884
  "version": "15.8.1",
7885
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
 
7914
  "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
7915
  "license": "ISC"
7916
  },
7917
+ "node_modules/pump": {
7918
+ "version": "3.0.2",
7919
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
7920
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
7921
+ "dependencies": {
7922
+ "end-of-stream": "^1.1.0",
7923
+ "once": "^1.3.1"
7924
+ }
7925
+ },
7926
  "node_modules/qs": {
7927
  "version": "6.13.0",
7928
  "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
 
7991
  "node": ">= 0.8"
7992
  }
7993
  },
7994
+ "node_modules/rc": {
7995
+ "version": "1.2.8",
7996
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
7997
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
7998
+ "dependencies": {
7999
+ "deep-extend": "^0.6.0",
8000
+ "ini": "~1.3.0",
8001
+ "minimist": "^1.2.0",
8002
+ "strip-json-comments": "~2.0.1"
8003
+ },
8004
+ "bin": {
8005
+ "rc": "cli.js"
8006
+ }
8007
+ },
8008
  "node_modules/react": {
8009
  "version": "18.3.1",
8010
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
 
8242
  "pify": "^2.3.0"
8243
  }
8244
  },
8245
+ "node_modules/readable-stream": {
8246
+ "version": "3.6.2",
8247
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
8248
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
8249
+ "dependencies": {
8250
+ "inherits": "^2.0.3",
8251
+ "string_decoder": "^1.1.1",
8252
+ "util-deprecate": "^1.0.1"
8253
+ },
8254
+ "engines": {
8255
+ "node": ">= 6"
8256
+ }
8257
+ },
8258
  "node_modules/readdirp": {
8259
  "version": "3.6.0",
8260
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
 
8602
  "url": "https://github.com/sponsors/isaacs"
8603
  }
8604
  },
8605
+ "node_modules/simple-concat": {
8606
+ "version": "1.0.1",
8607
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
8608
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
8609
+ "funding": [
8610
+ {
8611
+ "type": "github",
8612
+ "url": "https://github.com/sponsors/feross"
8613
+ },
8614
+ {
8615
+ "type": "patreon",
8616
+ "url": "https://www.patreon.com/feross"
8617
+ },
8618
+ {
8619
+ "type": "consulting",
8620
+ "url": "https://feross.org/support"
8621
+ }
8622
+ ]
8623
+ },
8624
+ "node_modules/simple-get": {
8625
+ "version": "4.0.1",
8626
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
8627
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
8628
+ "funding": [
8629
+ {
8630
+ "type": "github",
8631
+ "url": "https://github.com/sponsors/feross"
8632
+ },
8633
+ {
8634
+ "type": "patreon",
8635
+ "url": "https://www.patreon.com/feross"
8636
+ },
8637
+ {
8638
+ "type": "consulting",
8639
+ "url": "https://feross.org/support"
8640
+ }
8641
+ ],
8642
+ "dependencies": {
8643
+ "decompress-response": "^6.0.0",
8644
+ "once": "^1.3.1",
8645
+ "simple-concat": "^1.0.0"
8646
+ }
8647
+ },
8648
  "node_modules/source-map": {
8649
  "version": "0.6.1",
8650
  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
 
8693
  "node": ">= 0.8"
8694
  }
8695
  },
8696
+ "node_modules/streamsearch": {
8697
+ "version": "1.1.0",
8698
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
8699
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
8700
+ "engines": {
8701
+ "node": ">=10.0.0"
8702
+ }
8703
+ },
8704
+ "node_modules/string_decoder": {
8705
+ "version": "1.3.0",
8706
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
8707
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
8708
+ "dependencies": {
8709
+ "safe-buffer": "~5.2.0"
8710
+ }
8711
+ },
8712
  "node_modules/string-width": {
8713
  "version": "5.1.2",
8714
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
 
8805
  "node": ">=8"
8806
  }
8807
  },
8808
+ "node_modules/strip-json-comments": {
8809
+ "version": "2.0.1",
8810
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
8811
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
8812
+ "engines": {
8813
+ "node": ">=0.10.0"
8814
+ }
8815
+ },
8816
  "node_modules/sucrase": {
8817
  "version": "3.35.0",
8818
  "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
 
8909
  "node": ">=6"
8910
  }
8911
  },
8912
+ "node_modules/tar-fs": {
8913
+ "version": "2.1.3",
8914
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
8915
+ "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
8916
+ "dependencies": {
8917
+ "chownr": "^1.1.1",
8918
+ "mkdirp-classic": "^0.5.2",
8919
+ "pump": "^3.0.0",
8920
+ "tar-stream": "^2.1.4"
8921
+ }
8922
+ },
8923
+ "node_modules/tar-stream": {
8924
+ "version": "2.2.0",
8925
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
8926
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
8927
+ "dependencies": {
8928
+ "bl": "^4.0.3",
8929
+ "end-of-stream": "^1.4.1",
8930
+ "fs-constants": "^1.0.0",
8931
+ "inherits": "^2.0.3",
8932
+ "readable-stream": "^3.1.1"
8933
+ },
8934
+ "engines": {
8935
+ "node": ">=6"
8936
+ }
8937
+ },
8938
  "node_modules/thenify": {
8939
  "version": "3.3.1",
8940
  "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
 
9518
  "@esbuild/win32-x64": "0.23.1"
9519
  }
9520
  },
9521
+ "node_modules/tunnel-agent": {
9522
+ "version": "0.6.0",
9523
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
9524
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
9525
+ "dependencies": {
9526
+ "safe-buffer": "^5.0.1"
9527
+ },
9528
+ "engines": {
9529
+ "node": "*"
9530
+ }
9531
+ },
9532
  "node_modules/tw-animate-css": {
9533
  "version": "1.2.5",
9534
  "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz",
 
9550
  "node": ">= 0.6"
9551
  }
9552
  },
9553
+ "node_modules/typedarray": {
9554
+ "version": "0.0.6",
9555
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
9556
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
9557
+ },
9558
  "node_modules/typescript": {
9559
  "version": "5.6.3",
9560
  "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
 
10327
  "node": ">=8"
10328
  }
10329
  },
10330
+ "node_modules/wrappy": {
10331
+ "version": "1.0.2",
10332
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
10333
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
10334
+ },
10335
  "node_modules/ws": {
10336
  "version": "8.18.0",
10337
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
package.json CHANGED
@@ -43,9 +43,12 @@
43
  "@radix-ui/react-tooltip": "^1.2.0",
44
  "@tailwindcss/typography": "^0.5.15",
45
  "@tanstack/react-query": "^5.60.5",
 
46
  "@types/d3": "^7.4.3",
 
47
  "@vitejs/plugin-react": "^4.3.2",
48
  "autoprefixer": "^10.4.20",
 
49
  "class-variance-authority": "^0.7.1",
50
  "clsx": "^2.1.1",
51
  "cmdk": "^1.1.1",
@@ -66,6 +69,7 @@
66
  "input-otp": "^1.4.2",
67
  "lucide-react": "^0.453.0",
68
  "memorystore": "^1.6.7",
 
69
  "next-themes": "^0.4.6",
70
  "openai": "^5.1.0",
71
  "passport": "^0.7.0",
 
43
  "@radix-ui/react-tooltip": "^1.2.0",
44
  "@tailwindcss/typography": "^0.5.15",
45
  "@tanstack/react-query": "^5.60.5",
46
+ "@types/better-sqlite3": "^7.6.13",
47
  "@types/d3": "^7.4.3",
48
+ "@types/multer": "^1.4.13",
49
  "@vitejs/plugin-react": "^4.3.2",
50
  "autoprefixer": "^10.4.20",
51
+ "better-sqlite3": "^11.10.0",
52
  "class-variance-authority": "^0.7.1",
53
  "clsx": "^2.1.1",
54
  "cmdk": "^1.1.1",
 
69
  "input-otp": "^1.4.2",
70
  "lucide-react": "^0.453.0",
71
  "memorystore": "^1.6.7",
72
+ "multer": "^2.0.1",
73
  "next-themes": "^0.4.6",
74
  "openai": "^5.1.0",
75
  "passport": "^0.7.0",
server/document-processor.ts ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { modalClient } from './modal-client';
4
+ import { nebiusClient } from './nebius-client';
5
+ import { FileProcessor } from './file-upload';
6
+ import { type Document, type InsertDocument } from '@shared/schema';
7
+
8
+ export interface ProcessingResult {
9
+ success: boolean;
10
+ extractedText?: string;
11
+ embeddings?: number[];
12
+ modalTaskId?: string;
13
+ error?: string;
14
+ processingTime: number;
15
+ }
16
+
17
+ export interface BatchProcessingResult {
18
+ success: boolean;
19
+ processedCount: number;
20
+ failedCount: number;
21
+ results: Array<{
22
+ documentId: number;
23
+ success: boolean;
24
+ extractedText?: string;
25
+ embeddings?: number[];
26
+ error?: string;
27
+ }>;
28
+ totalProcessingTime: number;
29
+ }
30
+
31
+ export class DocumentProcessor {
32
+ private static instance: DocumentProcessor;
33
+
34
+ static getInstance(): DocumentProcessor {
35
+ if (!DocumentProcessor.instance) {
36
+ DocumentProcessor.instance = new DocumentProcessor();
37
+ }
38
+ return DocumentProcessor.instance;
39
+ }
40
+
41
+ /**
42
+ * Process a single document using Modal for heavy workloads
43
+ */
44
+ async processDocument(
45
+ document: Document,
46
+ operations: Array<'extract_text' | 'generate_embedding' | 'build_index'> = ['extract_text']
47
+ ): Promise<ProcessingResult> {
48
+ const startTime = Date.now();
49
+
50
+ try {
51
+ let extractedText = document.content;
52
+ let embeddings: number[] | undefined;
53
+ let modalTaskId: string | undefined;
54
+
55
+ // Step 1: Extract text if needed (for PDFs and images)
56
+ if (operations.includes('extract_text') && document.filePath) {
57
+ const textResult = await this.extractText(document);
58
+ if (textResult.success) {
59
+ extractedText = textResult.extractedText || document.content;
60
+ modalTaskId = textResult.modalTaskId;
61
+ } else {
62
+ console.warn(`Text extraction failed for document ${document.id}: ${textResult.error}`);
63
+ }
64
+ }
65
+
66
+ // Step 2: Generate embeddings if requested
67
+ if (operations.includes('generate_embedding') && extractedText) {
68
+ const embeddingResult = await this.generateEmbeddings(extractedText);
69
+ if (embeddingResult.success) {
70
+ embeddings = embeddingResult.embeddings;
71
+ } else {
72
+ console.warn(`Embedding generation failed for document ${document.id}: ${embeddingResult.error}`);
73
+ }
74
+ }
75
+
76
+ const processingTime = Date.now() - startTime;
77
+
78
+ return {
79
+ success: true,
80
+ extractedText,
81
+ embeddings,
82
+ modalTaskId,
83
+ processingTime
84
+ };
85
+
86
+ } catch (error) {
87
+ const processingTime = Date.now() - startTime;
88
+ return {
89
+ success: false,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ processingTime
92
+ };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Process multiple documents in batch using Modal's distributed computing
98
+ */
99
+ async batchProcessDocuments(
100
+ documents: Document[],
101
+ operations: Array<'extract_text' | 'generate_embedding' | 'build_index'> = ['extract_text']
102
+ ): Promise<BatchProcessingResult> {
103
+ const startTime = Date.now();
104
+ const results: BatchProcessingResult['results'] = [];
105
+
106
+ try {
107
+ // Separate documents by processing requirements
108
+ const documentsForModal = documents.filter(doc =>
109
+ doc.filePath && FileProcessor.requiresOCR(doc.mimeType || '')
110
+ );
111
+
112
+ const documentsForLocal = documents.filter(doc =>
113
+ !doc.filePath || !FileProcessor.requiresOCR(doc.mimeType || '')
114
+ );
115
+
116
+ // Process Modal-required documents in batch
117
+ if (documentsForModal.length > 0 && operations.includes('extract_text')) {
118
+ try {
119
+ const modalResults = await this.batchExtractTextModal(documentsForModal);
120
+ results.push(...modalResults);
121
+ } catch (error) {
122
+ console.error('Modal batch processing failed:', error);
123
+ // Fall back to individual processing
124
+ for (const doc of documentsForModal) {
125
+ const result = await this.processDocument(doc, operations);
126
+ results.push({
127
+ documentId: doc.id,
128
+ success: result.success,
129
+ extractedText: result.extractedText,
130
+ embeddings: result.embeddings,
131
+ error: result.error
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ // Process local documents
138
+ for (const doc of documentsForLocal) {
139
+ const result = await this.processDocument(doc, operations);
140
+ results.push({
141
+ documentId: doc.id,
142
+ success: result.success,
143
+ extractedText: result.extractedText,
144
+ embeddings: result.embeddings,
145
+ error: result.error
146
+ });
147
+ }
148
+
149
+ const totalProcessingTime = Date.now() - startTime;
150
+ const successCount = results.filter(r => r.success).length;
151
+ const failedCount = results.length - successCount;
152
+
153
+ return {
154
+ success: true,
155
+ processedCount: successCount,
156
+ failedCount,
157
+ results,
158
+ totalProcessingTime
159
+ };
160
+
161
+ } catch (error) {
162
+ const totalProcessingTime = Date.now() - startTime;
163
+ return {
164
+ success: false,
165
+ processedCount: 0,
166
+ failedCount: documents.length,
167
+ results: documents.map(doc => ({
168
+ documentId: doc.id,
169
+ success: false,
170
+ error: error instanceof Error ? error.message : String(error)
171
+ })),
172
+ totalProcessingTime
173
+ };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Extract text from a document using Modal for PDFs/images or direct reading for text files
179
+ */
180
+ private async extractText(document: Document): Promise<{
181
+ success: boolean;
182
+ extractedText?: string;
183
+ modalTaskId?: string;
184
+ error?: string;
185
+ }> {
186
+ if (!document.filePath) {
187
+ return { success: true, extractedText: document.content };
188
+ }
189
+
190
+ const mimeType = document.mimeType || '';
191
+
192
+ try {
193
+ // For text files, read directly
194
+ if (FileProcessor.isTextFile(mimeType)) {
195
+ const content = await FileProcessor.readTextFile(document.filePath);
196
+ return { success: true, extractedText: content };
197
+ }
198
+
199
+ // For PDFs and images, use Modal
200
+ if (FileProcessor.requiresOCR(mimeType)) {
201
+ return await this.extractTextModal(document);
202
+ }
203
+
204
+ // Fallback: return existing content
205
+ return { success: true, extractedText: document.content };
206
+
207
+ } catch (error) {
208
+ return {
209
+ success: false,
210
+ error: error instanceof Error ? error.message : String(error)
211
+ };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Extract text using Modal for OCR-required files
217
+ */
218
+ private async extractTextModal(document: Document): Promise<{
219
+ success: boolean;
220
+ extractedText?: string;
221
+ modalTaskId?: string;
222
+ error?: string;
223
+ }> {
224
+ try {
225
+ if (!document.filePath) {
226
+ throw new Error('No file path provided for Modal processing');
227
+ }
228
+
229
+ // Read file and convert to base64
230
+ const fileBuffer = await fs.promises.readFile(document.filePath);
231
+ const base64Content = fileBuffer.toString('base64');
232
+
233
+ // Prepare document for Modal
234
+ const modalDocument = {
235
+ id: document.id.toString(),
236
+ content: base64Content,
237
+ contentType: document.mimeType || 'application/octet-stream'
238
+ };
239
+
240
+ // Call Modal extract-text endpoint
241
+ const result = await modalClient.extractTextFromDocuments([modalDocument]);
242
+
243
+ if (result.status === 'completed' && result.results?.length > 0) {
244
+ const extractionResult = result.results[0];
245
+ if (extractionResult.status === 'completed') {
246
+ return {
247
+ success: true,
248
+ extractedText: extractionResult.extracted_text,
249
+ modalTaskId: result.task_id
250
+ };
251
+ } else {
252
+ return {
253
+ success: false,
254
+ error: extractionResult.error || 'Modal extraction failed'
255
+ };
256
+ }
257
+ } else {
258
+ return {
259
+ success: false,
260
+ error: result.error || 'Modal processing failed'
261
+ };
262
+ }
263
+
264
+ } catch (error) {
265
+ console.error('Modal text extraction failed:', error);
266
+ return {
267
+ success: false,
268
+ error: error instanceof Error ? error.message : String(error)
269
+ };
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Batch extract text using Modal
275
+ */
276
+ private async batchExtractTextModal(documents: Document[]): Promise<Array<{
277
+ documentId: number;
278
+ success: boolean;
279
+ extractedText?: string;
280
+ error?: string;
281
+ }>> {
282
+ const modalDocuments = await Promise.all(
283
+ documents.map(async (doc) => {
284
+ if (!doc.filePath) return null;
285
+
286
+ try {
287
+ const fileBuffer = await fs.promises.readFile(doc.filePath);
288
+ return {
289
+ id: doc.id.toString(),
290
+ content: fileBuffer.toString('base64'),
291
+ contentType: doc.mimeType || 'application/octet-stream'
292
+ };
293
+ } catch (error) {
294
+ console.error(`Failed to read file for document ${doc.id}:`, error);
295
+ return null;
296
+ }
297
+ })
298
+ );
299
+
300
+ const validDocuments = modalDocuments.filter(doc => doc !== null) as any[];
301
+
302
+ if (validDocuments.length === 0) {
303
+ return documents.map(doc => ({
304
+ documentId: doc.id,
305
+ success: false,
306
+ error: 'No valid documents for processing'
307
+ }));
308
+ }
309
+
310
+ try {
311
+ const batchResult = await modalClient.batchProcessDocuments({
312
+ documents: validDocuments,
313
+ modelName: 'text-embedding-3-small',
314
+ batchSize: Math.min(validDocuments.length, 10)
315
+ });
316
+
317
+ if (batchResult.status === 'completed' && batchResult.extraction_results) {
318
+ return batchResult.extraction_results.map((result: any) => ({
319
+ documentId: parseInt(result.id),
320
+ success: result.status === 'completed',
321
+ extractedText: result.extracted_text,
322
+ error: result.error
323
+ }));
324
+ } else {
325
+ throw new Error(batchResult.error || 'Batch processing failed');
326
+ }
327
+
328
+ } catch (error) {
329
+ console.error('Modal batch processing failed:', error);
330
+ return documents.map(doc => ({
331
+ documentId: doc.id,
332
+ success: false,
333
+ error: error instanceof Error ? error.message : String(error)
334
+ }));
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Generate embeddings using Nebius AI
340
+ */
341
+ private async generateEmbeddings(text: string): Promise<{
342
+ success: boolean;
343
+ embeddings?: number[];
344
+ error?: string;
345
+ }> {
346
+ try {
347
+ // Truncate text if too long (most embedding models have token limits)
348
+ const maxLength = 8000; // Conservative limit
349
+ const truncatedText = text.length > maxLength ? text.substring(0, maxLength) : text;
350
+
351
+ const result = await nebiusClient.generateEmbeddings(truncatedText);
352
+
353
+ if (result.success && result.embeddings) {
354
+ return {
355
+ success: true,
356
+ embeddings: result.embeddings
357
+ };
358
+ } else {
359
+ return {
360
+ success: false,
361
+ error: result.error || 'Embedding generation failed'
362
+ };
363
+ }
364
+
365
+ } catch (error) {
366
+ return {
367
+ success: false,
368
+ error: error instanceof Error ? error.message : String(error)
369
+ };
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Build vector index using Modal
375
+ */
376
+ async buildVectorIndex(
377
+ documents: Document[],
378
+ indexName = 'main_index'
379
+ ): Promise<{
380
+ success: boolean;
381
+ indexName?: string;
382
+ documentCount?: number;
383
+ error?: string;
384
+ }> {
385
+ try {
386
+ const modalDocuments = documents.map(doc => ({
387
+ id: doc.id.toString(),
388
+ content: doc.content,
389
+ title: doc.title,
390
+ source: doc.source
391
+ }));
392
+
393
+ const result = await modalClient.buildVectorIndex(modalDocuments, {
394
+ indexName,
395
+ dimension: 1536, // Standard OpenAI embedding dimension
396
+ indexType: 'IVF',
397
+ nlist: Math.min(100, Math.max(10, Math.floor(documents.length / 10)))
398
+ });
399
+
400
+ if (result.status === 'completed') {
401
+ return {
402
+ success: true,
403
+ indexName: result.index_name,
404
+ documentCount: result.document_count
405
+ };
406
+ } else {
407
+ return {
408
+ success: false,
409
+ error: result.error || 'Index building failed'
410
+ };
411
+ }
412
+
413
+ } catch (error) {
414
+ return {
415
+ success: false,
416
+ error: error instanceof Error ? error.message : String(error)
417
+ };
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Search vector index using Modal
423
+ */
424
+ async searchVectorIndex(
425
+ query: string,
426
+ indexName = 'main_index',
427
+ maxResults = 10
428
+ ): Promise<{
429
+ success: boolean;
430
+ results?: Array<{
431
+ id: string;
432
+ title: string;
433
+ content: string;
434
+ source: string;
435
+ relevanceScore: number;
436
+ rank: number;
437
+ snippet: string;
438
+ }>;
439
+ error?: string;
440
+ }> {
441
+ try {
442
+ const result = await modalClient.vectorSearch(query, indexName, maxResults);
443
+
444
+ if (result.status === 'completed') {
445
+ return {
446
+ success: true,
447
+ results: result.results
448
+ };
449
+ } else {
450
+ return {
451
+ success: false,
452
+ error: result.error || 'Vector search failed'
453
+ };
454
+ }
455
+
456
+ } catch (error) {
457
+ return {
458
+ success: false,
459
+ error: error instanceof Error ? error.message : String(error)
460
+ };
461
+ }
462
+ }
463
+ }
464
+
465
+ export const documentProcessor = DocumentProcessor.getInstance();
server/document-routes.ts ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { upload, validateUpload, FileProcessor } from './file-upload';
3
+ import { documentProcessor } from './document-processor';
4
+ import { storage } from './storage';
5
+ import { fileUploadSchema, documentProcessingSchema, batchProcessingSchema } from '@shared/schema';
6
+ import path from 'path';
7
+
8
+ const router = Router();
9
+
10
+ /**
11
+ * Upload documents (multiple files supported)
12
+ */
13
+ router.post('/upload', upload.array('files', 10), validateUpload, async (req, res) => {
14
+ try {
15
+ const files = req.files as Express.Multer.File[];
16
+ const uploadedDocuments = [];
17
+
18
+ for (const file of files) {
19
+ // Extract title from filename or use provided title
20
+ const title = req.body.title || path.basename(file.originalname, path.extname(file.originalname));
21
+ const source = req.body.source || `Uploaded file: ${file.originalname}`;
22
+
23
+ // Determine source type based on MIME type
24
+ let sourceType = 'document';
25
+ if (FileProcessor.isPdfFile(file.mimetype)) {
26
+ sourceType = 'pdf';
27
+ } else if (FileProcessor.isImageFile(file.mimetype)) {
28
+ sourceType = 'image';
29
+ } else if (file.mimetype.includes('text') || file.mimetype.includes('json')) {
30
+ sourceType = 'text';
31
+ }
32
+
33
+ // Read text content for text files
34
+ let content = 'Processing...';
35
+ if (FileProcessor.isTextFile(file.mimetype)) {
36
+ try {
37
+ content = await FileProcessor.readTextFile(file.path);
38
+ } catch (error) {
39
+ console.warn(`Failed to read text file ${file.originalname}:`, error);
40
+ content = 'Failed to read file content';
41
+ }
42
+ }
43
+
44
+ // Create document record
45
+ const document = await storage.createDocument({
46
+ title,
47
+ content,
48
+ source,
49
+ sourceType,
50
+ url: null,
51
+ metadata: {
52
+ originalName: file.originalname,
53
+ uploadedAt: new Date().toISOString(),
54
+ mimeType: file.mimetype,
55
+ size: file.size
56
+ },
57
+ embedding: null,
58
+ filePath: file.path,
59
+ fileName: file.originalname,
60
+ fileSize: file.size,
61
+ mimeType: file.mimetype,
62
+ processingStatus: FileProcessor.requiresOCR(file.mimetype) ? 'pending' : 'completed'
63
+ } as any);
64
+
65
+ uploadedDocuments.push(document);
66
+ }
67
+
68
+ res.status(201).json({
69
+ success: true,
70
+ message: `Successfully uploaded ${uploadedDocuments.length} document(s)`,
71
+ documents: uploadedDocuments
72
+ });
73
+
74
+ } catch (error) {
75
+ console.error('File upload error:', error);
76
+ res.status(500).json({
77
+ success: false,
78
+ error: 'File upload failed',
79
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
80
+ });
81
+ }
82
+ });
83
+
84
+ /**
85
+ * Process a single document
86
+ */
87
+ router.post('/process/:id', async (req, res) => {
88
+ try {
89
+ const documentId = parseInt(req.params.id);
90
+ const { operations = ['extract_text'], indexName } = documentProcessingSchema.parse(req.body);
91
+
92
+ const document = await storage.getDocument(documentId);
93
+ if (!document) {
94
+ return res.status(404).json({
95
+ success: false,
96
+ error: 'Document not found'
97
+ });
98
+ }
99
+
100
+ // Update status to processing
101
+ await storage.updateDocument(documentId, {
102
+ processingStatus: 'processing'
103
+ } as any);
104
+
105
+ // Process the document
106
+ const result = await documentProcessor.processDocument(document, operations);
107
+
108
+ if (result.success) {
109
+ // Update document with results
110
+ const updateData: any = {
111
+ processingStatus: 'completed',
112
+ processedAt: new Date()
113
+ };
114
+
115
+ if (result.extractedText && result.extractedText !== document.content) {
116
+ updateData.content = result.extractedText;
117
+ }
118
+
119
+ if (result.embeddings) {
120
+ updateData.embedding = JSON.stringify(result.embeddings);
121
+ }
122
+
123
+ if (result.modalTaskId) {
124
+ updateData.modalTaskId = result.modalTaskId;
125
+ }
126
+
127
+ const updatedDocument = await storage.updateDocument(documentId, updateData);
128
+
129
+ res.json({
130
+ success: true,
131
+ message: 'Document processed successfully',
132
+ document: updatedDocument,
133
+ processingTime: result.processingTime
134
+ });
135
+
136
+ } else {
137
+ // Update status to failed
138
+ await storage.updateDocument(documentId, {
139
+ processingStatus: 'failed'
140
+ } as any);
141
+
142
+ res.status(500).json({
143
+ success: false,
144
+ error: 'Document processing failed',
145
+ message: result.error,
146
+ processingTime: result.processingTime
147
+ });
148
+ }
149
+
150
+ } catch (error) {
151
+ console.error('Document processing error:', error);
152
+ res.status(500).json({
153
+ success: false,
154
+ error: 'Processing request failed',
155
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
156
+ });
157
+ }
158
+ });
159
+
160
+ /**
161
+ * Batch process multiple documents
162
+ */
163
+ router.post('/process/batch', async (req, res) => {
164
+ try {
165
+ const { documentIds, operations = ['extract_text'], indexName } = batchProcessingSchema.parse(req.body);
166
+
167
+ // Fetch all documents
168
+ const documents = await Promise.all(
169
+ documentIds.map(id => storage.getDocument(id))
170
+ );
171
+
172
+ const validDocuments = documents.filter(doc => doc !== undefined) as any[];
173
+
174
+ if (validDocuments.length === 0) {
175
+ return res.status(404).json({
176
+ success: false,
177
+ error: 'No valid documents found'
178
+ });
179
+ }
180
+
181
+ // Update all documents to processing status
182
+ await Promise.all(
183
+ validDocuments.map(doc =>
184
+ storage.updateDocument(doc.id, { processingStatus: 'processing' } as any)
185
+ )
186
+ );
187
+
188
+ // Process documents in batch
189
+ const batchResult = await documentProcessor.batchProcessDocuments(validDocuments, operations);
190
+
191
+ // Update documents with results
192
+ const updatePromises = batchResult.results.map(async (result) => {
193
+ const updateData: any = {
194
+ processingStatus: result.success ? 'completed' : 'failed',
195
+ processedAt: new Date()
196
+ };
197
+
198
+ if (result.success) {
199
+ if (result.extractedText) {
200
+ updateData.content = result.extractedText;
201
+ }
202
+ if (result.embeddings) {
203
+ updateData.embedding = JSON.stringify(result.embeddings);
204
+ }
205
+ }
206
+
207
+ return storage.updateDocument(result.documentId, updateData);
208
+ });
209
+
210
+ await Promise.all(updatePromises);
211
+
212
+ res.json({
213
+ success: true,
214
+ message: `Batch processing completed: ${batchResult.processedCount} successful, ${batchResult.failedCount} failed`,
215
+ processedCount: batchResult.processedCount,
216
+ failedCount: batchResult.failedCount,
217
+ results: batchResult.results,
218
+ totalProcessingTime: batchResult.totalProcessingTime
219
+ });
220
+
221
+ } catch (error) {
222
+ console.error('Batch processing error:', error);
223
+ res.status(500).json({
224
+ success: false,
225
+ error: 'Batch processing failed',
226
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
227
+ });
228
+ }
229
+ });
230
+
231
+ /**
232
+ * Build vector index from documents
233
+ */
234
+ router.post('/index/build', async (req, res) => {
235
+ try {
236
+ const { documentIds, indexName = 'main_index' } = req.body;
237
+
238
+ let documents;
239
+ if (documentIds && Array.isArray(documentIds)) {
240
+ // Build index from specific documents
241
+ const fetchedDocs = await Promise.all(
242
+ documentIds.map((id: number) => storage.getDocument(id))
243
+ );
244
+ documents = fetchedDocs.filter(doc => doc !== undefined) as any[];
245
+ } else {
246
+ // Build index from all completed documents
247
+ documents = await storage.getDocuments(1000, 0);
248
+ documents = documents.filter(doc => doc.processingStatus === 'completed');
249
+ }
250
+
251
+ if (documents.length === 0) {
252
+ return res.status(400).json({
253
+ success: false,
254
+ error: 'No processed documents available for indexing'
255
+ });
256
+ }
257
+
258
+ const result = await documentProcessor.buildVectorIndex(documents, indexName);
259
+
260
+ if (result.success) {
261
+ res.json({
262
+ success: true,
263
+ message: 'Vector index built successfully',
264
+ indexName: result.indexName,
265
+ documentCount: result.documentCount
266
+ });
267
+ } else {
268
+ res.status(500).json({
269
+ success: false,
270
+ error: 'Index building failed',
271
+ message: result.error
272
+ });
273
+ }
274
+
275
+ } catch (error) {
276
+ console.error('Index building error:', error);
277
+ res.status(500).json({
278
+ success: false,
279
+ error: 'Index building request failed',
280
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
281
+ });
282
+ }
283
+ });
284
+
285
+ /**
286
+ * Search vector index
287
+ */
288
+ router.post('/search/vector', async (req, res) => {
289
+ try {
290
+ const { query, indexName = 'main_index', maxResults = 10 } = req.body;
291
+
292
+ if (!query || typeof query !== 'string') {
293
+ return res.status(400).json({
294
+ success: false,
295
+ error: 'Query parameter is required and must be a string'
296
+ });
297
+ }
298
+
299
+ const result = await documentProcessor.searchVectorIndex(query, indexName, maxResults);
300
+
301
+ if (result.success) {
302
+ res.json({
303
+ success: true,
304
+ query,
305
+ indexName,
306
+ results: result.results,
307
+ totalFound: result.results?.length || 0
308
+ });
309
+ } else {
310
+ res.status(500).json({
311
+ success: false,
312
+ error: 'Vector search failed',
313
+ message: result.error
314
+ });
315
+ }
316
+
317
+ } catch (error) {
318
+ console.error('Vector search error:', error);
319
+ res.status(500).json({
320
+ success: false,
321
+ error: 'Vector search request failed',
322
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
323
+ });
324
+ }
325
+ });
326
+
327
+ /**
328
+ * Get document processing status
329
+ */
330
+ router.get('/status/:id', async (req, res) => {
331
+ try {
332
+ const documentId = parseInt(req.params.id);
333
+ const document = await storage.getDocument(documentId);
334
+
335
+ if (!document) {
336
+ return res.status(404).json({
337
+ success: false,
338
+ error: 'Document not found'
339
+ });
340
+ }
341
+
342
+ res.json({
343
+ success: true,
344
+ document: {
345
+ id: document.id,
346
+ title: document.title,
347
+ processingStatus: (document as any).processingStatus,
348
+ modalTaskId: (document as any).modalTaskId,
349
+ createdAt: document.createdAt,
350
+ processedAt: (document as any).processedAt,
351
+ fileSize: (document as any).fileSize,
352
+ mimeType: (document as any).mimeType
353
+ }
354
+ });
355
+
356
+ } catch (error) {
357
+ console.error('Status check error:', error);
358
+ res.status(500).json({
359
+ success: false,
360
+ error: 'Status check failed',
361
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
362
+ });
363
+ }
364
+ });
365
+
366
+ /**
367
+ * Get all documents with filtering
368
+ */
369
+ router.get('/list', async (req, res) => {
370
+ try {
371
+ const {
372
+ limit = 50,
373
+ offset = 0,
374
+ sourceType,
375
+ processingStatus
376
+ } = req.query;
377
+
378
+ let documents;
379
+
380
+ if (sourceType) {
381
+ documents = await storage.getDocumentsBySourceType(sourceType as string);
382
+ } else if (processingStatus && 'getDocumentsByProcessingStatus' in storage) {
383
+ documents = await (storage as any).getDocumentsByProcessingStatus(processingStatus as string);
384
+ } else {
385
+ documents = await storage.getDocuments(Number(limit), Number(offset));
386
+ }
387
+
388
+ res.json({
389
+ success: true,
390
+ documents,
391
+ totalCount: documents.length
392
+ });
393
+
394
+ } catch (error) {
395
+ console.error('Document list error:', error);
396
+ res.status(500).json({
397
+ success: false,
398
+ error: 'Failed to retrieve documents',
399
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
400
+ });
401
+ }
402
+ });
403
+
404
+ /**
405
+ * Delete a document and its file
406
+ */
407
+ router.delete('/:id', async (req, res) => {
408
+ try {
409
+ const documentId = parseInt(req.params.id);
410
+ const document = await storage.getDocument(documentId);
411
+
412
+ if (!document) {
413
+ return res.status(404).json({
414
+ success: false,
415
+ error: 'Document not found'
416
+ });
417
+ }
418
+
419
+ // Delete file if it exists
420
+ if ((document as any).filePath) {
421
+ await FileProcessor.deleteFile((document as any).filePath);
422
+ }
423
+
424
+ // Delete document record
425
+ const deleted = await storage.deleteDocument(documentId);
426
+
427
+ if (deleted) {
428
+ res.json({
429
+ success: true,
430
+ message: 'Document deleted successfully'
431
+ });
432
+ } else {
433
+ res.status(500).json({
434
+ success: false,
435
+ error: 'Failed to delete document'
436
+ });
437
+ }
438
+
439
+ } catch (error) {
440
+ console.error('Document deletion error:', error);
441
+ res.status(500).json({
442
+ success: false,
443
+ error: 'Document deletion failed',
444
+ message: error instanceof Error ? error.message : 'Unknown error occurred'
445
+ });
446
+ }
447
+ });
448
+
449
+ export default router;
server/file-upload.ts ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import multer from 'multer';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import crypto from 'crypto';
5
+ import { Request } from 'express';
6
+
7
+ // Ensure uploads directory exists
8
+ const uploadsDir = path.join(process.cwd(), 'uploads');
9
+ if (!fs.existsSync(uploadsDir)) {
10
+ fs.mkdirSync(uploadsDir, { recursive: true });
11
+ }
12
+
13
+ // Configure multer for file uploads
14
+ const storage = multer.diskStorage({
15
+ destination: (req, file, cb) => {
16
+ // Create subdirectories by date for organization
17
+ const dateDir = new Date().toISOString().split('T')[0];
18
+ const fullPath = path.join(uploadsDir, dateDir);
19
+
20
+ if (!fs.existsSync(fullPath)) {
21
+ fs.mkdirSync(fullPath, { recursive: true });
22
+ }
23
+
24
+ cb(null, fullPath);
25
+ },
26
+ filename: (req, file, cb) => {
27
+ // Generate unique filename with timestamp and random string
28
+ const timestamp = Date.now();
29
+ const randomString = crypto.randomBytes(8).toString('hex');
30
+ const ext = path.extname(file.originalname);
31
+ const baseName = path.basename(file.originalname, ext);
32
+
33
+ // Sanitize filename
34
+ const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '_');
35
+ const filename = `${timestamp}_${randomString}_${sanitizedBaseName}${ext}`;
36
+
37
+ cb(null, filename);
38
+ }
39
+ });
40
+
41
+ // File filter to accept specific file types
42
+ const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
43
+ const allowedMimeTypes = [
44
+ 'application/pdf',
45
+ 'image/jpeg',
46
+ 'image/jpg',
47
+ 'image/png',
48
+ 'image/gif',
49
+ 'image/webp',
50
+ 'text/plain',
51
+ 'text/markdown',
52
+ 'application/msword',
53
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
54
+ 'application/json'
55
+ ];
56
+
57
+ if (allowedMimeTypes.includes(file.mimetype)) {
58
+ cb(null, true);
59
+ } else {
60
+ cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed types: PDF, images (JPEG, PNG, GIF, WebP), text files, Word documents, JSON`));
61
+ }
62
+ };
63
+
64
+ // Configure multer with size limits
65
+ export const upload = multer({
66
+ storage,
67
+ fileFilter,
68
+ limits: {
69
+ fileSize: 50 * 1024 * 1024, // 50MB limit
70
+ files: 10 // Maximum 10 files per upload
71
+ }
72
+ });
73
+
74
+ // File processing utilities
75
+ export class FileProcessor {
76
+ static async getFileInfo(filePath: string): Promise<{
77
+ size: number;
78
+ mimeType: string;
79
+ exists: boolean;
80
+ }> {
81
+ try {
82
+ const stats = await fs.promises.stat(filePath);
83
+ return {
84
+ size: stats.size,
85
+ mimeType: await this.getMimeType(filePath),
86
+ exists: true
87
+ };
88
+ } catch (error) {
89
+ return {
90
+ size: 0,
91
+ mimeType: '',
92
+ exists: false
93
+ };
94
+ }
95
+ }
96
+
97
+ static async getMimeType(filePath: string): Promise<string> {
98
+ const ext = path.extname(filePath).toLowerCase();
99
+ const mimeTypes: Record<string, string> = {
100
+ '.pdf': 'application/pdf',
101
+ '.jpg': 'image/jpeg',
102
+ '.jpeg': 'image/jpeg',
103
+ '.png': 'image/png',
104
+ '.gif': 'image/gif',
105
+ '.webp': 'image/webp',
106
+ '.txt': 'text/plain',
107
+ '.md': 'text/markdown',
108
+ '.doc': 'application/msword',
109
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
110
+ '.json': 'application/json'
111
+ };
112
+
113
+ return mimeTypes[ext] || 'application/octet-stream';
114
+ }
115
+
116
+ static async readTextFile(filePath: string): Promise<string> {
117
+ try {
118
+ const content = await fs.promises.readFile(filePath, 'utf-8');
119
+ return content;
120
+ } catch (error) {
121
+ throw new Error(`Failed to read text file: ${error}`);
122
+ }
123
+ }
124
+
125
+ static async deleteFile(filePath: string): Promise<boolean> {
126
+ try {
127
+ await fs.promises.unlink(filePath);
128
+ return true;
129
+ } catch (error) {
130
+ console.error(`Failed to delete file ${filePath}:`, error);
131
+ return false;
132
+ }
133
+ }
134
+
135
+ static isTextFile(mimeType: string): boolean {
136
+ return mimeType.startsWith('text/') ||
137
+ mimeType === 'application/json' ||
138
+ mimeType.includes('document');
139
+ }
140
+
141
+ static isImageFile(mimeType: string): boolean {
142
+ return mimeType.startsWith('image/');
143
+ }
144
+
145
+ static isPdfFile(mimeType: string): boolean {
146
+ return mimeType === 'application/pdf';
147
+ }
148
+
149
+ static requiresOCR(mimeType: string): boolean {
150
+ return this.isImageFile(mimeType) || this.isPdfFile(mimeType);
151
+ }
152
+ }
153
+
154
+ // Upload validation middleware
155
+ export const validateUpload = (req: Request, res: any, next: any) => {
156
+ if (!req.files || (Array.isArray(req.files) && req.files.length === 0)) {
157
+ return res.status(400).json({
158
+ error: 'No files uploaded',
159
+ message: 'Please select at least one file to upload'
160
+ });
161
+ }
162
+
163
+ next();
164
+ };
165
+
166
+ export default upload;
server/routes.ts CHANGED
@@ -6,6 +6,7 @@ import { searchRequestSchema } from "@shared/schema";
6
  import { smartIngestionService } from "./smart-ingestion";
7
  import { nebiusClient } from "./nebius-client";
8
  import { modalClient } from "./modal-client";
 
9
 
10
  interface GitHubRepo {
11
  id: number;
@@ -1139,6 +1140,9 @@ Provide a brief, engaging explanation (2-3 sentences) that would be pleasant to
1139
  }
1140
  });
1141
 
 
 
 
1142
  const httpServer = createServer(app);
1143
  return httpServer;
1144
  }
 
6
  import { smartIngestionService } from "./smart-ingestion";
7
  import { nebiusClient } from "./nebius-client";
8
  import { modalClient } from "./modal-client";
9
+ import documentRoutes from "./document-routes";
10
 
11
  interface GitHubRepo {
12
  id: number;
 
1140
  }
1141
  });
1142
 
1143
+ // Register document routes
1144
+ app.use("/api/documents", documentRoutes);
1145
+
1146
  const httpServer = createServer(app);
1147
  return httpServer;
1148
  }
server/sqlite-storage.ts ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import {
5
+ type Document,
6
+ type InsertDocument,
7
+ type SearchQuery,
8
+ type InsertSearchQuery,
9
+ type SearchResult,
10
+ type InsertSearchResult,
11
+ type Citation,
12
+ type InsertCitation,
13
+ type SearchRequest,
14
+ type SearchResponse,
15
+ type DocumentWithContext
16
+ } from "@shared/schema";
17
+ import { IStorage } from './storage';
18
+
19
+ export class SQLiteStorage implements IStorage {
20
+ private db: Database.Database;
21
+
22
+ constructor(dbPath = './data/knowledgebridge.db') {
23
+ // Ensure data directory exists
24
+ const dir = path.dirname(dbPath);
25
+ if (!fs.existsSync(dir)) {
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ }
28
+
29
+ this.db = new Database(dbPath);
30
+ this.initializeTables();
31
+ }
32
+
33
+ private initializeTables() {
34
+ // Enable foreign keys
35
+ this.db.pragma('foreign_keys = ON');
36
+
37
+ // Create documents table
38
+ this.db.exec(`
39
+ CREATE TABLE IF NOT EXISTS documents (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ title TEXT NOT NULL,
42
+ content TEXT NOT NULL,
43
+ source TEXT NOT NULL,
44
+ source_type TEXT NOT NULL,
45
+ url TEXT,
46
+ metadata TEXT, -- JSON string
47
+ embedding TEXT, -- JSON string
48
+ file_path TEXT,
49
+ file_name TEXT,
50
+ file_size INTEGER,
51
+ mime_type TEXT,
52
+ processing_status TEXT NOT NULL DEFAULT 'pending',
53
+ modal_task_id TEXT,
54
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
55
+ processed_at DATETIME
56
+ )
57
+ `);
58
+
59
+ // Create search_queries table
60
+ this.db.exec(`
61
+ CREATE TABLE IF NOT EXISTS search_queries (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ query TEXT NOT NULL,
64
+ search_type TEXT NOT NULL DEFAULT 'semantic',
65
+ filters TEXT, -- JSON string
66
+ results_count INTEGER DEFAULT 0,
67
+ search_time REAL,
68
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
69
+ )
70
+ `);
71
+
72
+ // Create search_results table
73
+ this.db.exec(`
74
+ CREATE TABLE IF NOT EXISTS search_results (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ query_id INTEGER NOT NULL,
77
+ document_id INTEGER NOT NULL,
78
+ relevance_score REAL NOT NULL,
79
+ snippet TEXT NOT NULL,
80
+ rank INTEGER NOT NULL,
81
+ FOREIGN KEY (query_id) REFERENCES search_queries(id),
82
+ FOREIGN KEY (document_id) REFERENCES documents(id)
83
+ )
84
+ `);
85
+
86
+ // Create citations table
87
+ this.db.exec(`
88
+ CREATE TABLE IF NOT EXISTS citations (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ document_id INTEGER NOT NULL,
91
+ citation_text TEXT NOT NULL,
92
+ page_number INTEGER,
93
+ section TEXT,
94
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
95
+ FOREIGN KEY (document_id) REFERENCES documents(id)
96
+ )
97
+ `);
98
+
99
+ // Create indexes for better performance
100
+ this.db.exec(`
101
+ CREATE INDEX IF NOT EXISTS idx_documents_source_type ON documents(source_type);
102
+ CREATE INDEX IF NOT EXISTS idx_documents_processing_status ON documents(processing_status);
103
+ CREATE INDEX IF NOT EXISTS idx_search_results_query_id ON search_results(query_id);
104
+ CREATE INDEX IF NOT EXISTS idx_search_results_document_id ON search_results(document_id);
105
+ CREATE INDEX IF NOT EXISTS idx_citations_document_id ON citations(document_id);
106
+ `);
107
+ }
108
+
109
+ async getDocument(id: number): Promise<Document | undefined> {
110
+ const stmt = this.db.prepare('SELECT * FROM documents WHERE id = ?');
111
+ const row = stmt.get(id) as any;
112
+ return row ? this.mapDocumentRow(row) : undefined;
113
+ }
114
+
115
+ async getDocuments(limit = 50, offset = 0): Promise<Document[]> {
116
+ const stmt = this.db.prepare('SELECT * FROM documents ORDER BY created_at DESC LIMIT ? OFFSET ?');
117
+ const rows = stmt.all(limit, offset) as any[];
118
+ return rows.map(row => this.mapDocumentRow(row));
119
+ }
120
+
121
+ async createDocument(insertDocument: InsertDocument): Promise<Document> {
122
+ const stmt = this.db.prepare(`
123
+ INSERT INTO documents (
124
+ title, content, source, source_type, url, metadata, embedding,
125
+ file_path, file_name, file_size, mime_type, processing_status, modal_task_id
126
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
127
+ `);
128
+
129
+ const result = stmt.run(
130
+ insertDocument.title,
131
+ insertDocument.content,
132
+ insertDocument.source,
133
+ insertDocument.sourceType,
134
+ insertDocument.url || null,
135
+ insertDocument.metadata ? JSON.stringify(insertDocument.metadata) : null,
136
+ insertDocument.embedding || null,
137
+ (insertDocument as any).filePath || null,
138
+ (insertDocument as any).fileName || null,
139
+ (insertDocument as any).fileSize || null,
140
+ (insertDocument as any).mimeType || null,
141
+ (insertDocument as any).processingStatus || 'pending',
142
+ (insertDocument as any).modalTaskId || null
143
+ );
144
+
145
+ const created = await this.getDocument(result.lastInsertRowid as number);
146
+ if (!created) throw new Error('Failed to create document');
147
+ return created;
148
+ }
149
+
150
+ async updateDocument(id: number, updateData: Partial<InsertDocument & { processingStatus?: string; modalTaskId?: string; processedAt?: Date }>): Promise<Document | undefined> {
151
+ const existing = await this.getDocument(id);
152
+ if (!existing) return undefined;
153
+
154
+ const fields: string[] = [];
155
+ const values: any[] = [];
156
+
157
+ Object.entries(updateData).forEach(([key, value]) => {
158
+ if (value !== undefined) {
159
+ switch (key) {
160
+ case 'sourceType':
161
+ fields.push('source_type = ?');
162
+ break;
163
+ case 'processingStatus':
164
+ fields.push('processing_status = ?');
165
+ break;
166
+ case 'modalTaskId':
167
+ fields.push('modal_task_id = ?');
168
+ break;
169
+ case 'filePath':
170
+ fields.push('file_path = ?');
171
+ break;
172
+ case 'fileName':
173
+ fields.push('file_name = ?');
174
+ break;
175
+ case 'fileSize':
176
+ fields.push('file_size = ?');
177
+ break;
178
+ case 'mimeType':
179
+ fields.push('mime_type = ?');
180
+ break;
181
+ case 'processedAt':
182
+ fields.push('processed_at = ?');
183
+ value = value instanceof Date ? value.toISOString() : value;
184
+ break;
185
+ case 'metadata':
186
+ fields.push('metadata = ?');
187
+ value = value ? JSON.stringify(value) : null;
188
+ break;
189
+ default:
190
+ fields.push(`${key} = ?`);
191
+ }
192
+ values.push(value);
193
+ }
194
+ });
195
+
196
+ if (fields.length === 0) return existing;
197
+
198
+ values.push(id);
199
+ const stmt = this.db.prepare(`UPDATE documents SET ${fields.join(', ')} WHERE id = ?`);
200
+ stmt.run(...values);
201
+
202
+ return await this.getDocument(id);
203
+ }
204
+
205
+ async deleteDocument(id: number): Promise<boolean> {
206
+ const stmt = this.db.prepare('DELETE FROM documents WHERE id = ?');
207
+ const result = stmt.run(id);
208
+ return result.changes > 0;
209
+ }
210
+
211
+ async searchDocuments(request: SearchRequest): Promise<SearchResponse> {
212
+ const startTime = Date.now();
213
+
214
+ let sql = `
215
+ SELECT * FROM documents
216
+ WHERE (title LIKE ? OR content LIKE ?)
217
+ `;
218
+ const params: any[] = [`%${request.query}%`, `%${request.query}%`];
219
+
220
+ // Add source type filter if specified
221
+ if (request.filters?.sourceTypes?.length) {
222
+ const placeholders = request.filters.sourceTypes.map(() => '?').join(',');
223
+ sql += ` AND source_type IN (${placeholders})`;
224
+ params.push(...request.filters.sourceTypes);
225
+ }
226
+
227
+ sql += ` ORDER BY
228
+ CASE
229
+ WHEN title LIKE ? THEN 1
230
+ WHEN content LIKE ? THEN 2
231
+ ELSE 3
232
+ END,
233
+ created_at DESC
234
+ LIMIT ? OFFSET ?
235
+ `;
236
+
237
+ params.push(`%${request.query}%`, `%${request.query}%`, request.limit, request.offset);
238
+
239
+ const stmt = this.db.prepare(sql);
240
+ const rows = stmt.all(...params) as any[];
241
+
242
+ const results = rows.map((row, index) => {
243
+ const doc = this.mapDocumentRow(row);
244
+ return {
245
+ ...doc,
246
+ relevanceScore: this.calculateRelevanceScore(doc, request.query),
247
+ snippet: this.extractSnippet(doc.content, request.query),
248
+ rank: index + 1
249
+ };
250
+ });
251
+
252
+ const searchTime = (Date.now() - startTime) / 1000;
253
+
254
+ // Save search query
255
+ const searchQuery = await this.createSearchQuery({
256
+ query: request.query,
257
+ searchType: request.searchType,
258
+ filters: request.filters,
259
+ resultsCount: results.length,
260
+ searchTime
261
+ });
262
+
263
+ // Save search results
264
+ for (const doc of results) {
265
+ await this.createSearchResult({
266
+ queryId: searchQuery.id,
267
+ documentId: doc.id,
268
+ relevanceScore: doc.relevanceScore,
269
+ snippet: doc.snippet,
270
+ rank: doc.rank
271
+ });
272
+ }
273
+
274
+ return {
275
+ results,
276
+ totalCount: results.length,
277
+ searchTime,
278
+ query: request.query,
279
+ queryId: searchQuery.id
280
+ };
281
+ }
282
+
283
+ private calculateRelevanceScore(doc: Document, query: string): number {
284
+ const queryLower = query.toLowerCase();
285
+ const titleLower = doc.title.toLowerCase();
286
+ const contentLower = doc.content.toLowerCase();
287
+
288
+ let score = 0;
289
+
290
+ // Exact title match gets highest score
291
+ if (titleLower === queryLower) score += 1.0;
292
+ else if (titleLower.includes(queryLower)) score += 0.8;
293
+
294
+ // Content matches
295
+ if (contentLower.includes(queryLower)) score += 0.3;
296
+
297
+ // Word-by-word scoring
298
+ const queryWords = queryLower.split(' ');
299
+ queryWords.forEach(word => {
300
+ if (titleLower.includes(word)) score += 0.2;
301
+ if (contentLower.includes(word)) score += 0.1;
302
+ });
303
+
304
+ return Math.min(score, 1.0);
305
+ }
306
+
307
+ private extractSnippet(content: string, query: string, maxLength = 200): string {
308
+ const queryLower = query.toLowerCase();
309
+ const contentLower = content.toLowerCase();
310
+
311
+ const index = contentLower.indexOf(queryLower);
312
+ if (index === -1) {
313
+ return content.substring(0, maxLength) + (content.length > maxLength ? '...' : '');
314
+ }
315
+
316
+ const start = Math.max(0, index - 50);
317
+ const end = Math.min(content.length, index + queryLower.length + 150);
318
+
319
+ let snippet = content.substring(start, end);
320
+ if (start > 0) snippet = '...' + snippet;
321
+ if (end < content.length) snippet = snippet + '...';
322
+
323
+ return snippet;
324
+ }
325
+
326
+ async getDocumentsBySourceType(sourceType: string): Promise<Document[]> {
327
+ const stmt = this.db.prepare('SELECT * FROM documents WHERE source_type = ? ORDER BY created_at DESC');
328
+ const rows = stmt.all(sourceType) as any[];
329
+ return rows.map(row => this.mapDocumentRow(row));
330
+ }
331
+
332
+ async getDocumentsByProcessingStatus(status: string): Promise<Document[]> {
333
+ const stmt = this.db.prepare('SELECT * FROM documents WHERE processing_status = ? ORDER BY created_at DESC');
334
+ const rows = stmt.all(status) as any[];
335
+ return rows.map(row => this.mapDocumentRow(row));
336
+ }
337
+
338
+ async createSearchQuery(insertQuery: InsertSearchQuery): Promise<SearchQuery> {
339
+ const stmt = this.db.prepare(`
340
+ INSERT INTO search_queries (query, search_type, filters, results_count, search_time)
341
+ VALUES (?, ?, ?, ?, ?)
342
+ `);
343
+
344
+ const result = stmt.run(
345
+ insertQuery.query,
346
+ insertQuery.searchType || 'semantic',
347
+ insertQuery.filters ? JSON.stringify(insertQuery.filters) : null,
348
+ insertQuery.resultsCount || null,
349
+ insertQuery.searchTime || null
350
+ );
351
+
352
+ const created = this.db.prepare('SELECT * FROM search_queries WHERE id = ?').get(result.lastInsertRowid) as any;
353
+ return this.mapSearchQueryRow(created);
354
+ }
355
+
356
+ async getSearchQueries(limit = 50): Promise<SearchQuery[]> {
357
+ const stmt = this.db.prepare('SELECT * FROM search_queries ORDER BY created_at DESC LIMIT ?');
358
+ const rows = stmt.all(limit) as any[];
359
+ return rows.map(row => this.mapSearchQueryRow(row));
360
+ }
361
+
362
+ async createSearchResult(insertResult: InsertSearchResult): Promise<SearchResult> {
363
+ const stmt = this.db.prepare(`
364
+ INSERT INTO search_results (query_id, document_id, relevance_score, snippet, rank)
365
+ VALUES (?, ?, ?, ?, ?)
366
+ `);
367
+
368
+ const result = stmt.run(
369
+ insertResult.queryId,
370
+ insertResult.documentId,
371
+ insertResult.relevanceScore,
372
+ insertResult.snippet,
373
+ insertResult.rank
374
+ );
375
+
376
+ const created = this.db.prepare('SELECT * FROM search_results WHERE id = ?').get(result.lastInsertRowid) as any;
377
+ return this.mapSearchResultRow(created);
378
+ }
379
+
380
+ async getSearchResults(queryId: number): Promise<SearchResult[]> {
381
+ const stmt = this.db.prepare('SELECT * FROM search_results WHERE query_id = ? ORDER BY rank');
382
+ const rows = stmt.all(queryId) as any[];
383
+ return rows.map(row => this.mapSearchResultRow(row));
384
+ }
385
+
386
+ async createCitation(insertCitation: InsertCitation): Promise<Citation> {
387
+ const stmt = this.db.prepare(`
388
+ INSERT INTO citations (document_id, citation_text, page_number, section)
389
+ VALUES (?, ?, ?, ?)
390
+ `);
391
+
392
+ const result = stmt.run(
393
+ insertCitation.documentId,
394
+ insertCitation.citationText,
395
+ insertCitation.pageNumber || null,
396
+ insertCitation.section || null
397
+ );
398
+
399
+ const created = this.db.prepare('SELECT * FROM citations WHERE id = ?').get(result.lastInsertRowid) as any;
400
+ return this.mapCitationRow(created);
401
+ }
402
+
403
+ async getCitationsByDocument(documentId: number): Promise<Citation[]> {
404
+ const stmt = this.db.prepare('SELECT * FROM citations WHERE document_id = ? ORDER BY created_at DESC');
405
+ const rows = stmt.all(documentId) as any[];
406
+ return rows.map(row => this.mapCitationRow(row));
407
+ }
408
+
409
+ async deleteCitation(id: number): Promise<boolean> {
410
+ const stmt = this.db.prepare('DELETE FROM citations WHERE id = ?');
411
+ const result = stmt.run(id);
412
+ return result.changes > 0;
413
+ }
414
+
415
+ private mapDocumentRow(row: any): Document {
416
+ return {
417
+ id: row.id,
418
+ title: row.title,
419
+ content: row.content,
420
+ source: row.source,
421
+ sourceType: row.source_type,
422
+ url: row.url,
423
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
424
+ embedding: row.embedding,
425
+ createdAt: new Date(row.created_at),
426
+ filePath: row.file_path,
427
+ fileName: row.file_name,
428
+ fileSize: row.file_size,
429
+ mimeType: row.mime_type,
430
+ processingStatus: row.processing_status,
431
+ modalTaskId: row.modal_task_id,
432
+ processedAt: row.processed_at ? new Date(row.processed_at) : null,
433
+ } as Document;
434
+ }
435
+
436
+ private mapSearchQueryRow(row: any): SearchQuery {
437
+ return {
438
+ id: row.id,
439
+ query: row.query,
440
+ searchType: row.search_type,
441
+ filters: row.filters ? JSON.parse(row.filters) : null,
442
+ resultsCount: row.results_count,
443
+ searchTime: row.search_time,
444
+ createdAt: new Date(row.created_at)
445
+ };
446
+ }
447
+
448
+ private mapSearchResultRow(row: any): SearchResult {
449
+ return {
450
+ id: row.id,
451
+ queryId: row.query_id,
452
+ documentId: row.document_id,
453
+ relevanceScore: row.relevance_score,
454
+ snippet: row.snippet,
455
+ rank: row.rank
456
+ };
457
+ }
458
+
459
+ private mapCitationRow(row: any): Citation {
460
+ return {
461
+ id: row.id,
462
+ documentId: row.document_id,
463
+ citationText: row.citation_text,
464
+ pageNumber: row.page_number,
465
+ section: row.section,
466
+ createdAt: new Date(row.created_at)
467
+ };
468
+ }
469
+
470
+ close() {
471
+ this.db.close();
472
+ }
473
+ }
server/storage.ts CHANGED
@@ -433,4 +433,7 @@ export class MemStorage implements IStorage {
433
  }
434
  }
435
 
436
- export const storage = new MemStorage();
 
 
 
 
433
  }
434
  }
435
 
436
+ import { SQLiteStorage } from './sqlite-storage';
437
+
438
+ // Use SQLite storage in production, keep MemStorage for testing
439
+ export const storage = process.env.NODE_ENV === 'test' ? new MemStorage() : new SQLiteStorage();
shared/schema.ts CHANGED
@@ -7,11 +7,18 @@ export const documents = pgTable("documents", {
7
  title: text("title").notNull(),
8
  content: text("content").notNull(),
9
  source: text("source").notNull(),
10
- sourceType: text("source_type").notNull(), // pdf, web, code, academic
11
  url: text("url"),
12
  metadata: jsonb("metadata"), // author, date, tags, etc.
13
  embedding: text("embedding"), // vector embedding as JSON string
 
 
 
 
 
 
14
  createdAt: timestamp("created_at").defaultNow().notNull(),
 
15
  });
16
 
17
  export const searchQueries = pgTable("search_queries", {
@@ -114,3 +121,32 @@ export interface DocumentWithContext extends Document {
114
  pageNumber?: number;
115
  }>;
116
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  title: text("title").notNull(),
8
  content: text("content").notNull(),
9
  source: text("source").notNull(),
10
+ sourceType: text("source_type").notNull(), // pdf, web, code, academic, image
11
  url: text("url"),
12
  metadata: jsonb("metadata"), // author, date, tags, etc.
13
  embedding: text("embedding"), // vector embedding as JSON string
14
+ filePath: text("file_path"), // local file path for uploaded files
15
+ fileName: text("file_name"), // original file name
16
+ fileSize: integer("file_size"), // file size in bytes
17
+ mimeType: text("mime_type"), // MIME type of uploaded file
18
+ processingStatus: text("processing_status").notNull().default("pending"), // pending, processing, completed, failed
19
+ modalTaskId: text("modal_task_id"), // Modal processing task ID
20
  createdAt: timestamp("created_at").defaultNow().notNull(),
21
+ processedAt: timestamp("processed_at"),
22
  });
23
 
24
  export const searchQueries = pgTable("search_queries", {
 
121
  pageNumber?: number;
122
  }>;
123
  }
124
+
125
+ // File upload schemas
126
+ export const fileUploadSchema = z.object({
127
+ fileName: z.string().min(1),
128
+ fileSize: z.number().min(1),
129
+ mimeType: z.string().min(1),
130
+ title: z.string().optional(),
131
+ source: z.string().optional(),
132
+ });
133
+
134
+ export type FileUpload = z.infer<typeof fileUploadSchema>;
135
+
136
+ // Document processing schemas
137
+ export const documentProcessingSchema = z.object({
138
+ documentId: z.number(),
139
+ operations: z.array(z.enum(["extract_text", "build_index", "generate_embedding"])).default(["extract_text"]),
140
+ indexName: z.string().optional(),
141
+ });
142
+
143
+ export type DocumentProcessing = z.infer<typeof documentProcessingSchema>;
144
+
145
+ // Batch processing schemas
146
+ export const batchProcessingSchema = z.object({
147
+ documentIds: z.array(z.number()).min(1),
148
+ operations: z.array(z.enum(["extract_text", "build_index", "generate_embedding"])).default(["extract_text"]),
149
+ indexName: z.string().optional(),
150
+ });
151
+
152
+ export type BatchProcessing = z.infer<typeof batchProcessingSchema>;