windsurf/converter/components/file-upload.tsx

198 lines
6.0 KiB
TypeScript
Raw Permalink Normal View History

2024-11-20 23:27:41 +00:00
'use client';
import { useState, useCallback } from 'react';
import { Upload, File as FileIcon, X, Eye } from 'lucide-react';
import { Button } from './ui/button';
import { Card } from './ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from './ui/dialog';
interface UploadedFile {
file: File;
text?: string;
}
interface FileUploadProps {
onFilesUploaded: (files: File[]) => void;
onTextExtracted?: (fileName: string, text: string) => void;
}
export function FileUpload({ onFilesUploaded, onTextExtracted }: FileUploadProps) {
const [files, setFiles] = useState<UploadedFile[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [selectedFile, setSelectedFile] = useState<UploadedFile | null>(null);
const [isLoading, setIsLoading] = useState<{ [key: string]: boolean }>({});
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
}, []);
const processFile = async (file: File) => {
setIsLoading(prev => ({ ...prev, [file.name]: true }));
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/extract', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Failed to extract text');
}
const data = await response.json();
if (onTextExtracted) {
onTextExtracted(file.name, data.text);
}
return data.text;
} catch (error) {
console.error('Error extracting text:', error);
return undefined;
} finally {
setIsLoading(prev => ({ ...prev, [file.name]: false }));
}
};
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files).filter(
file => file.type === 'application/pdf'
);
if (droppedFiles.length > 0) {
const newFiles = droppedFiles.map(file => ({ file }));
setFiles(prev => [...prev, ...newFiles]);
onFilesUploaded(droppedFiles);
// Process each file
for (const file of droppedFiles) {
const text = await processFile(file);
setFiles(prev =>
prev.map(f =>
f.file.name === file.name ? { ...f, text } : f
)
);
}
}
}, [onFilesUploaded]);
const handleFileInput = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const selectedFiles = Array.from(e.target.files).filter(
file => file.type === 'application/pdf'
);
if (selectedFiles.length > 0) {
const newFiles = selectedFiles.map(file => ({ file }));
setFiles(prev => [...prev, ...newFiles]);
onFilesUploaded(selectedFiles);
// Process each file
for (const file of selectedFiles) {
const text = await processFile(file);
setFiles(prev =>
prev.map(f =>
f.file.name === file.name ? { ...f, text } : f
)
);
}
}
}
}, [onFilesUploaded]);
const removeFile = useCallback((index: number) => {
setFiles(prev => prev.filter((_, i) => i !== index));
}, []);
return (
<div className="w-full space-y-4">
<div
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer ${
isDragging ? 'border-primary bg-primary/10' : 'border-gray-300'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => document.getElementById('file-upload')?.click()}
>
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<span className="text-primary hover:text-primary/80">
Click to upload
</span>
<span className="text-gray-500"> or drag and drop</span>
<input
id="file-upload"
type="file"
className="hidden"
multiple
accept=".pdf"
onChange={handleFileInput}
/>
</div>
<p className="text-sm text-gray-500 mt-2">PDF files only</p>
</div>
{files.length > 0 && (
<div className="space-y-2">
{files.map((uploadedFile, index) => (
<Card key={index} className="p-4 flex items-center justify-between">
<div className="flex items-center space-x-4">
<FileIcon className="h-6 w-6 text-gray-400" />
<span className="text-sm">{uploadedFile.file.name}</span>
{isLoading[uploadedFile.file.name] && (
<span className="text-sm text-gray-500">Processing...</span>
)}
</div>
<div className="flex items-center space-x-2">
{uploadedFile.text && (
<Button
variant="outline"
size="icon"
onClick={() => setSelectedFile(uploadedFile)}
>
<Eye className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={() => removeFile(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
</Card>
))}
</div>
)}
<Dialog open={!!selectedFile} onOpenChange={() => setSelectedFile(null)}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{selectedFile?.file.name}</DialogTitle>
</DialogHeader>
<div className="mt-4 whitespace-pre-wrap font-mono text-sm">
{selectedFile?.text}
</div>
</DialogContent>
</Dialog>
</div>
);
}