FIXED ISSUES: 1. Download functionality (404 errors): - Added PDF generation to jobQueueService after document processing - PDFs are now generated from summaries and stored in summary_pdf_path - Download endpoint now works correctly 2. Frontend-Backend communication: - Verified Vite proxy configuration is correct (/api -> localhost:5000) - Backend is responding to health checks - API authentication is working 3. Temporary files cleanup: - Removed 50+ temporary debug/test files from backend/ - Cleaned up check-*.js, test-*.js, debug-*.js, fix-*.js files - Removed one-time processing scripts and debug utilities TECHNICAL DETAILS: - Modified jobQueueService.ts to generate PDFs using pdfGenerationService - Added path import for file path handling - PDFs are generated with timestamp in filename for uniqueness - All temporary development files have been removed STATUS: Download functionality should now work. Frontend-backend communication verified.
408 lines
14 KiB
TypeScript
408 lines
14 KiB
TypeScript
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
|
import { useDropzone } from 'react-dropzone';
|
|
import { Upload, FileText, X, CheckCircle, AlertCircle } from 'lucide-react';
|
|
import { cn } from '../utils/cn';
|
|
import { documentService } from '../services/documentService';
|
|
|
|
interface UploadedFile {
|
|
id: string;
|
|
name: string;
|
|
size: number;
|
|
type: string;
|
|
status: 'uploading' | 'uploaded' | 'processing' | 'completed' | 'error';
|
|
progress: number;
|
|
error?: string;
|
|
documentId?: string; // Real document ID from backend
|
|
}
|
|
|
|
interface DocumentUploadProps {
|
|
onUploadComplete?: (fileId: string) => void;
|
|
onUploadError?: (error: string) => void;
|
|
}
|
|
|
|
const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
|
onUploadComplete,
|
|
onUploadError,
|
|
}) => {
|
|
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const abortControllers = useRef<Map<string, AbortController>>(new Map());
|
|
|
|
// Cleanup function to cancel ongoing uploads when component unmounts
|
|
useEffect(() => {
|
|
return () => {
|
|
// Cancel all ongoing uploads when component unmounts
|
|
abortControllers.current.forEach((controller, fileId) => {
|
|
controller.abort();
|
|
console.log(`Cancelled upload for file: ${fileId}`);
|
|
});
|
|
abortControllers.current.clear();
|
|
};
|
|
}, []);
|
|
|
|
// Handle page visibility changes (tab switching, minimizing)
|
|
useEffect(() => {
|
|
const handleVisibilityChange = () => {
|
|
if (document.hidden && isUploading && abortControllers.current.size > 0) {
|
|
console.warn('Page hidden during upload - uploads may be cancelled');
|
|
// Optionally show a notification to the user
|
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
new Notification('Upload in Progress', {
|
|
body: 'Please return to the tab to continue uploads',
|
|
icon: '/favicon.ico',
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
return () => {
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
};
|
|
}, [isUploading]);
|
|
|
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
|
setIsUploading(true);
|
|
|
|
const newFiles: UploadedFile[] = acceptedFiles.map(file => ({
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
name: file.name,
|
|
size: file.size,
|
|
type: file.type,
|
|
status: 'uploading',
|
|
progress: 0,
|
|
}));
|
|
|
|
setUploadedFiles(prev => [...prev, ...newFiles]);
|
|
|
|
// Upload files using the document service
|
|
for (let i = 0; i < acceptedFiles.length; i++) {
|
|
const file = acceptedFiles[i];
|
|
const uploadedFile = newFiles[i];
|
|
|
|
// Create AbortController for this upload
|
|
const abortController = new AbortController();
|
|
abortControllers.current.set(uploadedFile.id, abortController);
|
|
|
|
try {
|
|
// Upload the document with optimized agentic RAG processing (no strategy selection needed)
|
|
const document = await documentService.uploadDocument(
|
|
file,
|
|
(progress) => {
|
|
setUploadedFiles(prev =>
|
|
prev.map(f =>
|
|
f.id === uploadedFile.id
|
|
? { ...f, progress }
|
|
: f
|
|
)
|
|
);
|
|
},
|
|
abortController.signal
|
|
);
|
|
|
|
// Upload completed - update status to "uploaded"
|
|
setUploadedFiles(prev =>
|
|
prev.map(f =>
|
|
f.id === uploadedFile.id
|
|
? {
|
|
...f,
|
|
id: document.id,
|
|
documentId: document.id,
|
|
status: 'uploaded',
|
|
progress: 100
|
|
}
|
|
: f
|
|
)
|
|
);
|
|
|
|
// Call the completion callback with the document ID
|
|
onUploadComplete?.(document.id);
|
|
|
|
// Start monitoring processing progress
|
|
monitorProcessingProgress(document.id, uploadedFile.id);
|
|
|
|
} catch (error) {
|
|
// Check if this was an abort error
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
console.log(`Upload cancelled for file: ${uploadedFile.name}`);
|
|
setUploadedFiles(prev =>
|
|
prev.map(f =>
|
|
f.id === uploadedFile.id
|
|
? { ...f, status: 'error', error: 'Upload cancelled' }
|
|
: f
|
|
)
|
|
);
|
|
} else {
|
|
console.error('Upload failed:', error);
|
|
setUploadedFiles(prev =>
|
|
prev.map(f =>
|
|
f.id === uploadedFile.id
|
|
? { ...f, status: 'error', error: error instanceof Error ? error.message : 'Upload failed' }
|
|
: f
|
|
)
|
|
);
|
|
onUploadError?.(error instanceof Error ? error.message : 'Upload failed');
|
|
}
|
|
} finally {
|
|
// Clean up the abort controller
|
|
abortControllers.current.delete(uploadedFile.id);
|
|
}
|
|
}
|
|
|
|
setIsUploading(false);
|
|
}, [onUploadComplete, onUploadError]);
|
|
|
|
// Monitor processing progress for uploaded documents
|
|
const monitorProcessingProgress = useCallback((documentId: string, fileId: string) => {
|
|
// Guard against undefined or null document IDs
|
|
if (!documentId || documentId === 'undefined' || documentId === 'null') {
|
|
console.warn('Attempted to monitor progress for document with invalid ID:', documentId);
|
|
return;
|
|
}
|
|
|
|
const checkProgress = async () => {
|
|
try {
|
|
const response = await fetch(`/api/documents/${documentId}/progress`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const progress = await response.json();
|
|
|
|
// Update status based on progress
|
|
let newStatus: UploadedFile['status'] = 'uploaded';
|
|
if (progress.status === 'processing' || progress.status === 'extracting_text' || progress.status === 'processing_llm' || progress.status === 'generating_pdf') {
|
|
newStatus = 'processing';
|
|
} else if (progress.status === 'completed') {
|
|
newStatus = 'completed';
|
|
} else if (progress.status === 'error' || progress.status === 'failed') {
|
|
newStatus = 'error';
|
|
}
|
|
|
|
setUploadedFiles(prev =>
|
|
prev.map(f =>
|
|
f.id === fileId
|
|
? {
|
|
...f,
|
|
status: newStatus,
|
|
progress: progress.progress || f.progress
|
|
}
|
|
: f
|
|
)
|
|
);
|
|
|
|
// Stop monitoring if completed or error
|
|
if (newStatus === 'completed' || newStatus === 'error') {
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch processing progress:', error);
|
|
}
|
|
|
|
// Continue monitoring
|
|
setTimeout(checkProgress, 2000);
|
|
};
|
|
|
|
// Start monitoring
|
|
setTimeout(checkProgress, 1000);
|
|
}, []);
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
onDrop,
|
|
accept: {
|
|
'application/pdf': ['.pdf'],
|
|
},
|
|
multiple: true,
|
|
maxSize: 50 * 1024 * 1024, // 50MB
|
|
});
|
|
|
|
const removeFile = (fileId: string) => {
|
|
// Cancel the upload if it's still in progress
|
|
const controller = abortControllers.current.get(fileId);
|
|
if (controller) {
|
|
controller.abort();
|
|
abortControllers.current.delete(fileId);
|
|
}
|
|
|
|
setUploadedFiles(prev => prev.filter(f => f.id !== fileId));
|
|
};
|
|
|
|
const formatFileSize = (bytes: number) => {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
const getStatusIcon = (status: UploadedFile['status']) => {
|
|
switch (status) {
|
|
case 'uploading':
|
|
return <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600" />;
|
|
case 'uploaded':
|
|
return <CheckCircle className="h-4 w-4 text-success-500" />;
|
|
case 'processing':
|
|
return <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-accent-500" />;
|
|
case 'completed':
|
|
return <CheckCircle className="h-4 w-4 text-success-500" />;
|
|
case 'error':
|
|
return <AlertCircle className="h-4 w-4 text-error-500" />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getStatusText = (status: UploadedFile['status'], error?: string) => {
|
|
switch (status) {
|
|
case 'uploading':
|
|
return 'Uploading...';
|
|
case 'uploaded':
|
|
return 'Uploaded ✓';
|
|
case 'processing':
|
|
return 'Processing with Optimized Agentic RAG...';
|
|
case 'completed':
|
|
return 'Completed ✓';
|
|
case 'error':
|
|
return error === 'Upload cancelled' ? 'Cancelled' : 'Error';
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Processing Information */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div className="flex items-center">
|
|
<CheckCircle className="h-5 w-5 text-blue-600 mr-2" />
|
|
<div>
|
|
<h3 className="text-sm font-medium text-blue-800">Optimized Agentic RAG Processing</h3>
|
|
<p className="text-sm text-blue-700 mt-1">
|
|
All documents are automatically processed using our advanced optimized agentic RAG system,
|
|
which includes intelligent chunking, vectorization, and multi-agent analysis for the best results.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upload Area */}
|
|
<div
|
|
{...getRootProps()}
|
|
className={cn(
|
|
'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-200',
|
|
isDragActive
|
|
? 'border-primary-500 bg-primary-50'
|
|
: 'border-gray-300 hover:border-primary-400'
|
|
)}
|
|
>
|
|
<input {...getInputProps()} />
|
|
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
|
<h3 className="text-lg font-medium text-primary-800 mb-2">
|
|
{isDragActive ? 'Drop files here' : 'Upload Documents'}
|
|
</h3>
|
|
<p className="text-sm text-gray-600 mb-4">
|
|
Drag and drop PDF files here, or click to browse
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
Maximum file size: 50MB • Supported format: PDF • Automatic Optimized Agentic RAG Processing
|
|
</p>
|
|
</div>
|
|
|
|
{/* Upload Cancellation Warning */}
|
|
{isUploading && (
|
|
<div className="bg-warning-50 border border-warning-200 rounded-lg p-4">
|
|
<div className="flex items-center">
|
|
<AlertCircle className="h-5 w-5 text-warning-600 mr-2" />
|
|
<div>
|
|
<h4 className="text-sm font-medium text-warning-800">Upload in Progress</h4>
|
|
<p className="text-sm text-warning-700 mt-1">
|
|
Please don't navigate away from this page while files are uploading.
|
|
Once files show "Uploaded ✓", you can safely navigate away - processing will continue in the background.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Upload Complete Success Message */}
|
|
{!isUploading && uploadedFiles.some(f => f.status === 'uploaded') && (
|
|
<div className="bg-success-50 border border-success-200 rounded-lg p-4">
|
|
<div className="flex items-center">
|
|
<CheckCircle className="h-5 w-5 text-success-600 mr-2" />
|
|
<div>
|
|
<h4 className="text-sm font-medium text-success-800">Upload Complete</h4>
|
|
<p className="text-sm text-success-700 mt-1">
|
|
Files have been uploaded successfully! You can now navigate away from this page.
|
|
Processing will continue in the background using Optimized Agentic RAG and you can check the status in the Documents tab.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Uploaded Files List */}
|
|
{uploadedFiles.length > 0 && (
|
|
<div className="space-y-3">
|
|
<h4 className="text-sm font-medium text-primary-800">Uploaded Files</h4>
|
|
<div className="space-y-2">
|
|
{uploadedFiles.map((file) => (
|
|
<div
|
|
key={file.id}
|
|
className="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg"
|
|
>
|
|
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
|
<FileText className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-gray-900 truncate">
|
|
{file.name}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{formatFileSize(file.size)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
{/* Progress Bar */}
|
|
{(file.status === 'uploading' || file.status === 'processing') && (
|
|
<div className="w-24 bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full transition-all duration-300 ${
|
|
file.status === 'uploading' ? 'bg-blue-600' : 'bg-orange-600'
|
|
}`}
|
|
style={{ width: `${file.progress}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Status */}
|
|
<div className="flex items-center space-x-1">
|
|
{getStatusIcon(file.status)}
|
|
<span className="text-xs text-gray-600">
|
|
{getStatusText(file.status, file.error)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Remove Button */}
|
|
<button
|
|
onClick={() => removeFile(file.id)}
|
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
disabled={file.status === 'uploading' || file.status === 'processing'}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DocumentUpload;
|