@@ -0,0 +1,79 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user