2025-11-07 14:28:30 -08:00
|
|
|
"use client";
|
|
|
|
|
|
2025-12-04 12:37:12 +00:00
|
|
|
import { useAtom } from "jotai";
|
2026-04-14 21:26:00 -07:00
|
|
|
import {
|
|
|
|
|
ChevronDown,
|
|
|
|
|
Crown,
|
|
|
|
|
Dot,
|
|
|
|
|
File as FileIcon,
|
|
|
|
|
FolderOpen,
|
|
|
|
|
Upload,
|
|
|
|
|
X,
|
|
|
|
|
Zap,
|
|
|
|
|
} from "lucide-react";
|
2026-03-07 12:31:55 +05:30
|
|
|
|
2025-11-07 14:28:30 -08:00
|
|
|
import { useTranslations } from "next-intl";
|
2026-04-03 00:05:06 -07:00
|
|
|
import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2025-11-07 14:28:30 -08:00
|
|
|
import { useDropzone } from "react-dropzone";
|
|
|
|
|
import { toast } from "sonner";
|
2025-12-04 12:37:12 +00:00
|
|
|
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
2026-01-02 04:10:37 +05:30
|
|
|
import {
|
|
|
|
|
Accordion,
|
|
|
|
|
AccordionContent,
|
|
|
|
|
AccordionItem,
|
|
|
|
|
AccordionTrigger,
|
|
|
|
|
} from "@/components/ui/accordion";
|
2025-11-07 14:28:30 -08:00
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-04-03 02:56:24 +05:30
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
2025-11-07 14:28:30 -08:00
|
|
|
import { Progress } from "@/components/ui/progress";
|
2026-01-26 23:32:30 -08:00
|
|
|
import { Spinner } from "@/components/ui/spinner";
|
2026-04-03 00:28:24 +05:30
|
|
|
import { Switch } from "@/components/ui/switch";
|
2026-04-14 21:26:00 -07:00
|
|
|
import type { ProcessingMode } from "@/contracts/types/document.types";
|
2026-04-07 00:43:40 -07:00
|
|
|
import { useElectronAPI } from "@/hooks/use-platform";
|
2026-04-09 11:18:56 +02:00
|
|
|
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
2025-12-25 13:53:41 -08:00
|
|
|
import {
|
|
|
|
|
trackDocumentUploadFailure,
|
|
|
|
|
trackDocumentUploadStarted,
|
|
|
|
|
trackDocumentUploadSuccess,
|
|
|
|
|
} from "@/lib/posthog/events";
|
2026-04-08 05:20:03 +05:30
|
|
|
import {
|
|
|
|
|
getAcceptedFileTypes,
|
|
|
|
|
getSupportedExtensions,
|
|
|
|
|
getSupportedExtensionsSet,
|
|
|
|
|
} from "@/lib/supported-extensions";
|
2025-11-07 14:28:30 -08:00
|
|
|
|
|
|
|
|
interface DocumentUploadTabProps {
|
|
|
|
|
searchSpaceId: string;
|
2026-01-02 04:07:13 +05:30
|
|
|
onSuccess?: () => void;
|
2026-01-02 16:51:37 +05:30
|
|
|
onAccordionStateChange?: (isExpanded: boolean) => void;
|
2025-11-07 14:28:30 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 19:48:38 +05:30
|
|
|
interface FileWithId {
|
|
|
|
|
id: string;
|
|
|
|
|
file: File;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
interface FolderEntry {
|
|
|
|
|
id: string;
|
|
|
|
|
file: File;
|
|
|
|
|
relativePath: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FolderUploadData {
|
|
|
|
|
folderName: string;
|
|
|
|
|
entries: FolderEntry[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FolderTreeNode {
|
|
|
|
|
name: string;
|
|
|
|
|
isFolder: boolean;
|
|
|
|
|
size?: number;
|
|
|
|
|
children: FolderTreeNode[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildFolderTree(entries: FolderEntry[]): FolderTreeNode[] {
|
|
|
|
|
const root: FolderTreeNode = { name: "", isFolder: true, children: [] };
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const parts = entry.relativePath.split("/");
|
|
|
|
|
let current = root;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
|
|
|
let child = current.children.find((c) => c.name === parts[i] && c.isFolder);
|
|
|
|
|
if (!child) {
|
|
|
|
|
child = { name: parts[i], isFolder: true, children: [] };
|
|
|
|
|
current.children.push(child);
|
|
|
|
|
}
|
|
|
|
|
current = child;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current.children.push({
|
|
|
|
|
name: parts[parts.length - 1],
|
|
|
|
|
isFolder: false,
|
|
|
|
|
size: entry.file.size,
|
|
|
|
|
children: [],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sortNodes(node: FolderTreeNode) {
|
|
|
|
|
node.children.sort((a, b) => {
|
|
|
|
|
if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1;
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
|
|
|
|
for (const child of node.children) sortNodes(child);
|
|
|
|
|
}
|
|
|
|
|
sortNodes(root);
|
|
|
|
|
|
|
|
|
|
return root.children;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function flattenTree(
|
|
|
|
|
nodes: FolderTreeNode[],
|
|
|
|
|
depth = 0
|
|
|
|
|
): { name: string; isFolder: boolean; depth: number; size?: number }[] {
|
|
|
|
|
const items: { name: string; isFolder: boolean; depth: number; size?: number }[] = [];
|
|
|
|
|
for (const node of nodes) {
|
|
|
|
|
items.push({ name: node.name, isFolder: node.isFolder, depth, size: node.size });
|
|
|
|
|
if (node.isFolder && node.children.length > 0) {
|
|
|
|
|
items.push(...flattenTree(node.children, depth + 1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FOLDER_BATCH_SIZE_BYTES = 20 * 1024 * 1024;
|
|
|
|
|
const FOLDER_BATCH_MAX_FILES = 10;
|
|
|
|
|
|
2026-04-02 19:39:10 -07:00
|
|
|
const MAX_FILE_SIZE_MB = 500;
|
|
|
|
|
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
2026-01-15 00:01:00 -08:00
|
|
|
|
2026-04-03 13:14:40 +05:30
|
|
|
const toggleRowClass =
|
|
|
|
|
"flex items-center justify-between rounded-lg bg-slate-400/5 dark:bg-white/5 p-3";
|
2026-04-03 04:14:09 +05:30
|
|
|
|
2026-01-03 00:18:17 +05:30
|
|
|
export function DocumentUploadTab({
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
onSuccess,
|
|
|
|
|
onAccordionStateChange,
|
|
|
|
|
}: DocumentUploadTabProps) {
|
2025-11-07 14:28:30 -08:00
|
|
|
const t = useTranslations("upload_documents");
|
2026-03-08 19:48:38 +05:30
|
|
|
const [files, setFiles] = useState<FileWithId[]>([]);
|
2025-11-07 14:28:30 -08:00
|
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
2026-01-02 16:51:37 +05:30
|
|
|
const [accordionValue, setAccordionValue] = useState<string>("");
|
2026-02-26 18:24:57 -08:00
|
|
|
const [shouldSummarize, setShouldSummarize] = useState(false);
|
2026-04-10 16:45:51 +02:00
|
|
|
const [useVisionLlm, setUseVisionLlm] = useState(false);
|
2026-04-14 21:26:00 -07:00
|
|
|
const [processingMode, setProcessingMode] = useState<ProcessingMode>("basic");
|
2025-12-04 12:37:12 +00:00
|
|
|
const [uploadDocumentMutation] = useAtom(uploadDocumentMutationAtom);
|
|
|
|
|
const { mutate: uploadDocuments, isPending: isUploading } = uploadDocumentMutation;
|
2026-01-02 04:07:13 +05:30
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
2026-04-02 19:39:10 -07:00
|
|
|
const folderInputRef = useRef<HTMLInputElement>(null);
|
2026-04-03 00:05:06 -07:00
|
|
|
const progressIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
2026-04-09 11:18:56 +02:00
|
|
|
const [folderUpload, setFolderUpload] = useState<FolderUploadData | null>(null);
|
|
|
|
|
const [isFolderUploading, setIsFolderUploading] = useState(false);
|
2026-04-03 00:05:06 -07:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (progressIntervalRef.current) {
|
|
|
|
|
clearInterval(progressIntervalRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
2025-12-04 12:37:12 +00:00
|
|
|
|
2026-04-07 00:43:40 -07:00
|
|
|
const electronAPI = useElectronAPI();
|
|
|
|
|
const isElectron = !!electronAPI?.browseFiles;
|
2026-04-03 00:28:24 +05:30
|
|
|
|
2026-04-08 04:11:49 +05:30
|
|
|
const acceptedFileTypes = useMemo(() => getAcceptedFileTypes(), []);
|
2026-01-02 04:07:13 +05:30
|
|
|
const supportedExtensions = useMemo(
|
2026-04-08 04:11:49 +05:30
|
|
|
() => getSupportedExtensions(acceptedFileTypes),
|
2026-01-02 04:07:13 +05:30
|
|
|
[acceptedFileTypes]
|
|
|
|
|
);
|
2026-04-02 19:39:10 -07:00
|
|
|
const supportedExtensionsSet = useMemo(
|
2026-04-08 04:11:49 +05:30
|
|
|
() => getSupportedExtensionsSet(acceptedFileTypes),
|
|
|
|
|
[acceptedFileTypes]
|
2026-04-02 19:39:10 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const addFiles = useCallback(
|
|
|
|
|
(incoming: File[]) => {
|
|
|
|
|
const oversized = incoming.filter((f) => f.size > MAX_FILE_SIZE_BYTES);
|
|
|
|
|
if (oversized.length > 0) {
|
|
|
|
|
toast.error(t("file_too_large"), {
|
|
|
|
|
description: t("file_too_large_desc", {
|
|
|
|
|
name: oversized[0].name,
|
|
|
|
|
maxMB: MAX_FILE_SIZE_MB,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const valid = incoming.filter((f) => f.size <= MAX_FILE_SIZE_BYTES);
|
|
|
|
|
if (valid.length === 0) return;
|
|
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
setFolderUpload(null);
|
2026-01-15 00:05:53 -08:00
|
|
|
setFiles((prev) => {
|
2026-04-02 19:39:10 -07:00
|
|
|
const newEntries = valid.map((f) => ({
|
2026-03-08 21:11:54 +05:30
|
|
|
id: crypto.randomUUID?.() ?? `file-${Date.now()}-${Math.random().toString(36)}`,
|
|
|
|
|
file: f,
|
|
|
|
|
}));
|
2026-04-02 19:39:10 -07:00
|
|
|
return [...prev, ...newEntries];
|
2026-01-15 00:05:53 -08:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[t]
|
|
|
|
|
);
|
2025-11-07 14:28:30 -08:00
|
|
|
|
2026-04-02 19:39:10 -07:00
|
|
|
const onDrop = useCallback(
|
|
|
|
|
(acceptedFiles: File[]) => {
|
|
|
|
|
addFiles(acceptedFiles);
|
|
|
|
|
},
|
|
|
|
|
[addFiles]
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-07 14:28:30 -08:00
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
|
|
|
onDrop,
|
|
|
|
|
accept: acceptedFileTypes,
|
2026-04-02 19:39:10 -07:00
|
|
|
maxSize: MAX_FILE_SIZE_BYTES,
|
2026-04-03 02:56:24 +05:30
|
|
|
noClick: isElectron,
|
2025-11-07 14:28:30 -08:00
|
|
|
});
|
|
|
|
|
|
2026-01-02 04:07:13 +05:30
|
|
|
const handleFileInputClick = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}, []);
|
2025-11-07 14:28:30 -08:00
|
|
|
|
2026-04-03 02:56:24 +05:30
|
|
|
const handleBrowseFiles = useCallback(async () => {
|
2026-04-07 00:43:40 -07:00
|
|
|
if (!electronAPI?.browseFiles) return;
|
2026-04-03 02:56:24 +05:30
|
|
|
|
2026-04-07 00:43:40 -07:00
|
|
|
const paths = await electronAPI.browseFiles();
|
2026-04-03 02:56:24 +05:30
|
|
|
if (!paths || paths.length === 0) return;
|
|
|
|
|
|
2026-04-07 00:43:40 -07:00
|
|
|
const fileDataList = await electronAPI.readLocalFiles(paths);
|
2026-04-08 05:20:03 +05:30
|
|
|
const filtered = fileDataList.filter(
|
|
|
|
|
(fd: { name: string; data: ArrayBuffer; mimeType: string }) => {
|
|
|
|
|
const ext = fd.name.includes(".") ? `.${fd.name.split(".").pop()?.toLowerCase()}` : "";
|
|
|
|
|
return ext !== "" && supportedExtensionsSet.has(ext);
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-04-08 04:11:49 +05:30
|
|
|
|
|
|
|
|
if (filtered.length === 0) {
|
|
|
|
|
toast.error(t("no_supported_files_in_folder"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 05:20:03 +05:30
|
|
|
const newFiles: FileWithId[] = filtered.map(
|
|
|
|
|
(fd: { name: string; data: ArrayBuffer; mimeType: string }) => ({
|
|
|
|
|
id: crypto.randomUUID?.() ?? `file-${Date.now()}-${Math.random().toString(36)}`,
|
|
|
|
|
file: new File([fd.data], fd.name, { type: fd.mimeType }),
|
|
|
|
|
})
|
|
|
|
|
);
|
2026-04-09 11:18:56 +02:00
|
|
|
setFolderUpload(null);
|
2026-04-03 17:24:06 +05:30
|
|
|
setFiles((prev) => [...prev, ...newFiles]);
|
2026-04-08 05:00:32 +05:30
|
|
|
}, [electronAPI, supportedExtensionsSet, t]);
|
2026-04-03 00:28:24 +05:30
|
|
|
|
2026-04-02 19:39:10 -07:00
|
|
|
const handleFolderChange = useCallback(
|
|
|
|
|
(e: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const fileList = e.target.files;
|
|
|
|
|
if (!fileList || fileList.length === 0) return;
|
|
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
const allFiles = Array.from(fileList);
|
|
|
|
|
const firstPath = allFiles[0]?.webkitRelativePath || "";
|
|
|
|
|
const folderName = firstPath.split("/")[0];
|
|
|
|
|
|
|
|
|
|
if (!folderName) {
|
|
|
|
|
addFiles(allFiles);
|
|
|
|
|
e.target.value = "";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const entries: FolderEntry[] = allFiles
|
|
|
|
|
.filter((f) => {
|
|
|
|
|
const ext = f.name.includes(".") ? `.${f.name.split(".").pop()?.toLowerCase()}` : "";
|
|
|
|
|
return ext !== "" && supportedExtensionsSet.has(ext);
|
|
|
|
|
})
|
|
|
|
|
.map((f) => ({
|
|
|
|
|
id: crypto.randomUUID?.() ?? `file-${Date.now()}-${Math.random().toString(36)}`,
|
|
|
|
|
file: f,
|
|
|
|
|
relativePath: f.webkitRelativePath.substring(folderName.length + 1),
|
|
|
|
|
}));
|
2026-04-02 19:39:10 -07:00
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
if (entries.length === 0) {
|
2026-04-02 19:39:10 -07:00
|
|
|
toast.error(t("no_supported_files_in_folder"));
|
|
|
|
|
e.target.value = "";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
setFiles([]);
|
|
|
|
|
setFolderUpload({ folderName, entries });
|
2026-04-02 19:39:10 -07:00
|
|
|
e.target.value = "";
|
|
|
|
|
},
|
|
|
|
|
[addFiles, supportedExtensionsSet, t]
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-07 14:28:30 -08:00
|
|
|
const formatFileSize = (bytes: number) => {
|
|
|
|
|
if (bytes === 0) return "0 Bytes";
|
|
|
|
|
const k = 1024;
|
|
|
|
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
const totalFileSize = folderUpload
|
|
|
|
|
? folderUpload.entries.reduce((total, entry) => total + entry.file.size, 0)
|
|
|
|
|
: files.reduce((total, entry) => total + entry.file.size, 0);
|
2026-01-02 04:07:13 +05:30
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
const fileCount = folderUpload ? folderUpload.entries.length : files.length;
|
|
|
|
|
const hasContent = files.length > 0 || folderUpload !== null;
|
|
|
|
|
const isAnyUploading = isUploading || isFolderUploading;
|
|
|
|
|
|
|
|
|
|
const folderTreeItems = useMemo(() => {
|
|
|
|
|
if (!folderUpload) return [];
|
|
|
|
|
return flattenTree(buildFolderTree(folderUpload.entries));
|
|
|
|
|
}, [folderUpload]);
|
2026-04-03 04:14:09 +05:30
|
|
|
|
2026-01-03 00:18:17 +05:30
|
|
|
const handleAccordionChange = useCallback(
|
|
|
|
|
(value: string) => {
|
|
|
|
|
setAccordionValue(value);
|
|
|
|
|
onAccordionStateChange?.(value === "supported-file-types");
|
|
|
|
|
},
|
|
|
|
|
[onAccordionStateChange]
|
|
|
|
|
);
|
2026-01-02 16:51:37 +05:30
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
const handleFolderUpload = async () => {
|
|
|
|
|
if (!folderUpload) return;
|
|
|
|
|
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
setIsFolderUploading(true);
|
|
|
|
|
const total = folderUpload.entries.length;
|
|
|
|
|
trackDocumentUploadStarted(Number(searchSpaceId), total, totalFileSize);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const batches: FolderEntry[][] = [];
|
|
|
|
|
let currentBatch: FolderEntry[] = [];
|
|
|
|
|
let currentSize = 0;
|
|
|
|
|
|
|
|
|
|
for (const entry of folderUpload.entries) {
|
|
|
|
|
const size = entry.file.size;
|
|
|
|
|
|
|
|
|
|
if (size >= FOLDER_BATCH_SIZE_BYTES) {
|
|
|
|
|
if (currentBatch.length > 0) {
|
|
|
|
|
batches.push(currentBatch);
|
|
|
|
|
currentBatch = [];
|
|
|
|
|
currentSize = 0;
|
|
|
|
|
}
|
|
|
|
|
batches.push([entry]);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
currentBatch.length >= FOLDER_BATCH_MAX_FILES ||
|
|
|
|
|
currentSize + size > FOLDER_BATCH_SIZE_BYTES
|
|
|
|
|
) {
|
|
|
|
|
batches.push(currentBatch);
|
|
|
|
|
currentBatch = [];
|
|
|
|
|
currentSize = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentBatch.push(entry);
|
|
|
|
|
currentSize += size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentBatch.length > 0) {
|
|
|
|
|
batches.push(currentBatch);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let rootFolderId: number | null = null;
|
|
|
|
|
let uploaded = 0;
|
|
|
|
|
|
|
|
|
|
for (const batch of batches) {
|
|
|
|
|
const result = await documentsApiService.folderUploadFiles(
|
|
|
|
|
batch.map((e) => e.file),
|
|
|
|
|
{
|
|
|
|
|
folder_name: folderUpload.folderName,
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
|
|
|
|
relative_paths: batch.map((e) => e.relativePath),
|
|
|
|
|
root_folder_id: rootFolderId,
|
|
|
|
|
enable_summary: shouldSummarize,
|
2026-04-10 16:45:51 +02:00
|
|
|
use_vision_llm: useVisionLlm,
|
2026-04-14 21:26:00 -07:00
|
|
|
processing_mode: processingMode,
|
2026-04-09 11:18:56 +02:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result.root_folder_id && !rootFolderId) {
|
|
|
|
|
rootFolderId = result.root_folder_id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uploaded += batch.length;
|
|
|
|
|
setUploadProgress(Math.round((uploaded / total) * 100));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackDocumentUploadSuccess(Number(searchSpaceId), total);
|
|
|
|
|
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
|
|
|
|
|
setFolderUpload(null);
|
|
|
|
|
onSuccess?.();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error instanceof Error ? error.message : "Upload failed";
|
|
|
|
|
trackDocumentUploadFailure(Number(searchSpaceId), message);
|
|
|
|
|
toast(t("upload_error"), {
|
|
|
|
|
description: `${t("upload_error_desc")}: ${message}`,
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setIsFolderUploading(false);
|
|
|
|
|
setUploadProgress(0);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-07 14:28:30 -08:00
|
|
|
const handleUpload = async () => {
|
2026-04-09 11:18:56 +02:00
|
|
|
if (folderUpload) {
|
|
|
|
|
await handleFolderUpload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 14:28:30 -08:00
|
|
|
setUploadProgress(0);
|
2026-01-02 04:07:13 +05:30
|
|
|
trackDocumentUploadStarted(Number(searchSpaceId), files.length, totalFileSize);
|
2025-11-07 14:28:30 -08:00
|
|
|
|
2026-04-03 00:05:06 -07:00
|
|
|
progressIntervalRef.current = setInterval(() => {
|
2026-01-02 04:07:13 +05:30
|
|
|
setUploadProgress((prev) => (prev >= 90 ? prev : prev + Math.random() * 10));
|
2025-12-04 12:37:12 +00:00
|
|
|
}, 200);
|
|
|
|
|
|
2026-03-08 19:48:38 +05:30
|
|
|
const rawFiles = files.map((entry) => entry.file);
|
2025-12-04 12:37:12 +00:00
|
|
|
uploadDocuments(
|
2026-03-08 20:57:29 +05:30
|
|
|
{
|
|
|
|
|
files: rawFiles,
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
|
|
|
|
should_summarize: shouldSummarize,
|
2026-04-10 16:45:51 +02:00
|
|
|
use_vision_llm: useVisionLlm,
|
2026-04-14 21:26:00 -07:00
|
|
|
processing_mode: processingMode,
|
2026-03-08 20:57:29 +05:30
|
|
|
},
|
2025-12-04 12:37:12 +00:00
|
|
|
{
|
|
|
|
|
onSuccess: () => {
|
2026-04-03 00:05:06 -07:00
|
|
|
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
|
2025-12-04 12:37:12 +00:00
|
|
|
setUploadProgress(100);
|
2025-12-25 13:53:41 -08:00
|
|
|
trackDocumentUploadSuccess(Number(searchSpaceId), files.length);
|
2026-01-02 04:07:13 +05:30
|
|
|
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
|
2026-01-16 11:32:06 -08:00
|
|
|
onSuccess?.();
|
2025-12-04 12:37:12 +00:00
|
|
|
},
|
2026-01-02 04:07:13 +05:30
|
|
|
onError: (error: unknown) => {
|
2026-04-03 00:05:06 -07:00
|
|
|
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
|
2025-12-04 12:37:12 +00:00
|
|
|
setUploadProgress(0);
|
2026-01-02 04:07:13 +05:30
|
|
|
const message = error instanceof Error ? error.message : "Upload failed";
|
|
|
|
|
trackDocumentUploadFailure(Number(searchSpaceId), message);
|
2025-12-04 12:37:12 +00:00
|
|
|
toast(t("upload_error"), {
|
2026-01-02 04:07:13 +05:30
|
|
|
description: `${t("upload_error_desc")}: ${message}`,
|
2025-12-04 12:37:12 +00:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-11-07 14:28:30 -08:00
|
|
|
};
|
|
|
|
|
|
2026-04-03 04:14:09 +05:30
|
|
|
const renderBrowseButton = (options?: { compact?: boolean; fullWidth?: boolean }) => {
|
|
|
|
|
const { compact, fullWidth } = options ?? {};
|
|
|
|
|
const sizeClass = compact ? "h-7" : "h-8";
|
|
|
|
|
const widthClass = fullWidth ? "w-full" : "";
|
|
|
|
|
|
|
|
|
|
if (isElectron) {
|
|
|
|
|
return (
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
2026-04-03 13:14:40 +05:30
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className={`text-xs gap-1 bg-neutral-700/50 hover:bg-neutral-600/50 ${sizeClass} ${widthClass}`}
|
|
|
|
|
>
|
2026-04-03 04:14:09 +05:30
|
|
|
Browse
|
|
|
|
|
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
2026-04-03 13:14:40 +05:30
|
|
|
<DropdownMenuContent
|
|
|
|
|
align="center"
|
|
|
|
|
className="dark:bg-neutral-800"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
2026-04-08 05:20:03 +05:30
|
|
|
<DropdownMenuItem onClick={handleBrowseFiles}>
|
|
|
|
|
<FileIcon className="h-4 w-4 mr-2" />
|
|
|
|
|
Files
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem onClick={() => folderInputRef.current?.click()}>
|
|
|
|
|
<FolderOpen className="h-4 w-4 mr-2" />
|
|
|
|
|
Folder
|
|
|
|
|
</DropdownMenuItem>
|
2026-04-03 04:14:09 +05:30
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-03 12:33:47 +05:30
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
2026-04-03 13:14:40 +05:30
|
|
|
<Button
|
2026-04-03 17:52:59 +05:30
|
|
|
variant="ghost"
|
2026-04-03 13:14:40 +05:30
|
|
|
size="sm"
|
2026-04-03 17:52:59 +05:30
|
|
|
className={`text-xs gap-1 bg-neutral-700/50 hover:bg-neutral-600/50 ${sizeClass} ${widthClass}`}
|
2026-04-03 13:14:40 +05:30
|
|
|
>
|
2026-04-03 12:33:47 +05:30
|
|
|
Browse
|
|
|
|
|
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
2026-04-03 17:52:59 +05:30
|
|
|
<DropdownMenuContent
|
|
|
|
|
align="center"
|
|
|
|
|
className="dark:bg-neutral-800"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
2026-04-03 12:33:47 +05:30
|
|
|
<DropdownMenuItem onClick={() => fileInputRef.current?.click()}>
|
|
|
|
|
<FileIcon className="h-4 w-4 mr-2" />
|
|
|
|
|
{t("browse_files")}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem onClick={() => folderInputRef.current?.click()}>
|
|
|
|
|
<FolderOpen className="h-4 w-4 mr-2" />
|
|
|
|
|
{t("browse_folder")}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
2026-04-03 04:14:09 +05:30
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-07 14:28:30 -08:00
|
|
|
return (
|
2026-04-03 04:14:09 +05:30
|
|
|
<div className="space-y-2 w-full mx-auto">
|
2026-04-03 11:42:43 +05:30
|
|
|
{/* Hidden file input */}
|
2026-04-03 04:14:09 +05:30
|
|
|
<input
|
|
|
|
|
{...getInputProps()}
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
className="hidden"
|
|
|
|
|
onClick={handleFileInputClick}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-04-03 11:42:43 +05:30
|
|
|
{/* Hidden folder input for web folder browsing */}
|
2026-04-02 19:39:10 -07:00
|
|
|
<input
|
|
|
|
|
ref={folderInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
className="hidden"
|
|
|
|
|
onChange={handleFolderChange}
|
|
|
|
|
multiple
|
|
|
|
|
{...({ webkitdirectory: "", directory: "" } as React.InputHTMLAttributes<HTMLInputElement>)}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-04-03 04:14:09 +05:30
|
|
|
{/* MOBILE DROP ZONE */}
|
|
|
|
|
<div className="sm:hidden">
|
2026-04-08 05:20:03 +05:30
|
|
|
{hasContent ? (
|
|
|
|
|
isElectron ? (
|
2026-04-03 13:14:40 +05:30
|
|
|
<div className="w-full">{renderBrowseButton({ compact: true, fullWidth: true })}</div>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="w-full text-xs h-8 flex items-center justify-center gap-1.5 rounded-md border border-dashed border-muted-foreground/30 text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
>
|
|
|
|
|
Add more files
|
|
|
|
|
</button>
|
2026-04-08 04:11:49 +05:30
|
|
|
)
|
2026-04-03 04:14:09 +05:30
|
|
|
) : (
|
2026-04-16 00:25:46 -07:00
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
className="flex flex-col items-center gap-4 py-12 px-4 cursor-pointer w-full bg-transparent outline-none select-none"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!isElectron) fileInputRef.current?.click();
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
|
|
|
e.preventDefault();
|
2026-04-07 05:55:39 +05:30
|
|
|
if (!isElectron) fileInputRef.current?.click();
|
2026-04-16 00:25:46 -07:00
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Upload className="h-10 w-10 text-muted-foreground" />
|
|
|
|
|
<div className="text-center space-y-1.5">
|
|
|
|
|
<p className="text-base font-medium">
|
|
|
|
|
{isElectron ? t("select_files_or_folder") : t("tap_select_files_or_folder")}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">{t("file_size_limit")}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<fieldset
|
|
|
|
|
className="w-full mt-1 border-none p-0 m-0"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
onKeyDown={(e) => e.stopPropagation()}
|
2026-04-07 05:55:39 +05:30
|
|
|
>
|
2026-04-16 00:25:46 -07:00
|
|
|
{renderBrowseButton({ fullWidth: true })}
|
|
|
|
|
</fieldset>
|
|
|
|
|
</div>
|
2026-04-03 02:56:24 +05:30
|
|
|
)}
|
2026-04-03 04:14:09 +05:30
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* DESKTOP DROP ZONE */}
|
|
|
|
|
<div
|
|
|
|
|
{...getRootProps()}
|
2026-04-03 17:24:06 +05:30
|
|
|
className={`hidden sm:block border-2 border-dashed rounded-lg transition-colors border-muted-foreground/30 hover:border-foreground/70 cursor-pointer ${hasContent ? "p-3" : "py-20 px-4"}`}
|
2026-04-03 04:14:09 +05:30
|
|
|
>
|
|
|
|
|
{hasContent ? (
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Upload className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
|
|
|
<span className="text-xs text-muted-foreground flex-1 truncate">
|
2026-04-03 17:24:34 +05:30
|
|
|
{isDragActive ? t("drop_files") : t("drag_drop_more")}
|
2026-04-03 04:14:09 +05:30
|
|
|
</span>
|
|
|
|
|
{renderBrowseButton({ compact: true })}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-04-03 17:52:59 +05:30
|
|
|
<div className="relative">
|
|
|
|
|
{isDragActive && (
|
|
|
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
|
|
|
|
<Upload className="h-8 w-8 text-primary" />
|
|
|
|
|
<p className="text-sm font-medium text-primary">{t("drop_files")}</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className={`flex flex-col items-center gap-2 ${isDragActive ? "invisible" : ""}`}>
|
|
|
|
|
<Upload className="h-8 w-8 text-muted-foreground" />
|
|
|
|
|
<p className="text-sm font-medium">{t("drag_drop")}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{t("file_size_limit")}</p>
|
|
|
|
|
<div className="mt-1">{renderBrowseButton()}</div>
|
|
|
|
|
</div>
|
2026-04-03 04:14:09 +05:30
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* FILES SELECTED */}
|
2026-04-09 11:18:56 +02:00
|
|
|
{hasContent && (
|
2026-04-03 04:14:09 +05:30
|
|
|
<div className="rounded-lg border border-border p-3 space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<p className="text-sm font-medium">
|
2026-04-09 11:18:56 +02:00
|
|
|
{folderUpload ? (
|
|
|
|
|
<>
|
|
|
|
|
<FolderOpen className="inline h-4 w-4 mr-1 -mt-0.5" />
|
|
|
|
|
{folderUpload.folderName}
|
|
|
|
|
<Dot className="inline h-4 w-4" />
|
|
|
|
|
{folderUpload.entries.length}{" "}
|
|
|
|
|
{folderUpload.entries.length === 1 ? "file" : "files"}
|
|
|
|
|
<Dot className="inline h-4 w-4" />
|
|
|
|
|
{formatFileSize(totalFileSize)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{t("selected_files", { count: files.length })}
|
|
|
|
|
<Dot className="inline h-4 w-4" />
|
|
|
|
|
{formatFileSize(totalFileSize)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-04-03 04:14:09 +05:30
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 text-xs text-muted-foreground hover:text-foreground"
|
2026-04-09 11:18:56 +02:00
|
|
|
onClick={() => {
|
|
|
|
|
setFiles([]);
|
|
|
|
|
setFolderUpload(null);
|
|
|
|
|
}}
|
|
|
|
|
disabled={isAnyUploading}
|
2026-04-03 04:14:09 +05:30
|
|
|
>
|
|
|
|
|
{t("clear_all")}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="max-h-[160px] sm:max-h-[200px] overflow-y-auto -mx-1">
|
2026-04-09 11:18:56 +02:00
|
|
|
{folderUpload
|
|
|
|
|
? folderTreeItems.map((item, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={`${item.depth}-${i}-${item.name}`}
|
|
|
|
|
className="flex items-center gap-1.5 py-0.5 px-2"
|
|
|
|
|
style={{ paddingLeft: `${item.depth * 16 + 8}px` }}
|
|
|
|
|
>
|
|
|
|
|
{item.isFolder ? (
|
|
|
|
|
<FolderOpen className="h-3.5 w-3.5 text-blue-400 shrink-0" />
|
|
|
|
|
) : (
|
|
|
|
|
<FileIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-sm truncate flex-1 min-w-0">{item.name}</span>
|
|
|
|
|
{!item.isFolder && item.size != null && (
|
|
|
|
|
<span className="text-xs text-muted-foreground shrink-0">
|
|
|
|
|
{formatFileSize(item.size)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
: files.map((entry) => (
|
|
|
|
|
<div
|
|
|
|
|
key={entry.id}
|
|
|
|
|
className="flex items-center gap-2 py-1.5 px-2 rounded-md hover:bg-slate-400/5 dark:hover:bg-white/5 group"
|
|
|
|
|
>
|
|
|
|
|
<span className="text-[10px] font-medium uppercase leading-none bg-muted px-1.5 py-0.5 rounded text-muted-foreground shrink-0">
|
|
|
|
|
{entry.file.name.split(".").pop() || "?"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-sm truncate flex-1 min-w-0">{entry.file.name}</span>
|
|
|
|
|
<span className="text-xs text-muted-foreground shrink-0">
|
|
|
|
|
{formatFileSize(entry.file.size)}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-6 w-6 shrink-0"
|
|
|
|
|
onClick={() => setFiles((prev) => prev.filter((e) => e.id !== entry.id))}
|
|
|
|
|
disabled={isAnyUploading}
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-04-03 04:14:09 +05:30
|
|
|
</div>
|
2026-02-26 18:24:57 -08:00
|
|
|
|
2026-04-09 11:18:56 +02:00
|
|
|
{isAnyUploading && (
|
2026-04-03 04:14:09 +05:30
|
|
|
<div className="space-y-1">
|
|
|
|
|
<div className="flex items-center justify-between text-xs">
|
2026-04-09 13:42:57 +02:00
|
|
|
<span>{folderUpload ? t("uploading_folder") : t("uploading_files")}</span>
|
2026-04-03 04:14:09 +05:30
|
|
|
<span>{Math.round(uploadProgress)}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Progress value={uploadProgress} className="h-1.5" />
|
2026-03-07 12:31:55 +05:30
|
|
|
</div>
|
2026-04-03 04:14:09 +05:30
|
|
|
)}
|
2026-03-07 12:31:55 +05:30
|
|
|
|
2026-04-03 04:14:09 +05:30
|
|
|
<div className={toggleRowClass}>
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<p className="font-medium text-sm">Enable AI Summary</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Improves search quality but adds latency
|
|
|
|
|
</p>
|
2026-03-07 12:31:55 +05:30
|
|
|
</div>
|
2026-04-03 04:14:09 +05:30
|
|
|
<Switch checked={shouldSummarize} onCheckedChange={setShouldSummarize} />
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-10 16:45:51 +02:00
|
|
|
<div className={toggleRowClass}>
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<p className="font-medium text-sm">Enable Vision LLM</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Describes images using AI vision (costly, slower)
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch checked={useVisionLlm} onCheckedChange={setUseVisionLlm} />
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-14 21:26:00 -07:00
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<p className="font-medium text-sm px-1">{t("processing_mode")}</p>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setProcessingMode("basic")}
|
|
|
|
|
className={`flex items-start gap-2.5 rounded-lg border p-3 text-left transition-colors ${
|
|
|
|
|
processingMode === "basic"
|
|
|
|
|
? "border-primary bg-primary/5"
|
|
|
|
|
: "border-border hover:border-muted-foreground/50"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Zap
|
|
|
|
|
className={`h-4 w-4 mt-0.5 shrink-0 ${processingMode === "basic" ? "text-primary" : "text-muted-foreground"}`}
|
|
|
|
|
/>
|
|
|
|
|
<div className="space-y-0.5 min-w-0">
|
|
|
|
|
<p className="font-medium text-sm">{t("basic_mode")}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{t("basic_mode_desc")}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setProcessingMode("premium")}
|
|
|
|
|
className={`flex items-start gap-2.5 rounded-lg border p-3 text-left transition-colors ${
|
|
|
|
|
processingMode === "premium"
|
|
|
|
|
? "border-amber-500 bg-amber-500/5"
|
|
|
|
|
: "border-border hover:border-muted-foreground/50"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Crown
|
|
|
|
|
className={`h-4 w-4 mt-0.5 shrink-0 ${processingMode === "premium" ? "text-amber-500" : "text-muted-foreground"}`}
|
|
|
|
|
/>
|
|
|
|
|
<div className="space-y-0.5 min-w-0">
|
|
|
|
|
<p className="font-medium text-sm">{t("premium_mode")}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{t("premium_mode_desc")}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-03 04:14:09 +05:30
|
|
|
<Button
|
|
|
|
|
className="w-full"
|
|
|
|
|
onClick={handleUpload}
|
2026-04-09 11:18:56 +02:00
|
|
|
disabled={isAnyUploading || fileCount === 0}
|
2026-04-03 04:14:09 +05:30
|
|
|
>
|
2026-04-09 11:18:56 +02:00
|
|
|
{isAnyUploading ? (
|
2026-04-03 04:14:09 +05:30
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
<Spinner size="sm" />
|
|
|
|
|
{t("uploading")}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="flex items-center gap-2">
|
2026-04-09 11:18:56 +02:00
|
|
|
{folderUpload
|
2026-04-09 13:42:57 +02:00
|
|
|
? t("upload_folder_button", { count: fileCount })
|
2026-04-09 11:18:56 +02:00
|
|
|
: t("upload_button", { count: fileCount })}
|
2026-04-03 04:14:09 +05:30
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-03-07 12:31:55 +05:30
|
|
|
)}
|
2025-11-07 14:28:30 -08:00
|
|
|
|
2026-04-03 04:14:09 +05:30
|
|
|
{/* SUPPORTED FORMATS */}
|
2026-01-02 04:10:37 +05:30
|
|
|
<Accordion
|
|
|
|
|
type="single"
|
|
|
|
|
collapsible
|
2026-01-02 16:51:37 +05:30
|
|
|
value={accordionValue}
|
|
|
|
|
onValueChange={handleAccordionChange}
|
2026-04-03 09:20:44 +05:30
|
|
|
className="w-full mt-5"
|
2026-01-02 04:10:37 +05:30
|
|
|
>
|
2026-04-03 04:14:09 +05:30
|
|
|
<AccordionItem value="supported-file-types" className="border border-border rounded-lg">
|
|
|
|
|
<AccordionTrigger className="px-3 py-2.5 hover:no-underline !items-center [&>svg]:!translate-y-0">
|
|
|
|
|
<span className="text-xs sm:text-sm text-muted-foreground font-normal">
|
|
|
|
|
{t("supported_file_types")}
|
|
|
|
|
</span>
|
2026-01-02 04:07:13 +05:30
|
|
|
</AccordionTrigger>
|
2026-04-03 04:14:09 +05:30
|
|
|
<AccordionContent className="px-3 pb-3">
|
2026-04-07 05:55:39 +05:30
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{supportedExtensions.map((ext) => (
|
|
|
|
|
<Badge
|
|
|
|
|
key={ext}
|
|
|
|
|
variant="secondary"
|
|
|
|
|
className="rounded border-0 bg-neutral-200/80 dark:bg-neutral-700/60 text-muted-foreground text-[10px] px-2 py-0.5 font-normal"
|
|
|
|
|
>
|
|
|
|
|
{ext}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-01-02 04:07:13 +05:30
|
|
|
</AccordionContent>
|
|
|
|
|
</AccordionItem>
|
|
|
|
|
</Accordion>
|
2026-03-07 12:31:55 +05:30
|
|
|
</div>
|
2025-11-07 14:28:30 -08:00
|
|
|
);
|
|
|
|
|
}
|