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-22 01:05:31 -07:00
// biome-ignore lint/a11y/useSemanticElements: cannot use <button> here because the contents include nested interactive elements (renderBrowseButton renders a Button), which would be invalid HTML.
< 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 = { ( ) = > {
2026-04-07 05:55:39 +05:30
if ( ! isElectron ) fileInputRef . current ? . click ( ) ;
2026-04-22 01:05:31 -07:00
} }
onKeyDown = { ( e ) = > {
if ( e . key === "Enter" || e . key === " " ) {
e . preventDefault ( ) ;
if ( ! isElectron ) fileInputRef . current ? . click ( ) ;
}
} }
2026-04-07 05:55:39 +05:30
>
2026-04-22 01:05:31 -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 ( ) }
>
{ 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
) ;
}