import { useCallback, useRef, useState } from 'react' import type { DragEvent } from 'react' import { uploadFile } from '../api/client.ts' import { useToast } from './shared/Toast.tsx' import Loading from './shared/Loading.tsx' import './upload.css' type FileStatus = 'pending' | 'uploading' | 'success' | 'error' interface UploadItem { id: string file: File status: FileStatus message?: string } const ACCEPTED_EXTENSIONS = ['.pdf', '.docx', '.txt', '.md'] const CONCURRENCY = 2 function formatFileSize(bytes: number): string { if (!bytes) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}` } function makeId(): string { return crypto.randomUUID ? crypto.randomUUID() : `f_${Date.now()}_${Math.random().toString(36).slice(2)}` } interface UploadViewProps { onIndexed: () => void } export default function UploadView({ onIndexed }: UploadViewProps) { const [items, setItems] = useState([]) const [isDragging, setIsDragging] = useState(false) const [isUploading, setIsUploading] = useState(false) const fileInputRef = useRef(null) const { showToast } = useToast() const addFiles = useCallback((files: FileList | File[]) => { const list = Array.from(files) if (list.length === 0) return setItems((prev) => [ ...prev, ...list.map((file) => ({ id: makeId(), file, status: 'pending' as FileStatus })), ]) }, []) const handleInputChange = useCallback( (e: React.ChangeEvent) => { if (e.target.files) addFiles(e.target.files) e.target.value = '' }, [addFiles], ) const handleDrop = useCallback( (e: DragEvent) => { e.preventDefault() setIsDragging(false) if (e.dataTransfer?.files?.length) { addFiles(e.dataTransfer.files) } }, [addFiles], ) const handleDragOver = useCallback((e: DragEvent) => { e.preventDefault() setIsDragging(true) }, []) const handleDragLeave = useCallback((e: DragEvent) => { e.preventDefault() setIsDragging(false) }, []) const uploadOne = useCallback(async (item: UploadItem) => { setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, status: 'uploading' } : it)), ) try { const res = await uploadFile(item.file) setItems((prev) => prev.map((it) => it.id === item.id ? { ...it, status: 'success', message: res.message } : it, ), ) return true } catch (err) { const message = err instanceof Error ? err.message : 'Upload failed' setItems((prev) => prev.map((it) => (it.id === item.id ? { ...it, status: 'error', message } : it)), ) return false } }, []) const handleUploadAll = useCallback(async () => { const pending = items.filter((it) => it.status === 'pending' || it.status === 'error') if (pending.length === 0) { showToast('No files to upload', 'error') return } setIsUploading(true) let successCount = 0 let queueIndex = 0 async function worker() { while (queueIndex < pending.length) { const item = pending[queueIndex] queueIndex += 1 if (!item) continue const ok = await uploadOne(item) if (ok) successCount += 1 } } const workers = Array.from({ length: Math.min(CONCURRENCY, pending.length) }, () => worker()) await Promise.all(workers) setIsUploading(false) if (successCount > 0) { showToast(`${successCount} file(s) uploaded and indexed`, 'success') onIndexed() } if (successCount < pending.length) { showToast(`${pending.length - successCount} file(s) failed to upload`, 'error') } }, [items, uploadOne, showToast, onIndexed]) const removeItem = useCallback((id: string) => { setItems((prev) => prev.filter((it) => it.id !== id)) }, []) const clearFinished = useCallback(() => { setItems((prev) => prev.filter((it) => it.status !== 'success')) }, []) const statusIcon: Record = { pending: '⏳', uploading: '⬆️', success: '✅', error: '❌', } return (

Upload Documents

Import PDF, DOCX, TXT, or Markdown files to build a searchable knowledge base

{items.length > 0 && (
{items.map((item) => (
📎 {item.file.name} {formatFileSize(item.file.size)} {statusIcon[item.status]}{' '} {item.status === 'uploading' ? 'Uploading…' : item.status === 'error' ? item.message ?? 'Failed' : item.status === 'success' ? 'Indexed' : 'Pending'} {item.status !== 'uploading' && ( )}
))}
)}
{items.some((it) => it.status === 'success') && ( )}
) }