## 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
455 lines
16 KiB
TypeScript
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;
|