198 lines
6.0 KiB
TypeScript
198 lines
6.0 KiB
TypeScript
'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>
|
|
);
|
|
}
|