80 lines
3.0 KiB
TypeScript
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>
|
|
)
|
|
}
|