@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
JavaScript
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 };