Files
cim_summary/frontend/src/components/DocumentUpload.tsx
Jon dccfcfaa23 Fix download functionality and clean up temporary files
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.
2025-07-28 21:33:28 -04:00

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;