Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useCallback, useState } from "react"; | |
| import { useDropzone } from "react-dropzone"; | |
| import { UploadCloud, Loader2 } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| function stringifyApiError(value: unknown): string | null { | |
| if (!value) return null; | |
| if (typeof value === "string") return value; | |
| if (Array.isArray(value)) { | |
| const parts = value | |
| .map((item) => { | |
| if (typeof item === "string") return item; | |
| if (item && typeof item === "object" && "msg" in item) { | |
| return String((item as { msg: unknown }).msg); | |
| } | |
| return JSON.stringify(item); | |
| }) | |
| .filter(Boolean); | |
| return parts.join("; "); | |
| } | |
| if (typeof value === "object") return JSON.stringify(value); | |
| return String(value); | |
| } | |
| export function FileUploader({ onUploaded }: { onUploaded: () => void }) { | |
| const [busy, setBusy] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [progress, setProgress] = useState<string | null>(null); | |
| const upload = useCallback( | |
| async (files: File[]) => { | |
| setError(null); | |
| setBusy(true); | |
| try { | |
| for (let i = 0; i < files.length; i++) { | |
| const f = files[i]; | |
| setProgress(`Uploading ${i + 1}/${files.length}: ${f.name}`); | |
| const fd = new FormData(); | |
| fd.append("file", f); | |
| const r = await fetch("/api/upload", { method: "POST", body: fd }); | |
| if (!r.ok) { | |
| const contentType = r.headers.get("content-type") ?? ""; | |
| let message = "Upload failed"; | |
| if (contentType.includes("application/json")) { | |
| const payload = (await r.json().catch(() => null)) as | |
| | { detail?: unknown; error?: unknown } | |
| | null; | |
| message = | |
| stringifyApiError(payload?.detail) ?? | |
| stringifyApiError(payload?.error) ?? | |
| message; | |
| } else { | |
| message = (await r.text()).trim() || message; | |
| } | |
| throw new Error(`${f.name}: ${message}`); | |
| } | |
| } | |
| onUploaded(); | |
| } catch (e) { | |
| setError((e as Error).message); | |
| } finally { | |
| setBusy(false); | |
| setProgress(null); | |
| } | |
| }, | |
| [onUploaded] | |
| ); | |
| const { getRootProps, getInputProps, isDragActive } = useDropzone({ | |
| accept: { "application/pdf": [".pdf"] }, | |
| multiple: true, | |
| disabled: busy, | |
| onDrop: upload, | |
| }); | |
| return ( | |
| <div className="space-y-2"> | |
| <div | |
| {...getRootProps()} | |
| className={cn( | |
| "flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-10 text-center transition-colors", | |
| isDragActive ? "border-primary bg-accent" : "border-border hover:bg-accent/50", | |
| busy && "pointer-events-none opacity-60" | |
| )} | |
| > | |
| <input {...getInputProps()} /> | |
| {busy ? ( | |
| <Loader2 className="mb-2 h-8 w-8 animate-spin text-muted-foreground" /> | |
| ) : ( | |
| <UploadCloud className="mb-2 h-8 w-8 text-muted-foreground" /> | |
| )} | |
| <p className="text-sm font-medium"> | |
| {busy | |
| ? progress ?? "Uploading…" | |
| : isDragActive | |
| ? "Drop the PDFs here" | |
| : "Drag & drop PDFs here, or click to select"} | |
| </p> | |
| <p className="mt-1 text-xs text-muted-foreground"> | |
| Files are pushed to your linked Hugging Face Dataset and indexed automatically. | |
| </p> | |
| </div> | |
| {error && <p className="text-sm text-destructive">{error}</p>} | |
| </div> | |
| ); | |
| } | |