fazeel007's picture
Fix Hugging Face Spaces deployment: Handle file permissions and production environment
ccf1a85
import React, { useState, useCallback } from 'react';
import { Upload, FileText, Image, FileCode, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
interface UploadedFile {
id: string;
file: File;
status: 'pending' | 'uploading' | 'processing' | 'completed' | 'failed';
progress: number;
documentId?: number;
error?: string;
}
interface Document {
id: number;
title: string;
fileName: string;
fileSize: number;
mimeType: string;
processingStatus: string;
createdAt: string;
}
export default function DocumentUpload() {
const [files, setFiles] = useState<UploadedFile[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [uploadTitle, setUploadTitle] = useState('');
const [uploadSource, setUploadSource] = useState('');
const [documents, setDocuments] = useState<Document[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const acceptedTypes = [
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'text/plain',
'text/markdown',
'application/json',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
const getFileIcon = (mimeType: string) => {
if (mimeType.startsWith('image/')) return <Image className="w-5 h-5" />;
if (mimeType === 'application/pdf') return <FileText className="w-5 h-5" />;
if (mimeType.includes('text') || mimeType.includes('json')) return <FileCode className="w-5 h-5" />;
return <FileText className="w-5 h-5" />;
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircle className="w-4 h-4 text-green-500" />;
case 'failed':
return <XCircle className="w-4 h-4 text-red-500" />;
case 'processing':
case 'uploading':
return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
default:
return <AlertCircle className="w-4 h-4 text-yellow-500" />;
}
};
const formatFileSize = (bytes: number): string => {
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 handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
handleFiles(droppedFiles);
}, []);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const selectedFiles = Array.from(e.target.files);
handleFiles(selectedFiles);
}
};
const handleFiles = (newFiles: File[]) => {
const validFiles = newFiles.filter(file => {
if (!acceptedTypes.includes(file.type)) {
alert(`File type ${file.type} is not supported`);
return false;
}
if (file.size > 50 * 1024 * 1024) { // 50MB limit
alert(`File ${file.name} is too large. Maximum size is 50MB`);
return false;
}
return true;
});
const uploadFiles: UploadedFile[] = validFiles.map(file => ({
id: `${Date.now()}-${Math.random()}`,
file,
status: 'pending',
progress: 0
}));
setFiles(prev => [...prev, ...uploadFiles]);
};
const uploadFiles = async () => {
const pendingFiles = files.filter(f => f.status === 'pending');
if (pendingFiles.length === 0) return;
for (const uploadFile of pendingFiles) {
try {
// Update status to uploading
setFiles(prev => prev.map(f =>
f.id === uploadFile.id
? { ...f, status: 'uploading', progress: 0 }
: f
));
const formData = new FormData();
formData.append('files', uploadFile.file);
if (uploadTitle) formData.append('title', uploadTitle);
if (uploadSource) formData.append('source', uploadSource);
const response = await fetch('/api/documents/upload', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (!response.ok) {
// Handle disabled uploads gracefully
if (response.status === 503) {
throw new Error(result.message || 'File uploads are disabled in this environment');
}
throw new Error(result.message || `Upload failed: ${response.statusText}`);
}
if (result.success && result.documents?.length > 0) {
const document = result.documents[0];
// Update file status to processing
setFiles(prev => prev.map(f =>
f.id === uploadFile.id
? {
...f,
status: 'processing',
progress: 100,
documentId: document.id
}
: f
));
// Add to documents list
setDocuments(prev => [document, ...prev]);
// If file requires processing, start processing
if (document.processingStatus === 'pending') {
await processDocument(document.id, uploadFile.id);
} else {
// Mark as completed if no processing needed
setFiles(prev => prev.map(f =>
f.id === uploadFile.id
? { ...f, status: 'completed' }
: f
));
}
} else {
throw new Error(result.message || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
setFiles(prev => prev.map(f =>
f.id === uploadFile.id
? {
...f,
status: 'failed',
error: error instanceof Error ? error.message : 'Upload failed'
}
: f
));
}
}
};
const processDocument = async (documentId: number, fileId: string) => {
try {
const response = await fetch(`/api/documents/process/${documentId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
operations: ['extract_text', 'generate_embedding']
})
});
const result = await response.json();
if (result.success) {
// Update file status to completed
setFiles(prev => prev.map(f =>
f.id === fileId
? { ...f, status: 'completed' }
: f
));
// Update document in list
setDocuments(prev => prev.map(doc =>
doc.id === documentId
? { ...doc, processingStatus: 'completed' }
: doc
));
} else {
throw new Error(result.message || 'Processing failed');
}
} catch (error) {
console.error('Processing error:', error);
setFiles(prev => prev.map(f =>
f.id === fileId
? {
...f,
status: 'failed',
error: error instanceof Error ? error.message : 'Processing failed'
}
: f
));
}
};
const batchProcessDocuments = async () => {
const completedDocuments = documents.filter(doc => doc.processingStatus === 'completed');
if (completedDocuments.length === 0) {
alert('No processed documents available for batch operations');
return;
}
setIsProcessing(true);
try {
// Build vector index
const response = await fetch('/api/documents/index/build', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
documentIds: completedDocuments.map(doc => doc.id),
indexName: 'main_index'
})
});
const result = await response.json();
if (result.success) {
alert(`Vector index built successfully with ${result.documentCount} documents`);
} else {
throw new Error(result.message || 'Index building failed');
}
} catch (error) {
console.error('Batch processing error:', error);
alert(`Batch processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsProcessing(false);
}
};
const removeFile = (fileId: string) => {
setFiles(prev => prev.filter(f => f.id !== fileId));
};
const clearCompleted = () => {
setFiles(prev => prev.filter(f => f.status !== 'completed' && f.status !== 'failed'));
};
return (
<div className="space-y-6">
{/* Upload Area */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5" />
Document Upload
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Optional metadata */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="title">Title (optional)</Label>
<Input
id="title"
placeholder="Document title"
value={uploadTitle}
onChange={(e) => setUploadTitle(e.target.value)}
/>
</div>
<div>
<Label htmlFor="source">Source (optional)</Label>
<Input
id="source"
placeholder="Document source"
value={uploadSource}
onChange={(e) => setUploadSource(e.target.value)}
/>
</div>
</div>
{/* Drag and drop area */}
<div
className={`
border-2 border-dashed rounded-lg p-8 text-center transition-colors
${isDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'border-gray-300 hover:border-gray-400 dark:border-gray-600'
}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<p className="text-lg font-medium mb-2">
Drop files here or click to browse
</p>
<p className="text-sm text-gray-500 mb-4">
Supports PDF, images, text files, Word documents (max 50MB each)
</p>
<input
type="file"
multiple
accept={acceptedTypes.join(',')}
onChange={handleFileSelect}
className="hidden"
id="file-upload"
/>
<Button asChild variant="outline">
<label htmlFor="file-upload" className="cursor-pointer">
Browse Files
</label>
</Button>
</div>
{/* File list */}
{files.length > 0 && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<h3 className="font-medium">Files ({files.length})</h3>
<div className="space-x-2">
<Button onClick={uploadFiles} size="sm" disabled={!files.some(f => f.status === 'pending')}>
Upload All
</Button>
<Button onClick={clearCompleted} variant="outline" size="sm">
Clear Completed
</Button>
</div>
</div>
{files.map((file) => (
<div key={file.id} className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center gap-3 flex-1">
{getFileIcon(file.file.type)}
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{file.file.name}</p>
<p className="text-sm text-gray-500">
{formatFileSize(file.file.size)}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant={
file.status === 'completed' ? 'default' :
file.status === 'failed' ? 'destructive' :
'secondary'
}>
{file.status}
</Badge>
{getStatusIcon(file.status)}
{(file.status === 'uploading' || file.status === 'processing') && (
<div className="w-24">
<Progress value={file.progress} className="h-2" />
</div>
)}
{(file.status === 'pending' || file.status === 'failed') && (
<Button
variant="ghost"
size="sm"
onClick={() => removeFile(file.id)}
>
<XCircle className="w-4 h-4" />
</Button>
)}
</div>
</div>
))}
</div>
)}
{/* Error alerts */}
{files.some(f => f.error) && (
<Alert variant="destructive">
<AlertCircle className="w-4 h-4" />
<AlertDescription>
Some files failed to upload. Check individual file status above.
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
{/* Document Management */}
{documents.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Uploaded Documents ({documents.length})</span>
<Button
onClick={batchProcessDocuments}
disabled={isProcessing || documents.filter(d => d.processingStatus === 'completed').length === 0}
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Building Index...
</>
) : (
'Build Vector Index'
)}
</Button>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{documents.map((doc) => (
<div key={doc.id} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-3 flex-1">
{getFileIcon(doc.mimeType)}
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{doc.title}</p>
<p className="text-sm text-gray-500">
{doc.fileName} • {formatFileSize(doc.fileSize)}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant={
doc.processingStatus === 'completed' ? 'default' :
doc.processingStatus === 'failed' ? 'destructive' :
'secondary'
}>
{doc.processingStatus}
</Badge>
{getStatusIcon(doc.processingStatus)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
}