import { useRef, useState } from 'react' import { useUIStore } from '@/stores/uiStore' import { ApiError } from '@/lib/http' import { env } from '@/config/env' import { cn } from '@/lib/utils' const ACCEPTED = '.pdf,.docx,.doc,.txt,.md,.csv,.xlsx,.xls' const ACCEPTED_SET = new Set(ACCEPTED.split(',')) const MAX_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB interface UploadResult { filename: string task_id: string } interface Props { teamId?: string onQueued?: (result: UploadResult) => void } export function FileUploadWidget({ teamId = 'default', onQueued }: Props) { const inputRef = useRef(null) const [dragging, setDragging] = useState(false) const [pending, setPending] = useState(null) const [loading, setLoading] = useState(false) const [result, setResult] = useState(null) const [fileError, setFileError] = useState(null) const addToast = useUIStore((s) => s.addToast) const validate = (file: File): string | null => { const ext = '.' + file.name.split('.').pop()?.toLowerCase() if (!ACCEPTED_SET.has(ext)) return `Unsupported type (${ext}). Accepted: PDF, DOCX, TXT, MD, CSV, XLSX` if (file.size > MAX_SIZE_BYTES) return `File too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max 50 MB` return null } const pick = (file: File) => { setResult(null) const err = validate(file) setFileError(err) setPending(err ? null : file) } const upload = async () => { if (!pending) return setLoading(true) try { const form = new FormData() form.append('file', pending) form.append('team_id', teamId) const res = await fetch(`${env.apiBaseUrl}/api/ingest/file`, { method: 'POST', credentials: 'include', body: form, // No Content-Type — browser sets multipart boundary automatically }) if (!res.ok) { const requestId = res.headers.get('X-Request-ID') ?? undefined const text = await res.text().catch(() => res.statusText) if (res.status >= 500) { addToast({ type: 'error', message: requestId ? `Upload error [${requestId}]` : 'Upload failed — server error', }) } else { addToast({ type: 'error', message: text || 'Upload failed' }) } throw new ApiError(res.status, text, requestId) } const data = await res.json() as UploadResult setResult(data) setPending(null) onQueued?.(data) addToast({ type: 'success', message: `${data.filename} queued for processing` }) } catch (err) { if (!(err instanceof ApiError)) { addToast({ type: 'error', message: 'Upload failed — no connection' }) } } finally { setLoading(false) } } const onDrop = (e: React.DragEvent) => { e.preventDefault() setDragging(false) const file = e.dataTransfer.files[0] if (file) pick(file) } return (
{/* Drop zone */}
{ e.preventDefault(); setDragging(true) }} onDragLeave={() => setDragging(false)} onDrop={onDrop} onClick={() => inputRef.current?.click()} onKeyDown={(e) => e.key === 'Enter' && inputRef.current?.click()} className={cn( 'flex cursor-pointer flex-col items-center gap-2 rounded-xl border-2 border-dashed p-8 text-center transition-colors', dragging ? 'border-brand bg-brand/5' : 'border-stone-200 hover:border-brand/50 dark:border-stone-700', )} aria-label="Upload file — drag and drop or click to browse" > 📄

Drag a file here or browse

PDF · DOCX · TXT · MD · CSV · XLSX — max 50 MB

{ const f = e.target.files?.[0]; if (f) pick(f) }} /> {/* Validation error */} {fileError && (

{fileError}

)} {/* Pending file preview */} {pending && (

{pending.name}

{(pending.size / 1024).toFixed(0)} KB

)} {/* Success result */} {result && (

Queued — processing in background

Task {result.task_id.slice(0, 12)}…

)}
) }