Files
template-app/modules/documents/ui/upload-form.tsx
T
2026-11-14 09:56:45 +00:00

80 lines
3.0 KiB
TypeScript

"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). Once it has, the page's live list reads again (`onUploaded`), so the upload shows as queued at
// once; the job's progress then reaches the list through the live stream.
import { TransferProgress } from "@conusai-kit/ui"
import { useUpload } from "@conusai-kit/ui/data"
import { FormLayout, PageSection, TextField, useInSidePanel } from "@conusai-kit/ui/patterns"
import { type ReactElement, useState } from "react"
import type { ProjectSlugs } from "./slugs.js"
export interface UploadLabels {
readonly heading: string
readonly label: string
readonly hint: string
readonly submit: string
readonly busy: string
/** `Uploading {filename}: {percent}%`, with both placeholders still in it. */
readonly progress: string
}
export function UploadForm({
slugs,
accept,
labels,
onUploaded,
}: {
slugs: ProjectSlugs
/** The content types the module reads, for the file picker; the server is what enforces them. */
accept: readonly string[]
labels: UploadLabels
/** Reads the page's live list again: an upload is not a change of a document row until the job takes it. */
onUploaded: () => Promise<void>
}): ReactElement {
const inPanel = useInSidePanel()
const upload = useUpload()
const [progress, setProgress] = useState<{ filename: string; sent: number; total: number } | undefined>(undefined)
const submit = async (_: unknown, data: FormData): Promise<void> => {
const file = data.get("file")
if (!(file instanceof File) || file.size === 0) return
setProgress({ filename: file.name, sent: 0, total: file.size })
try {
await upload(slugs, file, { onProgress: ({ sent, total }) => setProgress({ filename: file.name, sent, total }) })
await onUploaded()
} finally {
setProgress(undefined)
}
}
return (
<PageSection title={inPanel ? undefined : labels.heading} variant={inPanel ? "plain" : "card"}>
<FormLayout submitLabel={labels.submit} busyLabel={labels.busy} onSubmit={submit}>
<TextField
name="file"
type="file"
label={labels.label}
description={labels.hint}
accept={accept.join(",")}
required
/>
{progress ? (
<TransferProgress
label={labels.progress
.replace("{filename}", progress.filename)
.replace("{percent}", String(Math.floor((100 * progress.sent) / progress.total)))}
value={progress.sent}
max={progress.total}
/>
) : null}
</FormLayout>
</PageSection>
)
}