Luminaria / frontend /components /file-uploader.tsx
senlinyy's picture
fix: fix upload
d1b9d61
Raw
History Blame Contribute Delete
3.62 kB
"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>
);
}