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 +489 -0
- client/src/components/knowledge-base/vector-search.tsx +344 -0
- client/src/pages/knowledge-base.tsx +15 -2
- data/knowledgebridge.db +0 -0
- package-lock.json +456 -13
- package.json +4 -0
- server/document-processor.ts +465 -0
- server/document-routes.ts +449 -0
- server/file-upload.ts +166 -0
- server/routes.ts +4 -0
- server/sqlite-storage.ts +473 -0
- server/storage.ts +4 -1
- shared/schema.ts +37 -1
@@ -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 |
+
}
|
@@ -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 |
+
}
|
@@ -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-
|
184 |
<TabsTrigger value="search">π AI-Enhanced Search</TabsTrigger>
|
185 |
-
<TabsTrigger value="
|
|
|
|
|
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 />
|
Binary file (45.1 kB). View file
|
|
@@ -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",
|
@@ -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",
|
@@ -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();
|
@@ -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;
|
@@ -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;
|
@@ -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 |
}
|
@@ -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 |
+
}
|
@@ -433,4 +433,7 @@ export class MemStorage implements IStorage {
|
|
433 |
}
|
434 |
}
|
435 |
|
436 |
-
|
|
|
|
|
|
|
|
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();
|
@@ -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>;
|