"use client" import { useEffect, useCallback, useRef, useState } from "react" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" import { FileIcon, XIcon, AlertCircleIcon, CheckIcon } from "lucide-react" import { useHotkeys } from "react-hotkeys-hook" import { toast } from "sonner" export const FILE_ACCEPT = "image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.mdx,.json,.html,.htm,text/markdown,application/json,text/html" export type FileQueueItemStatus = "pending" | "uploading" | "success" | "error" export interface FileQueueItem { id: string file: File status: FileQueueItemStatus errorMessage?: string } export interface FileData { items: FileQueueItem[] title: string description: string } interface FileContentProps { data: FileData onDataChange: (data: FileData) => void onRequestSubmit: () => void isSubmitting?: boolean isOpen?: boolean } function isAcceptedFile(file: File): boolean { const name = file.name.toLowerCase() const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : "" const allowedExt = new Set([ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".csv", ".txt", ".md", ".mdx", ".json", ".html", ".htm", ]) if (allowedExt.has(ext)) return true if (file.type.startsWith("image/")) return true if (file.type === "text/markdown") return true if (file.type === "application/json") return true if (file.type === "text/html") return true return false } function fileQueueKey(file: File): string { return `${file.name}:${file.size}:${file.lastModified}` } function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B` const units = ["KB", "MB", "GB", "TB"] let size = bytes / 1024 let i = 0 while (size >= 1024 && i < units.length - 1) { size /= 1024 i++ } return `${size.toFixed(size < 10 ? 1 : 0)} ${units[i]}` } export function FileContent({ data, onDataChange, onRequestSubmit, isSubmitting, isOpen, }: FileContentProps) { const inputRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const anyUploading = data.items.some((i) => i.status === "uploading") const canSubmit = data.items.some((i) => i.status === "pending") && !isSubmitting && !anyUploading const updateData = useCallback( (partial: Partial) => { onDataChange({ ...data, ...partial }) }, [data, onDataChange], ) const addFiles = useCallback( (fileList: FileList | File[]) => { const incoming = Array.from(fileList) const accepted = incoming.filter(isAcceptedFile) const rejected = incoming.length - accepted.length if (rejected > 0) { toast.error( rejected === 1 ? "One file type is not supported" : `${rejected} files are not supported`, ) } if (accepted.length === 0) return const existingKeys = new Set(data.items.map((i) => fileQueueKey(i.file))) let duplicateCount = 0 const toAdd: FileQueueItem[] = [] for (const file of accepted) { const key = fileQueueKey(file) if (existingKeys.has(key)) { duplicateCount++ continue } existingKeys.add(key) toAdd.push({ id: crypto.randomUUID(), file, status: "pending", }) } if (duplicateCount > 0) { toast.message( duplicateCount === 1 ? "Skipped duplicate file" : `Skipped ${duplicateCount} duplicate files`, ) } if (toAdd.length === 0) return onDataChange({ ...data, items: [...data.items, ...toAdd], }) }, [data, onDataChange], ) const removeItem = useCallback( (id: string) => { onDataChange({ ...data, items: data.items.filter((i) => i.id !== id), }) }, [data, onDataChange], ) const handleTitleChange = useCallback( (title: string) => updateData({ title }), [updateData], ) const handleDescriptionChange = useCallback( (description: string) => updateData({ description }), [updateData], ) useHotkeys("mod+enter", onRequestSubmit, { enabled: Boolean(isOpen && canSubmit), enableOnFormTags: ["INPUT", "TEXTAREA"], }) useEffect(() => { if (!isOpen && inputRef.current) { inputRef.current.value = "" } }, [isOpen]) const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) if (e.dataTransfer.files?.length) { addFiles(e.dataTransfer.files) } } const handleFileSelect = (e: React.ChangeEvent) => { const list = e.target.files if (list?.length) { addFiles(list) } e.target.value = "" } const showTitleDescription = data.items.length <= 1 const hasItems = data.items.length > 0 return (

Upload files

Images, PDF, documents, sheets, markdown, HTML

{hasItems ? (
    {data.items.map((item) => (
  • {item.status === "uploading" ? (
    ) : null} {item.status === "success" ? (
    ) : null}
    {item.status === "uploading" ? ( Uploading {item.file.name} ) : null}

    {item.file.name}

    {formatFileSize(item.file.size)}

    {item.status === "error" && item.errorMessage ? (

    {item.errorMessage}

    ) : null}
    {item.status === "pending" ? ( ) : null} {item.status === "success" ? ( ) : null} {item.status === "error" ? ( ) : null}
  • ))}
) : null} {showTitleDescription ? ( <>

Title (optional)

handleTitleChange(e.target.value)} placeholder="Give this file a title" disabled={isSubmitting} className="w-full p-4 rounded-[14px] bg-[#14161A] shadow-inside-out disabled:opacity-50" />

Description (optional)