Files
cim_summary/frontend/src/components/DocumentUpload.tsx
Jon 6057d1d7fd 🔧 Fix authentication and document upload issues
## What was done:
 Fixed Firebase Admin initialization to use default credentials for Firebase Functions
 Updated frontend to use correct Firebase Functions URL (was using Cloud Run URL)
 Added comprehensive debugging to authentication middleware
 Added debugging to file upload middleware and CORS handling
 Added debug buttons to frontend for troubleshooting authentication
 Enhanced error handling and logging throughout the stack

## Current issues:
 Document upload still returns 400 Bad Request despite authentication working
 GET requests work fine (200 OK) but POST upload requests fail
 Frontend authentication is working correctly (valid JWT tokens)
 Backend authentication middleware is working (rejects invalid tokens)
 CORS is configured correctly and allowing requests

## Root cause analysis:
- Authentication is NOT the issue (tokens are valid, GET requests work)
- The problem appears to be in the file upload handling or multer configuration
- Request reaches the server but fails during upload processing
- Need to identify exactly where in the upload pipeline the failure occurs

## TODO next steps:
1. 🔍 Check Firebase Functions logs after next upload attempt to see debugging output
2. 🔍 Verify if request reaches upload middleware (look for '�� Upload middleware called' logs)
3. 🔍 Check if file validation is triggered (look for '🔍 File filter called' logs)
4. 🔍 Identify specific error in upload pipeline (multer, file processing, etc.)
5. 🔍 Test with smaller file or different file type to isolate issue
6. 🔍 Check if issue is with Firebase Functions file size limits or timeout
7. 🔍 Verify multer configuration and file handling in Firebase Functions environment

## Technical details:
- Frontend: https://cim-summarizer.web.app
- Backend: https://us-central1-cim-summarizer.cloudfunctions.net/api
- Authentication: Firebase Auth with JWT tokens (working correctly)
- File upload: Multer with memory storage for immediate GCS upload
- Debug buttons available in production frontend for troubleshooting
2025-07-31 16:18:53 -04:00

455 lines
16 KiB
TypeScript

import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, FileText, X, CheckCircle, AlertCircle, Cloud } from 'lucide-react';
import { cn } from '../utils/cn';
import { documentService, GCSErrorHandler, GCSError } from '../services/documentService';
import { useAuth } from '../contexts/AuthContext';
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
// GCS-specific fields
gcsError?: boolean;
storageType?: 'gcs' | 'local';
gcsUrl?: string;
}
interface DocumentUploadProps {
onUploadComplete?: (fileId: string) => void;
onUploadError?: (error: string) => void;
}
const DocumentUpload: React.FC<DocumentUploadProps> = ({
onUploadComplete,
onUploadError,
}) => {
const { token } = useAuth();
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);
// Handle GCS-specific errors
let errorMessage = 'Upload failed';
let isGCSError = false;
if (GCSErrorHandler.isGCSError(error)) {
errorMessage = GCSErrorHandler.getErrorMessage(error as GCSError);
isGCSError = true;
} else if (error instanceof Error) {
errorMessage = error.message;
}
setUploadedFiles(prev =>
prev.map(f =>
f.id === uploadedFile.id
? {
...f,
status: 'error',
error: errorMessage,
// Add GCS error indicator
...(isGCSError && { gcsError: true })
}
: f
)
);
onUploadError?.(errorMessage);
}
} 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;
}
// Validate UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(documentId)) {
console.warn('Attempted to monitor progress for document with invalid UUID format:', documentId);
return;
}
const checkProgress = async () => {
try {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/documents/${documentId}/progress`, {
headers: {
'Authorization': `Bearer ${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;
}
} else if (response.status === 404) {
// Document not found, stop monitoring
console.warn(`Document ${documentId} not found, stopping progress monitoring`);
return;
} else if (response.status === 401) {
// Unauthorized, stop monitoring
console.warn('Unauthorized access to document progress, stopping monitoring');
return;
}
} catch (error) {
console.error('Failed to fetch processing progress:', error);
// Don't stop monitoring on network errors, just log and continue
}
// Continue monitoring
setTimeout(checkProgress, 2000);
};
// Start monitoring
setTimeout(checkProgress, 1000);
}, [token]);
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, gcsError?: boolean) => {
switch (status) {
case 'uploading':
return 'Uploading to Google Cloud Storage...';
case 'uploaded':
return 'Uploaded to GCS ✓';
case 'processing':
return 'Processing with Optimized Agentic RAG...';
case 'completed':
return 'Completed ✓';
case 'error':
if (error === 'Upload cancelled') return 'Cancelled';
if (gcsError) return 'GCS Error';
return '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 Stored securely in Google Cloud Storage 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 to Google Cloud Storage! 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, file.gcsError)}
</span>
{/* GCS indicator */}
{file.storageType === 'gcs' && (
<Cloud className="h-3 w-3 text-blue-500" />
)}
</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;