ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
"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<State>({ kind: "idle" })
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
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 (
<form onSubmit={submit} aria-busy={busy}>
<div className="kit-field">
<label htmlFor={inputId}>{labels.label}</label>
<input id={inputId} name="file" type="file" accept={accept.join(",")} required aria-describedby={hintId} />
<p id={hintId}>{labels.hint}</p>
</div>
<button type="submit" disabled={busy}>
{busy ? labels.busy : labels.submit}
</button>
{state.kind === "busy" ? (
<TransferProgress
label={labels.progress
.replace("{filename}", state.filename)
.replace("{percent}", String(Math.floor((100 * state.sent) / state.total)))}
value={state.sent}
max={state.total}
/>
) : null}
<p role="status">{state.kind === "done" ? labels.done : ""}</p>
{state.kind === "failed" ? (
<p className="app-error" role="alert">
{labels.failed.replace("{detail}", state.detail)}
</p>
) : null}
</form>
)
}