import { FileTextIcon, TrashIcon, UploadIcon } from "@phosphor-icons/react"; import { Cross2Icon } from "@radix-ui/react-icons"; import { useRef, useState } from "react"; import { Button } from "../Button/Button"; import { formatFileSize, getFileLabel } from "./helpers"; import { cn } from "@/lib/utils"; import { Progress } from "../Progress/Progress"; import { Text } from "../Text/Text"; type UploadFileResult = { file_name: string; size: number; content_type: string; file_uri: string; }; type FileInputVariant = "default" | "compact"; interface BaseProps { value?: string; placeholder?: string; onChange: (value: string) => void; className?: string; maxFileSize?: number; accept?: string | string[]; variant?: FileInputVariant; showStorageNote?: boolean; } interface UploadModeProps extends BaseProps { mode?: "upload"; onUploadFile: (file: File) => Promise; uploadProgress: number; } interface Base64ModeProps extends BaseProps { mode: "base64"; onUploadFile?: never; uploadProgress?: never; } type Props = UploadModeProps | Base64ModeProps; export function FileInput(props: Props) { const { value, onChange, className, maxFileSize, accept, placeholder, variant = "default", showStorageNote = true, mode = "upload", } = props; const onUploadFile = mode === "upload" ? (props as UploadModeProps).onUploadFile : undefined; const uploadProgress = mode === "upload" ? (props as UploadModeProps).uploadProgress : 0; const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [fileInfo, setFileInfo] = useState<{ name: string; size: number; content_type: string; } | null>(null); const inputRef = useRef(null); const storageNote = "Files are stored securely and will be automatically deleted at most 24 hours after upload."; function acceptToString(a?: string | string[]) { if (!a) return "*/*"; return Array.isArray(a) ? a.join(",") : a; } function isAcceptedType(file: File, a?: string | string[]) { if (!a) return true; const list = Array.isArray(a) ? a : a.split(",").map((s) => s.trim()); const fileType = file.type; const fileExt = file.name.includes(".") ? `.${file.name.split(".").pop()}`.toLowerCase() : ""; for (const entry of list) { if (!entry) continue; const e = entry.toLowerCase(); if (e.includes("/")) { const [main, sub] = e.split("/"); const [fMain, fSub] = fileType.toLowerCase().split("/"); if (!fMain || !fSub) continue; if (sub === "*") { if (main === fMain) return true; } else { if (e === fileType.toLowerCase()) return true; } } else if (e.startsWith(".")) { if (fileExt === e) return true; } } return false; } const getFileLabelFromValue = (val: unknown): string => { // Handle object format from external API: { name, type, size, data } if (val && typeof val === "object") { const obj = val as Record; if (typeof obj.name === "string") { return getFileLabel( obj.name, typeof obj.type === "string" ? obj.type : "", ); } if (typeof obj.type === "string") { const mimeParts = obj.type.split("/"); if (mimeParts.length > 1) { return `${mimeParts[1].toUpperCase()} file`; } return `${obj.type} file`; } return "File"; } // Handle string values (data URIs or file paths) if (typeof val !== "string") { return "File"; } if (val.startsWith("data:")) { const matches = val.match(/^data:([^;]+);/); if (matches?.[1]) { const mimeParts = matches[1].split("/"); if (mimeParts.length > 1) { return `${mimeParts[1].toUpperCase()} file`; } return `${matches[1]} file`; } } else { const pathParts = val.split("."); if (pathParts.length > 1) { const ext = pathParts.pop(); if (ext) return `${ext.toUpperCase()} file`; } } return "File"; }; const processFileBase64 = (file: File) => { setIsUploading(true); setUploadError(null); const reader = new FileReader(); reader.onload = (e) => { const base64String = e.target?.result as string; setFileInfo({ name: file.name, size: file.size, content_type: file.type || "application/octet-stream", }); onChange(base64String); setIsUploading(false); }; reader.onerror = () => { setUploadError("Failed to read file"); setIsUploading(false); }; reader.readAsDataURL(file); }; const uploadFile = async (file: File) => { if (mode === "base64") { processFileBase64(file); return; } if (!onUploadFile) { setUploadError("Upload handler not provided"); return; } setIsUploading(true); setUploadError(null); try { const result = await onUploadFile(file); setFileInfo({ name: result.file_name, size: result.size, content_type: result.content_type, }); onChange(result.file_uri); } catch (error) { console.error("Upload failed:", error); setUploadError(error instanceof Error ? error.message : "Upload failed"); } finally { setIsUploading(false); } }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // Validate max size if (typeof maxFileSize === "number" && file.size > maxFileSize) { setUploadError( `File exceeds maximum size of ${formatFileSize(maxFileSize)} (selected ${formatFileSize(file.size)})`, ); return; } // Validate accept types if (!isAcceptedType(file, accept)) { setUploadError("Selected file type is not allowed"); return; } uploadFile(file); }; const handleFileDrop = (event: React.DragEvent) => { event.preventDefault(); const file = event.dataTransfer.files[0]; if (file) uploadFile(file); }; const handleClear = () => { if (inputRef.current) { inputRef.current.value = ""; } onChange(""); setFileInfo(null); }; const displayName = placeholder || "File"; if (variant === "compact") { return (
{isUploading ? (
{mode === "base64" ? "Processing..." : "Uploading..."} {mode === "upload" && ( {Math.round(uploadProgress)}% )}
{mode === "upload" && ( )}
) : value ? (
{fileInfo ? getFileLabel(fileInfo.name, fileInfo.content_type) : getFileLabelFromValue(value)} {fileInfo && ( {formatFileSize(fileInfo.size)} )}
) : (
)}
{uploadError && ( {uploadError} )}
); } return (
{isUploading ? (
{mode === "base64" ? "Processing..." : "Uploading..."} {mode === "upload" && ( {Math.round(uploadProgress)}% )}
{mode === "upload" && ( )}
{showStorageNote && mode === "upload" && (

{storageNote}

)}
) : value ? (
{fileInfo ? getFileLabel(fileInfo.name, fileInfo.content_type) : getFileLabelFromValue(value)} {fileInfo ? formatFileSize(fileInfo.size) : ""}
{showStorageNote && mode === "upload" && (

{storageNote}

)}
) : (
e.preventDefault()} className="agpt-border-input flex min-h-14 w-full items-center justify-center rounded-xl border-dashed bg-zinc-50 text-sm text-gray-500" > Choose a file or drag and drop it here
{uploadError && (
Error: {uploadError}
)} {showStorageNote && mode === "upload" && (

{storageNote}

)}
)}
); }