"use client" // Upload one document through the files module. A PDF of 10 to 100 MB cannot be a JSON document, so // the browser sends it the way the kit's `uploadFile` does: it asks `files.file.beginUpload` how the // file is to be cut, sends each piece as the body of `files.file.putPart` (so the bar below moves // as the bytes leave), and ends with `files.file.completeUpload`. The answer comes back before any // OCR has run, because reading the document is the worker's job (AGENTS.md: a request enqueues and // answers). import { TransferProgress, uploadFile, useTransport } from "@conusai-kit/ui" import { type FormEvent, type ReactElement, useId, useState } from "react" import type { ProjectSlugs } from "./slugs.js" export interface UploadLabels { readonly label: string readonly hint: string readonly submit: string readonly busy: string readonly done: string /** `Uploading {filename}: {percent}%`, with both placeholders still in it. */ readonly progress: string /** The app's `app.failed` message, with `{detail}` still in it. */ readonly failed: string } type State = | { kind: "idle" | "done" } | { kind: "busy"; filename: string; sent: number; total: number } | { kind: "failed"; detail: string } export function UploadForm({ slugs, accept, labels, }: { slugs: ProjectSlugs /** The content types the module reads, for the file picker; the server is what enforces them. */ accept: readonly string[] labels: UploadLabels }): ReactElement { const transport = useTransport() const { describe } = transport const inputId = useId() const hintId = useId() const [state, setState] = useState({ kind: "idle" }) const submit = async (event: FormEvent): Promise => { event.preventDefault() const form = event.currentTarget const file = new FormData(form).get("file") if (!(file instanceof File) || file.size === 0) return setState({ kind: "busy", filename: file.name, sent: 0, total: file.size }) try { await uploadFile(transport, slugs, file, { onProgress: ({ sent, total }) => setState({ kind: "busy", filename: file.name, sent, total }), }) form.reset() setState({ kind: "done" }) // The list is rendered on the server: the new row is one reload away, and the worker will // have picked it up by the next one. window.location.reload() } catch (error) { setState({ kind: "failed", detail: describe(error) }) } } const busy = state.kind === "busy" return (

{labels.hint}

{state.kind === "busy" ? ( ) : null}

{state.kind === "done" ? labels.done : ""}

{state.kind === "failed" ? (

{labels.failed.replace("{detail}", state.detail)}

) : null} ) }