amaniquery-frontend / src /components /chat /FileUpload.tsx
Benaah's picture
Upload folder using huggingface_hub
6a059d3 verified
Raw
History Blame Contribute Delete
11.7 kB
"use client"
import React, { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { X, FileText, Image, File, Upload, Plus, Music, Video } from "lucide-react"
interface FileUploadProps {
files: File[]
onFilesChange: (files: File[]) => void
maxFiles?: number
maxSizeMB?: number
enableMedia?: boolean // Enable audio/video support
enableDocuments?: boolean // Enable large document support
}
const ALLOWED_TYPES = {
// Documents
"application/pdf": [".pdf"],
"text/plain": [".txt"],
"text/markdown": [".md"],
// Images
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"],
"image/gif": [".gif"],
"image/webp": [".webp"],
}
// Extended document types for WeKnora knowledge base ingestion
const DOCUMENT_TYPES = {
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"application/vnd.ms-excel": [".xls"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
"text/csv": [".csv"],
"application/json": [".json"],
"application/rtf": [".rtf"],
"text/html": [".html", ".htm"],
}
const MEDIA_TYPES = {
// Audio
"audio/mpeg": [".mp3"],
"audio/wav": [".wav"],
"audio/mp4": [".m4a"],
"audio/ogg": [".ogg"],
"audio/flac": [".flac"],
"audio/aac": [".aac"],
// Video
"video/mp4": [".mp4"],
"video/quicktime": [".mov"],
"video/x-msvideo": [".avi"],
"video/x-matroska": [".mkv"],
"video/webm": [".webm"],
"video/x-m4v": [".m4v"],
}
export function FileUpload({
files,
onFilesChange,
maxFiles = 10,
maxSizeMB = 50, // Increased for larger documents
enableMedia = true,
enableDocuments = true, // Enable extended document types by default
}: FileUploadProps) {
const fileInputRef = useRef<HTMLInputElement>(null)
const [dragActive, setDragActive] = useState(false)
const [imagePreviews, setImagePreviews] = useState<Map<string, string>>(new Map())
// Combine allowed types based on flags
const getAllowedTypes = () => {
let types = { ...ALLOWED_TYPES }
if (enableDocuments) {
types = { ...types, ...DOCUMENT_TYPES }
}
if (enableMedia) {
types = { ...types, ...MEDIA_TYPES }
}
return types
}
const handleFiles = (fileList: FileList | null) => {
if (!fileList) return
const newFiles: File[] = []
const maxSize = maxSizeMB * 1024 * 1024
const allowedTypes = getAllowedTypes()
Array.from(fileList).forEach((file) => {
// Check file count
if (files.length + newFiles.length >= maxFiles) {
return
}
// Check file size
if (file.size > maxSize) {
alert(`File ${file.name} exceeds ${maxSizeMB}MB limit`)
return
}
// Check file type
const isValidType = Object.keys(allowedTypes).some((mimeType) => {
const extensions = allowedTypes[mimeType as keyof typeof allowedTypes]
return extensions.some((ext) => file.name.toLowerCase().endsWith(ext))
})
if (!isValidType) {
alert(`File type not supported: ${file.name}`)
return
}
newFiles.push(file)
})
if (newFiles.length > 0) {
const updatedFiles = [...files, ...newFiles]
onFilesChange(updatedFiles)
// Generate image previews for new image files
newFiles.forEach((file) => {
if (file.type.startsWith("image/")) {
const fileKey = `${file.name}-${file.size}`
const reader = new FileReader()
reader.onload = (e) => {
if (e.target?.result) {
setImagePreviews(prev => new Map(prev).set(fileKey, e.target!.result as string))
}
}
reader.readAsDataURL(file)
}
})
}
}
const handleDrag = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true)
} else if (e.type === "dragleave") {
setDragActive(false)
}
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragActive(false)
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files)
}
}
const removeFile = (index: number) => {
const fileToRemove = files[index]
const newFiles = files.filter((_, i) => i !== index)
onFilesChange(newFiles)
// Clean up preview for removed file
if (fileToRemove) {
const fileKey = `${fileToRemove.name}-${fileToRemove.size}`
setImagePreviews(prev => {
const newMap = new Map(prev)
newMap.delete(fileKey)
return newMap
})
}
}
const getFileIcon = (file: File) => {
if (file.type === "application/pdf") {
return <FileText className="w-4 h-4" />
} else if (file.type.startsWith("image/")) {
return <Image className="w-4 h-4" />
} else if (file.type.startsWith("audio/")) {
return <Music className="w-4 h-4 text-blue-400" />
} else if (file.type.startsWith("video/")) {
return <Video className="w-4 h-4 text-purple-400" />
} else {
return <File className="w-4 h-4" />
}
}
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B"
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
}
const isImage = (file: File) => file.type.startsWith("image/")
const canAddMore = files.length < maxFiles
// Build accept string based on enableMedia
const getAcceptString = () => {
let base = ".pdf,.png,.jpg,.jpeg,.gif,.webp,.txt,.md"
if (enableDocuments) {
base += ",.doc,.docx,.xls,.xlsx,.csv,.json,.rtf,.html,.htm"
}
if (enableMedia) {
base += ",.mp3,.wav,.m4a,.ogg,.flac,.aac,.mp4,.mov,.avi,.mkv,.webm,.m4v"
}
return base
}
const getDescriptionText = () => {
if (enableDocuments && enableMedia) {
return `Documents, Images, Audio, Video (max ${maxSizeMB}MB, up to ${maxFiles} files)`
} else if (enableDocuments) {
return `PDF, DOC, Excel, CSV, Images (max ${maxSizeMB}MB, up to ${maxFiles} files)`
} else if (enableMedia) {
return `PDF, Images, Audio, Video (max ${maxSizeMB}MB, up to ${maxFiles} files)`
}
return `PDF, Images, Text files (max ${maxSizeMB}MB, up to ${maxFiles} files)`
}
return (
<div className="space-y-2">
<input
ref={fileInputRef}
type="file"
title="Upload Files"
multiple
accept={getAcceptString()}
onChange={(e) => handleFiles(e.target.files)}
className="hidden"
/>
{/* Show upload area only when no files are present */}
{files.length === 0 && (
<div
className={`border-2 border-dashed rounded-lg p-3 md:p-4 transition-colors ${
dragActive
? "border-primary bg-primary/5"
: "border-muted-foreground/25 hover:border-muted-foreground/50"
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<div className="flex flex-col items-center justify-center space-y-2">
<Upload className="w-5 h-5 md:w-6 md:h-6 text-muted-foreground" />
<p className="text-xs md:text-sm text-muted-foreground text-center">
Drag and drop files here, or{" "}
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="text-primary hover:underline"
>
browse
</button>
</p>
<p className="text-[10px] md:text-xs text-muted-foreground">
{getDescriptionText()}
</p>
</div>
</div>
)}
{/* Show compact file list when files are present */}
{files.length > 0 && (
<div className="space-y-2">
{/* Horizontal scrollable file list */}
<div className="flex gap-2 overflow-x-auto pb-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
{files.map((file, index) => (
<div
key={index}
className="flex-shrink-0 w-24 md:w-28 bg-muted/50 rounded-lg border border-white/10 overflow-hidden group relative"
>
{/* Image preview or icon */}
{isImage(file) && imagePreviews.get(`${file.name}-${file.size}`) ? (
<div className="relative w-full h-20 md:h-24 bg-muted">
<img
src={imagePreviews.get(`${file.name}-${file.size}`)}
alt={file.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
</div>
) : (
<div className="w-full h-20 md:h-24 bg-muted/80 flex items-center justify-center">
{getFileIcon(file)}
</div>
)}
{/* File info */}
<div className="p-1.5 md:p-2 space-y-0.5">
<p className="text-[10px] md:text-xs font-medium truncate" title={file.name}>
{file.name}
</p>
<p className="text-[9px] md:text-[10px] text-muted-foreground">
{formatFileSize(file.size)}
</p>
</div>
{/* Remove button */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeFile(index)}
className="absolute top-1 right-1 h-6 w-6 md:h-7 md:w-7 p-0 rounded-full bg-black/50 hover:bg-black/70 text-white opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="w-3 h-3 md:w-3.5 md:h-3.5" />
</Button>
</div>
))}
{/* Add more files button */}
{canAddMore && (
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="flex-shrink-0 w-24 md:w-28 h-full min-h-[120px] md:min-h-[140px] border-2 border-dashed border-muted-foreground/25 hover:border-primary/50 rounded-lg flex flex-col items-center justify-center gap-1.5 md:gap-2 transition-colors bg-muted/30 hover:bg-muted/50"
>
<Plus className="w-5 h-5 md:w-6 md:h-6 text-muted-foreground" />
<span className="text-[10px] md:text-xs text-muted-foreground text-center px-1">
Add more
</span>
</button>
)}
</div>
{/* File count indicator */}
<div className="flex items-center justify-between text-[10px] md:text-xs text-muted-foreground px-1">
<span>
{files.length} of {maxFiles} files
</span>
{files.length >= maxFiles && (
<span className="text-orange-500">Maximum reached</span>
)}
</div>
</div>
)}
</div>
)
}