This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
// Upload and index: the browser sends the file to the files module (the kit's `uploadFile`), then
|
||||
// asks this module to index it (`knowledge.document.index`). The indexing itself is a worker job;
|
||||
// this form only ever waits on the request.
|
||||
|
||||
import { type ProjectSlugs, uploadFile, useTransport } from "@conusai-kit/ui"
|
||||
import { type FormEvent, type ReactElement, useId, useState } from "react"
|
||||
import { INDEXABLE_CONTENT_TYPES, MAX_INDEX_BYTES } from "../contract/index.js"
|
||||
|
||||
export interface IndexFormLabels {
|
||||
readonly heading: string
|
||||
readonly label: string
|
||||
readonly hint: string
|
||||
readonly submit: string
|
||||
readonly busy: string
|
||||
readonly done: string
|
||||
readonly failed: string
|
||||
}
|
||||
|
||||
type State = { kind: "idle" } | { kind: "busy" } | { kind: "done" } | { kind: "failed"; detail: string }
|
||||
|
||||
export function IndexForm({
|
||||
slugs,
|
||||
collectionName,
|
||||
labels,
|
||||
}: {
|
||||
slugs: ProjectSlugs
|
||||
collectionName: string
|
||||
labels: IndexFormLabels
|
||||
}): ReactElement {
|
||||
const transport = useTransport()
|
||||
const { call, describe } = transport
|
||||
const inputId = useId()
|
||||
const hintId = useId()
|
||||
const [state, setState] = useState<State>({ kind: "idle" })
|
||||
|
||||
const index = 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" })
|
||||
try {
|
||||
const stored = await uploadFile(transport, slugs, file)
|
||||
await call("knowledge.document.index", { ...slugs, collectionName, fileId: stored.fileId })
|
||||
form.reset()
|
||||
setState({ kind: "done" })
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
setState({ kind: "failed", detail: describe(error) })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-labelledby={`${inputId}-heading`}>
|
||||
<h2 id={`${inputId}-heading`}>{labels.heading}</h2>
|
||||
<form onSubmit={(event) => void index(event)}>
|
||||
<label htmlFor={inputId}>{labels.label}</label>
|
||||
<input
|
||||
id={inputId}
|
||||
name="file"
|
||||
type="file"
|
||||
required
|
||||
aria-describedby={hintId}
|
||||
accept={INDEXABLE_CONTENT_TYPES.join(",")}
|
||||
/>
|
||||
<p id={hintId}>
|
||||
{labels.hint} · {Math.round(MAX_INDEX_BYTES / 1024)} KB.
|
||||
</p>
|
||||
<button type="submit" disabled={state.kind === "busy"}>
|
||||
{state.kind === "busy" ? labels.busy : labels.submit}
|
||||
</button>
|
||||
</form>
|
||||
{state.kind === "done" ? <p role="status">{labels.done}</p> : null}
|
||||
{state.kind === "failed" ? <p role="alert">{labels.failed.replace("{detail}", state.detail)}</p> : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user