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>;
|