Spaces:
Sleeping
Sleeping
File size: 5,603 Bytes
9dfccd9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | 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<HTMLInputElement>(null)
const [dragging, setDragging] = useState(false)
const [pending, setPending] = useState<File | null>(null)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<UploadResult | null>(null)
const [fileError, setFileError] = useState<string | null>(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 (
<div className="flex flex-col gap-3">
{/* Drop zone */}
<div
role="button"
tabIndex={0}
onDragOver={(e) => { 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"
>
<span className="text-2xl" aria-hidden>📄</span>
<p className="text-sm font-medium text-stone-700 dark:text-stone-300">
Drag a file here or <span className="text-brand underline">browse</span>
</p>
<p className="text-xs text-stone-400">PDF · DOCX · TXT · MD · CSV · XLSX — max 50 MB</p>
</div>
<input
ref={inputRef}
type="file"
accept={ACCEPTED}
className="hidden"
onChange={(e) => { const f = e.target.files?.[0]; if (f) pick(f) }}
/>
{/* Validation error */}
{fileError && (
<p className="text-xs text-red-600 dark:text-red-400">{fileError}</p>
)}
{/* Pending file preview */}
{pending && (
<div className="flex items-center justify-between rounded-lg border border-surface-subtle px-4 py-3">
<div>
<p className="text-sm font-medium">{pending.name}</p>
<p className="text-xs text-stone-500">{(pending.size / 1024).toFixed(0)} KB</p>
</div>
<button
onClick={upload}
disabled={loading}
className="rounded-lg bg-brand px-4 py-1.5 text-xs font-medium text-white hover:bg-brand-dark disabled:opacity-60"
>
{loading ? 'Uploading…' : 'Upload'}
</button>
</div>
)}
{/* Success result */}
{result && (
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm dark:border-green-800 dark:bg-green-950/30">
<p className="font-medium text-green-700 dark:text-green-400">Queued — processing in background</p>
<p className="mt-0.5 font-mono text-xs text-stone-500">Task {result.task_id.slice(0, 12)}…</p>
</div>
)}
</div>
)
}
|