UNPKG

@llamaindex/ui

Version:

A comprehensive UI component library built with React, TypeScript, and Tailwind CSS for LlamaIndex applications

1,245 lines (1,237 loc) 44.9 kB
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './chunk-V2UNQEFC.mjs'; import { Progress } from './chunk-26WAV2SM.mjs'; import { Input } from './chunk-QMXCUFNI.mjs'; import { Tabs, TabsList, TabsTrigger, TabsContent } from './chunk-HHXQWWAC.mjs'; import { Button } from './chunk-SDCUNZIC.mjs'; import { cn } from './chunk-MG2ARK3A.mjs'; import { __publicField, __spreadProps, __spreadValues, __objRest } from './chunk-4AMAFZLZ.mjs'; import { FileText, ChevronDown, X, Loader2, Upload } from 'lucide-react'; import { useRef, useState, useCallback } from 'react'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { uploadFileApiV1FilesPost, readFileContentApiV1FilesIdContentGet, uploadFileFromUrlApiV1FilesUploadFromUrlPut } from 'llama-cloud-services/api'; function useFileDropzone({ onFilesSelected, multiple = false }) { const inputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const selectFiles = useCallback( (files) => { const fileArray = Array.isArray(files) ? files : Array.from(files); if (fileArray.length === 0) { return; } if (multiple) { onFilesSelected(fileArray); return; } onFilesSelected([fileArray[0]]); }, [multiple, onFilesSelected] ); const handleDragEnter = useCallback((event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(true); }, []); const handleDragOver = useCallback((event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback( (event) => { var _a, _b; event.preventDefault(); event.stopPropagation(); setIsDragging(false); if ((_b = (_a = event.dataTransfer) == null ? void 0 : _a.files) == null ? void 0 : _b.length) { selectFiles(event.dataTransfer.files); } }, [selectFiles] ); const handleFileInputChange = useCallback( (event) => { const { files } = event.target; if (files == null ? void 0 : files.length) { selectFiles(files); event.target.value = ""; } }, [selectFiles] ); const handleClick = useCallback(() => { var _a; (_a = inputRef.current) == null ? void 0 : _a.click(); }, []); return { inputRef, isDragging, handleDragEnter, handleDragOver, handleDragLeave, handleDrop, handleFileInputChange, handleClick }; } var unitLabels = ["B", "KB", "MB", "GB"]; function formatFileSize(bytes) { if (bytes <= 0) return "0 B"; let size = bytes; let index = 0; while (size >= 1024 && index < unitLabels.length - 1) { size /= 1024; index += 1; } const precision = size < 10 && index > 0 ? 1 : 0; return `${size.toFixed(precision)} ${unitLabels[index]}`; } function FileDropzone({ multiple = false, selectedFiles = [], onFilesSelected, onRemoveFile, className, allowedFileTypes, maxFileSizeBytes, listFooter, footer, showRemoveButton = true, disabled = false, emptyTitle = multiple ? "Upload files (drag or click)" : "Upload file (drag or click)", emptyDescription }) { const { inputRef, isDragging, handleDragEnter, handleDragOver, handleDragLeave, handleDrop, handleFileInputChange, handleClick } = useFileDropzone({ onFilesSelected: (files) => { if (!disabled) { onFilesSelected(files); } }, multiple }); const hasFiles = selectedFiles.length > 0; const acceptValue = (allowedFileTypes == null ? void 0 : allowedFileTypes.length) ? allowedFileTypes.map((type) => `.${type}`).join(",") : void 0; const displayAllowedTypes = (allowedFileTypes == null ? void 0 : allowedFileTypes.length) ? allowedFileTypes.map((type) => type.toUpperCase()).join(", ") : void 0; const maxSizeMb = maxFileSizeBytes && Math.round(maxFileSizeBytes / 1e3 / 1e3); const renderFileRow = (file) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-md bg-muted/30 px-3 py-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [ /* @__PURE__ */ jsx(Upload, { className: "h-4 w-4 flex-shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium", children: file.name }), /* @__PURE__ */ jsx("span", { className: "flex-shrink-0 text-xs text-muted-foreground", children: formatFileSize(file.size) }) ] }), showRemoveButton && onRemoveFile && /* @__PURE__ */ jsx( Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0 flex-shrink-0", onClick: (event) => { event.stopPropagation(); onRemoveFile(file); }, children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) } ) ] }); const renderFileContent = () => { if (!hasFiles) { return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-4 text-center", children: [ /* @__PURE__ */ jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-full bg-gray-100", children: /* @__PURE__ */ jsx(Upload, { className: "h-8 w-8" }) }), /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-gray-600", children: emptyTitle }), emptyDescription ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: emptyDescription }) : null ] }), (displayAllowedTypes || maxSizeMb) && /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-xs text-muted-foreground", children: [ displayAllowedTypes && !footer ? /* @__PURE__ */ jsxs("p", { children: [ "Supported: ", displayAllowedTypes ] }) : null, maxSizeMb ? /* @__PURE__ */ jsxs("p", { children: [ "Max size: ", maxSizeMb, "MB" ] }) : null ] }) ] }); } if (multiple) { return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-3", children: [ selectedFiles.map((file, index) => /* @__PURE__ */ jsx("div", { children: renderFileRow(file) }, `${file.name}-${file.size}-${index}`)), listFooter ] }); } return renderFileRow(selectedFiles[0]); }; return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs( "div", { className: cn( "flex flex-col gap-4 rounded-lg border-2 border-dotted p-8 transition-colors", disabled ? "opacity-60" : "cursor-pointer", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", !disabled && isDragging ? "border-primary bg-primary/5" : "border-gray-300 hover-border-primary/50", hasFiles ? "items-stretch text-left" : "items-center text-center min-h-[200px]", className ), onDragEnter: disabled ? void 0 : handleDragEnter, onDragLeave: disabled ? void 0 : handleDragLeave, onDragOver: disabled ? void 0 : handleDragOver, onDrop: disabled ? void 0 : handleDrop, onClick: disabled ? void 0 : handleClick, role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled, onKeyDown: disabled ? void 0 : (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); handleClick(); } }, children: [ /* @__PURE__ */ jsx( Input, { ref: inputRef, type: "file", className: "hidden", onChange: disabled ? void 0 : handleFileInputChange, accept: acceptValue, multiple, disabled } ), renderFileContent() ] } ), footer ] }); } function FileUpload({ className, heading, content, onContentChange, allowFileRemoval = false, showHeader = true, allowedFileTypes = [], maxFileSizeBytes, uploadDescription = "Upload file (drag or click)", fileUrlPlaceholder = "Paste the file link here", disableWhenHasSelection = false, footer }) { const selectedFile = content instanceof File ? content : null; const handleFilesSelected = (files) => { const [file] = files; if (file) { onContentChange(file); } }; const handleRemoveFile = (file) => { if (allowFileRemoval && selectedFile && selectedFile.name === file.name && selectedFile.size === file.size) { onContentChange(null); } }; return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), children: [ showHeader && /* @__PURE__ */ jsxs("div", { className: "text-center", children: [ /* @__PURE__ */ jsx("div", { className: "mb-6 flex justify-center", children: /* @__PURE__ */ jsx( "div", { className: "flex h-12 w-12 items-center justify-center rounded-full", style: { backgroundColor: "#F3F0FF", color: "#8B5CF6" }, children: /* @__PURE__ */ jsx(FileText, {}) } ) }), /* @__PURE__ */ jsx("h1", { className: "mb-8 text-sm font-semibold", children: heading }) ] }), /* @__PURE__ */ jsxs(Tabs, { defaultValue: "upload", children: [ /* @__PURE__ */ jsxs(TabsList, { className: "grid w-full grid-cols-2 rounded-none border-b border-gray-200 bg-transparent p-0", children: [ /* @__PURE__ */ jsx( TabsTrigger, { value: "upload", className: "rounded-none border-x-0 border-b-2 border-t-0 border-transparent bg-transparent text-xs font-semibold text-gray-500 shadow-none data-[state=active]:border-b-[#8B5CF6] data-[state=active]:text-[#8B5CF6] data-[state=active]:shadow-none", children: "Upload file" } ), /* @__PURE__ */ jsx( TabsTrigger, { value: "url", className: "rounded-none border-x-0 border-b-2 border-t-0 border-transparent bg-transparent text-xs font-semibold text-gray-500 shadow-none data-[state=active]:border-b-[#8B5CF6] data-[state=active]:text-[#8B5CF6] data-[state=active]:shadow-none", children: "File URL" } ) ] }), /* @__PURE__ */ jsx(TabsContent, { value: "upload", className: "mt-6", children: /* @__PURE__ */ jsx( FileDropzone, { selectedFiles: selectedFile ? [selectedFile] : [], onFilesSelected: handleFilesSelected, onRemoveFile: allowFileRemoval ? handleRemoveFile : void 0, allowedFileTypes, maxFileSizeBytes, emptyTitle: uploadDescription, showRemoveButton: allowFileRemoval, disabled: disableWhenHasSelection && typeof content === "string" && content.trim().length > 0 } ) }), /* @__PURE__ */ jsx(TabsContent, { value: "url", className: "mt-6", children: /* @__PURE__ */ jsx("div", { className: "rounded-lg border-2 border-gray-200 p-8", children: /* @__PURE__ */ jsx( Input, { type: "url", className: "w-full", placeholder: fileUrlPlaceholder, value: typeof content === "string" ? content : "", onChange: (event) => onContentChange(event.target.value), disabled: disableWhenHasSelection && selectedFile !== null } ) }) }) ] }), footer ] }); } // src/file-upload/utils/file-utils.ts var FileType = /* @__PURE__ */ ((FileType2) => { FileType2["JPEG"] = "jpeg"; FileType2["JPG"] = "jpg"; FileType2["PNG"] = "png"; FileType2["WEBP"] = "webp"; FileType2["PDF"] = "pdf"; FileType2["DOC"] = "doc"; FileType2["DOCX"] = "docx"; FileType2["XLS"] = "xls"; FileType2["XLSX"] = "xlsx"; FileType2["PPT"] = "ppt"; FileType2["PPTX"] = "pptx"; FileType2["TXT"] = "txt"; FileType2["CSV"] = "csv"; FileType2["JSON"] = "json"; FileType2["XML"] = "xml"; FileType2["HTML"] = "html"; FileType2["CSS"] = "css"; FileType2["JS"] = "js"; FileType2["TS"] = "ts"; FileType2["MD"] = "md"; return FileType2; })(FileType || {}); var FILE_TYPE_DEFINITIONS = { ["jpeg" /* JPEG */]: { extensions: ["jpg", "jpeg"], mimeTypes: ["image/jpeg", "image/jpg"], displayName: "JPEG Image", category: "image" }, ["jpg" /* JPG */]: { extensions: ["jpg", "jpeg"], mimeTypes: ["image/jpeg", "image/jpg"], displayName: "JPG Image", category: "image" }, ["png" /* PNG */]: { extensions: ["png"], mimeTypes: ["image/png"], displayName: "PNG Image", category: "image" }, ["webp" /* WEBP */]: { extensions: ["webp"], mimeTypes: ["image/webp"], displayName: "WebP Image", category: "image" }, ["pdf" /* PDF */]: { extensions: ["pdf"], mimeTypes: ["application/pdf"], displayName: "PDF Document", category: "document" }, ["doc" /* DOC */]: { extensions: ["doc"], mimeTypes: ["application/msword"], displayName: "Word Document", category: "document" }, ["docx" /* DOCX */]: { extensions: ["docx"], mimeTypes: [ "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ], displayName: "Word Document", category: "document" }, ["xls" /* XLS */]: { extensions: ["xls"], mimeTypes: ["application/vnd.ms-excel"], displayName: "Excel Spreadsheet", category: "document" }, ["xlsx" /* XLSX */]: { extensions: ["xlsx"], mimeTypes: [ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ], displayName: "Excel Spreadsheet", category: "document" }, ["ppt" /* PPT */]: { extensions: ["ppt"], mimeTypes: ["application/vnd.ms-powerpoint"], displayName: "PowerPoint Presentation", category: "document" }, ["pptx" /* PPTX */]: { extensions: ["pptx"], mimeTypes: [ "application/vnd.openxmlformats-officedocument.presentationml.presentation" ], displayName: "PowerPoint Presentation", category: "document" }, ["txt" /* TXT */]: { extensions: ["txt"], mimeTypes: ["text/plain"], displayName: "Text File", category: "text" }, ["csv" /* CSV */]: { extensions: ["csv"], mimeTypes: ["text/csv"], displayName: "CSV File", category: "text" }, ["json" /* JSON */]: { extensions: ["json"], mimeTypes: ["application/json"], displayName: "JSON File", category: "text" }, ["xml" /* XML */]: { extensions: ["xml"], mimeTypes: ["application/xml", "text/xml"], displayName: "XML File", category: "text" }, ["html" /* HTML */]: { extensions: ["html", "htm"], mimeTypes: ["text/html"], displayName: "HTML File", category: "text" }, ["css" /* CSS */]: { extensions: ["css"], mimeTypes: ["text/css"], displayName: "CSS File", category: "text" }, ["js" /* JS */]: { extensions: ["js"], mimeTypes: ["application/javascript", "text/javascript"], displayName: "JavaScript File", category: "text" }, ["ts" /* TS */]: { extensions: ["ts"], mimeTypes: ["application/typescript", "text/typescript"], displayName: "TypeScript File", category: "text" }, ["md" /* MD */]: { extensions: ["md", "markdown"], mimeTypes: ["text/markdown"], displayName: "Markdown File", category: "text" } }; var FILE_TYPE_GROUPS = { IMAGES: ["jpeg" /* JPEG */, "jpg" /* JPG */, "png" /* PNG */, "webp" /* WEBP */], DOCUMENTS: [ "pdf" /* PDF */, "doc" /* DOC */, "docx" /* DOCX */, "xls" /* XLS */, "xlsx" /* XLSX */, "ppt" /* PPT */, "pptx" /* PPTX */ ], TEXT: [ "txt" /* TXT */, "csv" /* CSV */, "json" /* JSON */, "xml" /* XML */, "html" /* HTML */, "css" /* CSS */, "js" /* JS */, "ts" /* TS */, "md" /* MD */ ], SPREADSHEETS: ["xls" /* XLS */, "xlsx" /* XLSX */, "csv" /* CSV */], PRESENTATIONS: ["ppt" /* PPT */, "pptx" /* PPTX */], COMMON_IMAGES: ["jpeg" /* JPEG */, "jpg" /* JPG */, "png" /* PNG */, "webp" /* WEBP */], OFFICE_DOCS: [ "pdf" /* PDF */, "doc" /* DOC */, "docx" /* DOCX */, "xls" /* XLS */, "xlsx" /* XLSX */, "ppt" /* PPT */, "pptx" /* PPTX */ ] }; var getFileTypeDefinition = (fileType) => { return FILE_TYPE_DEFINITIONS[fileType]; }; var getFileExtensions = (fileType) => { return FILE_TYPE_DEFINITIONS[fileType].extensions; }; var getFileMimeTypes = (fileType) => { return FILE_TYPE_DEFINITIONS[fileType].mimeTypes; }; var isFileTypeMatch = (file, fileType) => { var _a; const definition = FILE_TYPE_DEFINITIONS[fileType]; const fileExtension = (_a = file.name.split(".").pop()) == null ? void 0 : _a.toLowerCase(); const extensionMatch = fileExtension && definition.extensions.includes(fileExtension); const mimeTypeMatch = definition.mimeTypes.includes(file.type); return extensionMatch || mimeTypeMatch; }; var validateFile = (file, allowedFileTypes = [], maxFileSizeBytes = 10 * 1e3 * 1e3) => { if (file.size > maxFileSizeBytes) { return `File size exceeds ${Math.round(maxFileSizeBytes / 1e3 / 1e3)}MB limit`; } if (allowedFileTypes.length > 0) { const isValidType = allowedFileTypes.some((fileType) => { return isFileTypeMatch(file, fileType); }); if (!isValidType) { const allowedTypeNames = allowedFileTypes.map((fileType) => { return FILE_TYPE_DEFINITIONS[fileType].displayName; }); return `File type not allowed. Allowed types: ${allowedTypeNames.join(", ")}`; } } return null; }; var getFileTypesByCategory = (category) => { return Object.entries(FILE_TYPE_DEFINITIONS).filter(([, definition]) => definition.category === category).map(([fileType]) => fileType); }; var createFileTypeValidator = (allowedTypes, maxSizeBytes = 10 * 1e3 * 1e3) => { return (file) => validateFile(file, allowedTypes, maxSizeBytes); }; var formatFileSize2 = (bytes) => { if (bytes === 0) return "0 Bytes"; const k = 1e3; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; var isFileApiSupported = () => { return typeof File !== "undefined" && typeof FileReader !== "undefined"; }; var isCryptoSupported = () => { return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined"; }; // ../../shared/env.ts var import_meta = {}; function isDevelopment() { var _a; try { const env = import_meta == null ? void 0 : import_meta.env; if ((env == null ? void 0 : env.DEV) === true) return true; if ((env == null ? void 0 : env.MODE) === "development") return true; if ((env == null ? void 0 : env.PROD) === false) return true; } catch (e) { } if (typeof process !== "undefined" && ((_a = process == null ? void 0 : process.env) == null ? void 0 : _a.NODE_ENV)) { return process.env.NODE_ENV !== "production"; } return false; } // ../../shared/logger/index.ts var ConsoleLogger = class { constructor(name) { __publicField(this, "name"); this.name = name; } debug(message, ...args) { console.debug(`[${this.name}] ${message}`, ...args); } info(message, ...args) { console.info(`[${this.name}] ${message}`, ...args); } warn(message, ...args) { console.warn(`[${this.name}] ${message}`, ...args); } error(message, ...args) { console.error(`[${this.name}] ${message}`, ...args); } }; var NoOpLogger = class { debug(_message, ..._args) { } info(_message, ..._args) { } warn(_message, ..._args) { } error(_message, ..._args) { } }; var logger = isDevelopment() ? new ConsoleLogger("llama-ui") : new NoOpLogger(); function deriveFileNameFromUrl(url) { try { const parsed = new URL(url); const pathname = parsed.pathname.split("/").filter(Boolean); const lastSegment = pathname[pathname.length - 1]; if (lastSegment) { return decodeURIComponent(lastSegment); } } catch (error) { logger.error("failed to parse filename from url", { error, url }); } return url.replace(/[^a-z0-9_.-]/gi, "-") || "remote-file"; } function useFileUpload({ onProgress, onUploadStart, onUploadComplete, onUploadError } = {}) { const [isUploading, setIsUploading] = useState(false); const uploadFile = async (file) => { setIsUploading(true); onUploadStart == null ? void 0 : onUploadStart(file); try { const response = await uploadFileApiV1FilesPost({ body: { upload_file: file } }); if (response.error) { throw response.error; } const fileId = response.data.id; onProgress == null ? void 0 : onProgress(file, 10); const contentResponse = await readFileContentApiV1FilesIdContentGet({ path: { id: fileId } }); if (contentResponse.error) { throw contentResponse.error; } const fileUrl = contentResponse.data.url; onProgress == null ? void 0 : onProgress(file, 80); const fileData = { file, fileId, url: fileUrl }; onProgress == null ? void 0 : onProgress(file, 100); onUploadComplete == null ? void 0 : onUploadComplete(file); return { success: true, data: fileData, error: null }; } catch (error) { logger.error("uploadFile failed", { error }); const errorMessage = error instanceof Error ? error.message : "Upload failed"; onUploadError == null ? void 0 : onUploadError(file, errorMessage); return { success: false, data: null, error }; } finally { setIsUploading(false); } }; const uploadFromUrl = async (url, options = {}) => { const filename = options.name || deriveFileNameFromUrl(url); const virtualFile = new File([""], filename, { type: "text/url", lastModified: Date.now() }); setIsUploading(true); onUploadStart == null ? void 0 : onUploadStart(virtualFile); try { const response = await uploadFileFromUrlApiV1FilesUploadFromUrlPut({ body: { url, name: filename, proxy_url: options.proxyUrl, request_headers: options.requestHeaders } }); if (response.error) { throw response.error; } const fileId = response.data.id; onProgress == null ? void 0 : onProgress(virtualFile, 10); const contentResponse = await readFileContentApiV1FilesIdContentGet({ path: { id: fileId } }); if (contentResponse.error) { throw contentResponse.error; } const fileUrl = contentResponse.data.url; onProgress == null ? void 0 : onProgress(virtualFile, 80); const fileData = { file: virtualFile, fileId, url: fileUrl }; onProgress == null ? void 0 : onProgress(virtualFile, 100); onUploadComplete == null ? void 0 : onUploadComplete(virtualFile); return { success: true, data: fileData, error: null }; } catch (error) { logger.error("uploadFromUrl failed", { error }); const errorMessage = error instanceof Error ? error.message : "Upload failed"; onUploadError == null ? void 0 : onUploadError(virtualFile, errorMessage); return { success: false, data: null, error }; } finally { setIsUploading(false); } }; return { isUploading, uploadFile, uploadFromUrl, uploadAndReturn: uploadFile }; } // src/file-upload/store/upload-progress-store.ts var PROGRESS_THRESHOLD = 3; function getDisplayModes(fileCount) { return { showOverallProgress: fileCount > PROGRESS_THRESHOLD, isCompact: fileCount > PROGRESS_THRESHOLD }; } function addUploadToQueue(uploads, file) { const newUpload = { file, progress: 0, status: "uploading" }; const filtered = uploads.filter((upload) => upload.file.name !== file.name); return [...filtered, newUpload]; } function updateFileProgress(uploads, file, progress) { return uploads.map( (upload) => upload.file.name === file.name ? __spreadProps(__spreadValues({}, upload), { progress: Math.min(progress, 100) }) : upload ); } function completeFileUpload(uploads, file) { return uploads.map( (upload) => upload.file.name === file.name ? __spreadProps(__spreadValues({}, upload), { progress: 100, status: "completed" }) : upload ); } function failFileUpload(uploads, file, error) { return uploads.map( (upload) => upload.file.name === file.name ? __spreadProps(__spreadValues({}, upload), { status: "error", error }) : upload ); } function cancelFileUpload(uploads, file) { return uploads.map( (upload) => upload.file.name === file.name && upload.status === "uploading" ? __spreadProps(__spreadValues({}, upload), { status: "canceled" }) : upload ); } function cancelAllUploads(uploads) { return uploads.map( (upload) => upload.status === "uploading" ? __spreadProps(__spreadValues({}, upload), { status: "canceled" }) : upload ); } function removeFileUpload(uploads, file) { return uploads.filter((upload) => upload.file.name !== file.name); } function removeCompletedUploads(uploads) { return uploads.filter((upload) => upload.status !== "completed"); } function calculateUploadStats(uploads) { return { total: uploads.length, uploading: uploads.filter((f) => f.status === "uploading").length, completed: uploads.filter((f) => f.status === "completed").length, failed: uploads.filter((f) => f.status === "error").length, canceled: uploads.filter((f) => f.status === "canceled").length, totalProgress: uploads.length > 0 ? Math.round( uploads.reduce((sum, f) => sum + f.progress, 0) / uploads.length ) : 0 }; } function hasActiveUploads(uploads) { return uploads.length > 0; } function getVisibleFiles(uploads, showAll, maxVisible = 5) { const shouldShowViewMore = uploads.length > maxVisible; const filesToShow = showAll ? uploads : uploads.slice(0, maxVisible); return { filesToShow, shouldShowViewMore }; } // src/file-upload/hooks/use-upload-progress.ts function useUploadProgress() { const [uploadProgressFiles, setUploadProgressFiles] = useState([]); const [isVisible, setIsVisible] = useState(false); const startUpload = useCallback((file) => { setUploadProgressFiles((prev) => addUploadToQueue(prev, file)); setIsVisible(true); }, []); const updateProgress = useCallback((file, progress) => { setUploadProgressFiles((prev) => updateFileProgress(prev, file, progress)); }, []); const completeUpload = useCallback((file) => { setUploadProgressFiles((prev) => { const updated = completeFileUpload(prev, file); const allComplete = updated.every( (f) => f.status === "completed" || f.status === "error" ); if (allComplete) { setTimeout(() => { setUploadProgressFiles([]); setIsVisible(false); }, 1e4); } return updated; }); }, []); const failUpload = useCallback((file, error) => { setUploadProgressFiles((prev) => failFileUpload(prev, file, error)); }, []); const removeUpload = useCallback((file) => { setUploadProgressFiles((prev) => removeFileUpload(prev, file)); }, []); const clearAllUploads = useCallback(() => { setUploadProgressFiles([]); setIsVisible(false); }, []); const hideProgress = useCallback(() => { setIsVisible(false); }, []); const hasUploads = hasActiveUploads(uploadProgressFiles); if (!hasUploads && isVisible) { setIsVisible(false); } return { uploadProgressFiles, startUpload, updateProgress, completeUpload, failUpload, removeUpload, clearAllUploads, isVisible: isVisible && hasUploads, hideProgress }; } function UploadProgress({ files, onClose }) { const [isCollapsed, setIsCollapsed] = useState(false); const [showAll, setShowAll] = useState(false); if (files.length === 0) return null; const stats = calculateUploadStats(files); const { showOverallProgress, isCompact } = getDisplayModes(files.length); const maxVisible = 5; const { filesToShow, shouldShowViewMore } = getVisibleFiles( files, showAll, maxVisible ); return /* @__PURE__ */ jsxs("div", { className: "fixed bottom-4 right-4 z-50 w-80 bg-background border rounded-lg shadow-lg", children: [ /* @__PURE__ */ jsxs("div", { className: `p-4 ${showOverallProgress ? "border-b" : ""}`, children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [ /* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm", children: showOverallProgress ? `Uploading Files (${stats.completed}/${stats.total})` : files.length === 1 ? "Uploading File" : "Uploading Files" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ showOverallProgress && /* @__PURE__ */ jsx( Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: () => setIsCollapsed(!isCollapsed), children: /* @__PURE__ */ jsx( ChevronDown, { className: `h-4 w-4 transition-transform duration-200 ease-in-out ${isCollapsed ? "rotate-180" : "rotate-0"}` } ) } ), /* @__PURE__ */ jsx( Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: onClose, children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) } ) ] }) ] }), showOverallProgress && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( Progress, { value: stats.totalProgress, className: `h-2 mb-2 ${stats.uploading === 0 && stats.completed > 0 ? "[&>[data-slot=progress-indicator]]:bg-green-500" : ""}` } ), /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [ /* @__PURE__ */ jsxs("span", { children: [ stats.completed > 0 && `${stats.completed} completed`, stats.uploading > 0 && `, ${stats.uploading} uploading`, stats.failed > 0 && `, ${stats.failed} failed` ] }), /* @__PURE__ */ jsxs("span", { children: [ stats.totalProgress, "%" ] }) ] }) ] }) ] }), (!showOverallProgress || !isCollapsed) && /* @__PURE__ */ jsxs("div", { className: `${showOverallProgress ? "p-4" : "px-4 pb-4"}`, children: [ /* @__PURE__ */ jsx("div", { className: "space-y-3", children: filesToShow.map((fileProgress, index) => /* @__PURE__ */ jsx( FileProgressItem, { fileProgress, isCompact }, `${fileProgress.file.name}-${index}` )) }), shouldShowViewMore && /* @__PURE__ */ jsx("div", { className: "pt-2 border-t mt-3", children: /* @__PURE__ */ jsxs( Button, { variant: "ghost", size: "sm", className: "w-full h-6 text-xs transition-all duration-200 hover:bg-accent", onClick: () => setShowAll(!showAll), children: [ /* @__PURE__ */ jsx( ChevronDown, { className: `h-3 w-3 mr-1 transition-transform duration-200 ${showAll ? "rotate-180" : "rotate-0"}` } ), showAll ? "Show Less" : `View ${files.length - maxVisible} More Files` ] } ) }) ] }) ] }); } function FileProgressItem({ fileProgress, isCompact = false }) { const { file, progress, status, error } = fileProgress; if (isCompact) { return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs", children: [ /* @__PURE__ */ jsx( "div", { className: `w-2 h-2 rounded-full flex-shrink-0 ${status === "error" ? "bg-destructive" : status === "completed" ? "bg-green-500" : status === "canceled" ? "bg-muted-foreground" : "bg-primary animate-pulse"}` } ), /* @__PURE__ */ jsx("span", { className: "font-medium truncate flex-1", title: file.name, children: file.name }), /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: status === "completed" ? "\u2713" : status === "canceled" ? "\u2715" : `${Math.round(progress)}%` }) ] }), status === "error" && error && /* @__PURE__ */ jsx("div", { className: "text-xs text-destructive pl-4", children: error }), status === "canceled" && /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground pl-4", children: "Canceled" }) ] }); } return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm", children: [ /* @__PURE__ */ jsx("span", { className: "font-medium truncate flex-1", title: file.name, children: file.name }), /* @__PURE__ */ jsx("span", { className: "text-muted-foreground ml-2", children: formatFileSize2(file.size) }) ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [ /* @__PURE__ */ jsx( Progress, { value: Math.min(progress, 100), className: `h-2 transition-all duration-300 ${status === "error" ? "[&>[data-slot=progress-indicator]]:bg-destructive" : status === "completed" ? "[&>[data-slot=progress-indicator]]:bg-green-500" : status === "canceled" ? "[&>[data-slot=progress-indicator]]:bg-muted-foreground" : ""}` } ), /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between text-xs", children: /* @__PURE__ */ jsx( "span", { className: `${status === "error" ? "text-destructive" : status === "completed" ? "text-green-600" : status === "canceled" ? "text-muted-foreground" : "text-muted-foreground"}`, children: status === "error" && error ? error : status === "completed" ? "Completed" : status === "canceled" ? "Canceled" : `${Math.round(progress)}%` } ) }) ] }) ] }); } function FileUploader({ title, description, inputFields, allowedFileTypes = [], maxFileSizeBytes = 100 * 1024 * 1024, // 100MB default multiple = false, onSuccess, trigger, isProcessing = false }) { var _a; const [isOpen, setIsOpen] = useState(false); const [fieldValues, setFieldValues] = useState({}); const [selectedFiles, setSelectedFiles] = useState([]); const [fieldErrors, setFieldErrors] = useState({}); const [fileUrl, setFileUrl] = useState(""); const titleOrDefault = title || (multiple ? "Upload Files" : "Upload File"); const modalDescription = description || (multiple ? "Upload files and fill in the required information" : "Upload a file and fill in the required information"); const uploadProgress = useUploadProgress(); const { uploadAndReturn, uploadFromUrl } = useFileUpload({ onUploadStart: uploadProgress.startUpload, onProgress: uploadProgress.updateProgress, onUploadComplete: uploadProgress.completeUpload, onUploadError: uploadProgress.failUpload }); const handleClose = () => { setIsOpen(false); setFieldValues({}); setSelectedFiles([]); setFieldErrors({}); setFileUrl(""); }; const handleFieldChange = (key, value) => { setFieldValues((prev) => __spreadProps(__spreadValues({}, prev), { [key]: value })); if (fieldErrors[key]) { setFieldErrors((prev) => __spreadProps(__spreadValues({}, prev), { [key]: "" })); } }; const validateFields = () => { const errors = {}; inputFields == null ? void 0 : inputFields.forEach((field) => { const value = fieldValues[field.key] || ""; if (field.required && !value.trim()) { errors[field.key] = `${field.label} is required`; return; } if (field.validation && value.trim()) { const validationError = field.validation(value.trim()); if (validationError) { errors[field.key] = validationError; } } }); setFieldErrors(errors); return Object.keys(errors).length === 0; }; const handleFileSelect = (newFiles) => { const validFiles = []; newFiles.forEach((file) => { const validationError = validateFile( file, allowedFileTypes, maxFileSizeBytes ); if (!validationError) { validFiles.push(file); } }); if (validFiles.length > 0) { if (multiple) { setSelectedFiles((prev) => [...prev, ...validFiles]); } else { setSelectedFiles(validFiles.slice(0, 1)); } setFileUrl(""); } }; const handleContentChange = (content) => { if (content instanceof File) { handleFileSelect([content]); return; } if (typeof content === "string") { setSelectedFiles([]); setFileUrl(content); return; } setSelectedFiles([]); setFileUrl(""); }; const handleUpload = async () => { const hasFiles = selectedFiles.length > 0; const trimmedUrl = fileUrl.trim(); const currentFieldValues = __spreadValues({}, fieldValues); if (multiple) { if (!hasFiles) { return; } } else if (!hasFiles && trimmedUrl.length === 0) { return; } if (!validateFields()) { return; } const uploadFiles = async (files) => { handleClose(); try { const results = await Promise.all( files.map((file) => uploadAndReturn(file)) ); const successfulData = results.filter((result) => result.success && result.data).map((result) => result.data); if (successfulData.length > 0) { await onSuccess(successfulData, currentFieldValues); } } catch (error) { logger.error("FileUploader uploadFiles failed", { error, fileCount: files.length }); } }; if (!multiple && trimmedUrl.length > 0 && !hasFiles) { handleClose(); try { const result = await uploadFromUrl(trimmedUrl); if (result.success && result.data) { await onSuccess([result.data], __spreadProps(__spreadValues({}, currentFieldValues), { fileUrl: trimmedUrl })); } } catch (error) { logger.error("FileUploader uploadFromUrl failed", { error, url: trimmedUrl }); } return; } await uploadFiles(multiple ? selectedFiles : selectedFiles.slice(0, 1)); }; const removeFile = (fileToRemove) => { setSelectedFiles((prev) => prev.filter((file) => file !== fileToRemove)); }; const singleUploadContent = (_a = selectedFiles[0]) != null ? _a : fileUrl ? fileUrl : null; const canSubmit = () => { const requiredFieldsSatisfied = !inputFields || inputFields.length === 0 || inputFields.every( (field) => !field.required || fieldValues[field.key] && fieldValues[field.key].trim() ); if (!requiredFieldsSatisfied) { return false; } if (multiple) { return selectedFiles.length > 0; } return selectedFiles.length > 0 || fileUrl.trim().length > 0; }; return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: [ /* @__PURE__ */ jsx(DialogTrigger, { asChild: true, children: trigger || /* @__PURE__ */ jsxs(Button, { className: "cursor-pointer", disabled: isProcessing, children: [ isProcessing && /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin mr-2" }), /* @__PURE__ */ jsx(Upload, { className: "h-4 w-4" }), titleOrDefault ] }) }), /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [ /* @__PURE__ */ jsxs(DialogHeader, { children: [ /* @__PURE__ */ jsx(DialogTitle, { children: titleOrDefault }), /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: modalDescription }) ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-4 overflow-hidden", children: [ inputFields == null ? void 0 : inputFields.map((field) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsxs("label", { htmlFor: field.key, className: "text-sm font-medium", children: [ field.label, field.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-1", children: "*" }) ] }), /* @__PURE__ */ jsx( Input, { id: field.key, value: fieldValues[field.key] || "", onChange: (e) => handleFieldChange(field.key, e.target.value), placeholder: field.placeholder, className: fieldErrors[field.key] ? "border-destructive" : "" } ), fieldErrors[field.key] && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: fieldErrors[field.key] }) ] }, field.key)), /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [ /* @__PURE__ */ jsxs("label", { className: "text-sm font-medium", children: [ multiple ? "Files" : "File", " ", /* @__PURE__ */ jsx("span", { className: "text-destructive ml-1", children: "*" }) ] }), multiple ? /* @__PURE__ */ jsx( FileDropzone, { multiple: true, selectedFiles, onFilesSelected: handleFileSelect, onRemoveFile: removeFile, allowedFileTypes, maxFileSizeBytes, listFooter: /* @__PURE__ */ jsx("div", { className: "border-t border-muted-foreground/20 pt-2 text-xs text-muted-foreground", children: "Click to add more files or drag and drop" }) } ) : /* @__PURE__ */ jsx( FileUpload, { className: "mt-0", heading: titleOrDefault, content: singleUploadContent, onContentChange: handleContentChange, allowFileRemoval: true, showHeader: false, allowedFileTypes, maxFileSizeBytes, disableWhenHasSelection: true, footer: null } ) ] }) ] }), /* @__PURE__ */ jsxs(DialogFooter, { children: [ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleClose, children: "Cancel" }), /* @__PURE__ */ jsx(Button, { onClick: handleUpload, disabled: !canSubmit(), children: selectedFiles.length > 1 ? `Upload ${selectedFiles.length} Files & Process` : "Upload & Process" }) ] }) ] }) ] }), /* @__PURE__ */ jsx( UploadProgress, { files: uploadProgress.uploadProgressFiles, onClose: uploadProgress.clearAllUploads } ) ] }); } function isModalVariant(props) { return props.variant === "modal"; } function ManagedFileUpload(props) { if (isModalVariant(props)) { const modalProps = ((_a) => { var _b = _a, { variant: _variant } = _b, rest = __objRest(_b, ["variant"]); return rest; })(props); return /* @__PURE__ */ jsx(FileUploader, __spreadValues({}, modalProps)); } const inlineProps = ((_c) => { var _d = _c, { variant: _variant } = _d, rest = __objRest(_d, ["variant"]); return rest; })(props); return /* @__PURE__ */ jsx(FileUpload, __spreadValues({}, inlineProps)); } export { FILE_TYPE_GROUPS, FileDropzone, FileType, FileUpload, FileUploader, ManagedFileUpload, PROGRESS_THRESHOLD, UploadProgress, addUploadToQueue, calculateUploadStats, cancelAllUploads, cancelFileUpload, completeFileUpload, createFileTypeValidator, failFileUpload, formatFileSize2 as formatFileSize, getDisplayModes, getFileExtensions, getFileMimeTypes, getFileTypeDefinition, getFileTypesByCategory, getVisibleFiles, hasActiveUploads, isCryptoSupported, isFileApiSupported, isFileTypeMatch, logger, removeCompletedUploads, removeFileUpload, updateFileProgress, useFileDropzone, useFileUpload, useUploadProgress, validateFile };