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
+85
View File
@@ -0,0 +1,85 @@
// The project's documents, one row each, with where they are in the pipeline and how many lines wait
// for a person. A server component: it holds no state, and every string is passed in.
import type { ReactElement } from "react"
import type { DocumentView } from "../contract/index.js"
export interface DocumentListLabels {
readonly caption: string
readonly empty: string
readonly file: string
readonly status: string
readonly pages: string
readonly pending: string
readonly approved: string
readonly uploaded: string
readonly actions: string
/** `Download {filename}`: a same-origin, project-scoped file stream. */
readonly download: string
/** `Review {filename}`: the link's accessible name, so a list of links is not a list of "Review". */
readonly review: string
/** The status and failure texts, by code. */
readonly statuses: Readonly<Record<DocumentView["status"], string>>
readonly failures: Readonly<Record<NonNullable<DocumentView["failureCode"]>, string>>
}
export function DocumentList({
documents,
hrefFor,
downloadHrefFor,
labels,
formatDate,
}: {
documents: readonly DocumentView[]
hrefFor: (fileId: string) => string
downloadHrefFor: (fileId: string) => string
labels: DocumentListLabels
formatDate: (iso: string) => string
}): ReactElement {
if (documents.length === 0) {
return <p>{labels.empty}</p>
}
return (
<table>
<caption>{labels.caption}</caption>
<thead>
<tr>
<th scope="col">{labels.file}</th>
<th scope="col">{labels.status}</th>
<th scope="col">{labels.pages}</th>
<th scope="col">{labels.pending}</th>
<th scope="col">{labels.approved}</th>
<th scope="col">{labels.uploaded}</th>
<th scope="col">{labels.actions}</th>
</tr>
</thead>
<tbody>
{documents.map((document) => (
<tr key={document.fileId}>
<th scope="row">
<a href={hrefFor(document.fileId)} aria-label={labels.review.replace("{filename}", document.filename)}>
{document.filename}
</a>
</th>
<td>
{labels.statuses[document.status]}
{document.failureCode ? ` ${labels.failures[document.failureCode]}` : ""}
</td>
<td>{document.pageCount ?? ""}</td>
<td>{document.pending}</td>
<td>{document.approved}</td>
<td>{formatDate(document.uploadedAt)}</td>
<td>
<a
href={downloadHrefFor(document.fileId)}
aria-label={labels.download.replace("{filename}", document.filename)}
>
{labels.download.replace("{filename}", document.filename)}
</a>
</td>
</tr>
))}
</tbody>
</table>
)
}
+74
View File
@@ -0,0 +1,74 @@
"use client"
// Download the approved lines as an Excel workbook. The procedure answers with the file as base64
// (a procedure's answer is JSON on every transport); the browser turns it into a download. With
// nothing approved the button is disabled and says why, in text a screen reader reads with it.
import { type ReactElement, useId, useState } from "react"
import type { ProjectSlugs } from "./slugs.js"
import { useTransport } from "./transport.js"
export interface ExportLabels {
readonly export: string
readonly hint: string
readonly nothing: string
/** Said after the workbook was handed to the browser. */
readonly exported: string
readonly failed: string
}
export function ExportButton({
slugs,
fileId,
approved,
labels,
}: {
slugs: ProjectSlugs
fileId: string
/** How many lines are approved: with none, there is nothing to export. */
approved: number
labels: ExportLabels
}): ReactElement {
const { call, describe } = useTransport()
const hintId = useId()
const [message, setMessage] = useState<{ ok: boolean; text: string } | undefined>(undefined)
const [busy, setBusy] = useState(false)
const run = async (): Promise<void> => {
setBusy(true)
setMessage(undefined)
try {
const made = (await call("documents.export.create", { ...slugs, fileId })) as {
filename: string
contentType: string
body: string
}
const bytes = Uint8Array.from(atob(made.body), (char) => char.charCodeAt(0))
const url = URL.createObjectURL(new Blob([bytes], { type: made.contentType }))
const link = document.createElement("a")
link.href = url
link.download = made.filename
// Attached while it is clicked, and the object URL kept for a while after: a browser starts the
// download after the click returns, and a URL revoked first is a download that never starts.
document.body.append(link)
link.click()
link.remove()
setTimeout(() => URL.revokeObjectURL(url), 60_000)
setMessage({ ok: true, text: labels.exported })
} catch (error) {
setMessage({ ok: false, text: labels.failed.replace("{detail}", describe(error)) })
} finally {
setBusy(false)
}
}
return (
<div>
<button type="button" disabled={busy || approved === 0} aria-describedby={hintId} onClick={run}>
{labels.export}
</button>
<p id={hintId}>{approved === 0 ? labels.nothing : labels.hint}</p>
<p role={message?.ok === false ? "alert" : "status"}>{message?.text ?? ""}</p>
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
// The documents module's pages and components. Server-safe ones (`DocumentList`) and client ones
// (the rest, each marked "use client") are exported side by side; a page of apps/web composes them
// inside the app's `TransportProvider`.
export { DocumentList, type DocumentListLabels } from "./document-list.js"
export { ExportButton, type ExportLabels } from "./export-button.js"
export { exportLabels, listLabels, reviewLabels, type Translate, uploadLabels } from "./labels.js"
export { LineReview, type ReviewLabels } from "./line-review.js"
export type { ProjectSlugs } from "./slugs.js"
export { type DescribeError, type Transport, TransportProvider, useTransport } from "./transport.js"
export { UploadForm, type UploadLabels } from "./upload-form.js"
+100
View File
@@ -0,0 +1,100 @@
// The labels each component takes, built from a message lookup. Every key this module's pages use is
// named here and nowhere else, so `__tests__/i18n.test.ts` can run these builders against the
// catalogues and fail on a key either language lacks -- the page itself holds no string.
//
// `t` takes a key relative to the module (`status.queued`); the page binds it to
// `i18n.message("documents." + key)`. The two messages that are not this module's own -- the app's
// `app.failed`, and the kit's `ui.aiDisclosure` and `ui.page` -- are passed in already looked up.
import { DOCUMENT_STATUSES, FAILURE_CODES, LINE_STATUSES } from "../contract/constants.js"
import type { DocumentListLabels } from "./document-list.js"
import type { ExportLabels } from "./export-button.js"
import type { ReviewLabels } from "./line-review.js"
import type { UploadLabels } from "./upload-form.js"
/** `{ a: f("a"), b: f("b") }` for a list of keys, typed by the keys. */
function byKey<K extends string>(keys: readonly K[], f: (key: K) => string): Record<K, string> {
return Object.fromEntries(keys.map((key) => [key, f(key)])) as Record<K, string>
}
export type Translate = (key: string, values?: Record<string, string | number>) => string
/** The statuses a document shows: the stored ones, and `queued` for an upload nobody has read yet. */
const LISTED_STATUSES = ["queued", ...DOCUMENT_STATUSES] as const
export function uploadLabels(t: Translate, failed: string): UploadLabels {
return {
label: t("upload.label"),
hint: t("upload.hint"),
submit: t("upload.submit"),
busy: t("upload.busy"),
done: t("upload.done"),
progress: t("upload.progress"),
failed,
}
}
export function listLabels(t: Translate): DocumentListLabels {
return {
caption: t("title"),
empty: t("list.empty"),
file: t("list.file"),
status: t("list.status"),
pages: t("list.pages"),
pending: t("list.pending"),
approved: t("list.approved"),
uploaded: t("list.uploaded"),
actions: t("list.actions"),
// The `{filename}` placeholder is the component's to fill, so it is passed through untouched.
review: t("list.review", { filename: "{filename}" }),
download: t("list.download", { filename: "{filename}" }),
statuses: byKey(LISTED_STATUSES, (status) => t(`status.${status}`)),
failures: byKey(FAILURE_CODES, (code) => t(`failure.${code}`)),
}
}
export function reviewLabels(
t: Translate,
shared: { failed: string; aiDisclosure: string; pageWord: string },
): ReviewLabels {
// A message with a `{label}` or `{page}` or `{model}` placeholder is filled per line by the
// component; it is looked up with the placeholder kept in place.
const keep = (key: string, name: string) => t(key, { [name]: `{${name}}` })
return {
caption: t("review.tableLabel"),
empty: t("review.noLines"),
label: t("review.label"),
value: t("review.value"),
page: t("review.page"),
source: t("review.source"),
confidence: t("review.confidence"),
decision: t("review.decision"),
actions: t("review.actions"),
approve: keep("review.approve", "label"),
reject: keep("review.reject", "label"),
edit: keep("review.edit", "label"),
save: t("review.save"),
cancel: t("review.cancel"),
approveAll: t("review.approveAll"),
needsReview: t("review.needsReview"),
scannedQuote: t("review.scannedQuote"),
block: keep("review.block", "id"),
edited: t("review.edited"),
sourceOnPage: keep("review.sourceOnPage", "page"),
proposedBy: keep("review.proposedBy", "model"),
aiDisclosure: shared.aiDisclosure,
pageWord: shared.pageWord,
states: byKey(LINE_STATUSES, (status) => t(`review.state.${status}`)),
failed: shared.failed,
}
}
export function exportLabels(t: Translate, failed: string): ExportLabels {
return {
export: t("review.export"),
hint: t("review.exportHint"),
nothing: t("review.exportNothing"),
exported: t("review.exported"),
failed,
}
}
+238
View File
@@ -0,0 +1,238 @@
"use client"
// The review table: every extracted line with where it was read, and a person's decision on each --
// approve, edit or reject -- and a bulk approve for what is still pending. A decision is a call to
// the proposals module's own procedures (or `documents.line.edit`), so the permission, the
// transaction and the audit event are the kernel's, and this component adds none of its own.
//
// What is machine-proposed says so: the disclosure badge (EU AI Act Art. 50) heads the table, and
// each row names the model and extractor version that proposed it.
import { AiDisclosureBadge, EvidencePanel } from "@conusai-kit/ui"
import { type FormEvent, type ReactElement, useState } from "react"
import type { LineView } from "../contract/index.js"
import { REVIEW_PROCEDURES } from "../contract/review.js"
import type { ProjectSlugs } from "./slugs.js"
import { useTransport } from "./transport.js"
export interface ReviewLabels {
readonly caption: string
readonly empty: string
readonly label: string
readonly value: string
readonly page: string
readonly source: string
readonly confidence: string
readonly decision: string
readonly actions: string
/** Contain `{label}`, replaced with the line's label: the button's accessible name. */
readonly approve: string
readonly reject: string
readonly edit: string
readonly save: string
readonly cancel: string
readonly approveAll: string
/** Shown beside a line whose quote was not found on the page it cites. */
readonly needsReview: string
/** Shown instead of `needsReview` for a quote the model read off the image of a scanned page. */
readonly scannedQuote: string
/** Contains `{id}`, replaced with the reader's block id of the quote. */
readonly block: string
readonly edited: string
readonly sourceOnPage: string
readonly proposedBy: string
readonly aiDisclosure: string
/** The word before a page number ("Page"): the evidence panel prints it beside the number. */
readonly pageWord: string
readonly states: Readonly<Record<LineView["status"], string>>
/** The app's `app.failed` message, with `{detail}` still in it. */
readonly failed: string
}
/** `approveMany` takes at most the proposals policy's `maxBatch`; its default is 100. */
const BATCH = 100
export function LineReview({
slugs,
lines,
labels,
locale,
fileId,
}: {
slugs: ProjectSlugs
lines: readonly LineView[]
labels: ReviewLabels
/** 0.97 as "97 %": the locale's own number format. */
locale: string
fileId: string
}): ReactElement {
const { call, describe } = useTransport()
const [editing, setEditing] = useState<string | undefined>(undefined)
const [busy, setBusy] = useState(false)
const [problem, setProblem] = useState<string | undefined>(undefined)
const pending = lines.filter((line) => line.status === "pending")
// A line whose quote nobody could find on the page is never approved in bulk: a person looks at it.
const approvable = pending.filter((line) => line.verification === "verified")
/** Runs one decision, then reloads: the server renders the truth, including the counts and the export. */
const run = async (work: () => Promise<unknown>): Promise<void> => {
setBusy(true)
setProblem(undefined)
try {
await work()
window.location.reload()
} catch (error) {
setProblem(labels.failed.replace("{detail}", describe(error)))
setBusy(false)
}
}
const approve = (line: LineView) =>
run(() =>
call(REVIEW_PROCEDURES.approve, { ...slugs, proposalId: line.lineId, expectedVersion: line.expectedVersion }),
)
const reject = (line: LineView) => run(() => call(REVIEW_PROCEDURES.reject, { ...slugs, proposalId: line.lineId }))
const approveAll = () =>
run(async () => {
for (let at = 0; at < approvable.length; at += BATCH) {
await call(REVIEW_PROCEDURES.approveMany, {
...slugs,
proposalIds: approvable.slice(at, at + BATCH).map((line) => line.lineId),
expectations: Object.fromEntries(
approvable.slice(at, at + BATCH).map((line) => [line.lineId, { expectedVersion: line.expectedVersion }]),
),
})
}
})
const save = (line: LineView, event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const form = new FormData(event.currentTarget)
return run(() =>
call("documents.line.edit", {
...slugs,
lineId: line.lineId,
label: String(form.get("label") ?? ""),
value: String(form.get("value") ?? ""),
}),
)
}
if (lines.length === 0) {
return <p>{labels.empty}</p>
}
return (
<section aria-busy={busy}>
<p>
<AiDisclosureBadge label={labels.aiDisclosure} />
</p>
{approvable.length > 0 ? (
<button type="button" disabled={busy} onClick={approveAll}>
{labels.approveAll}
</button>
) : null}
{problem ? (
<p className="app-error" role="alert">
{problem}
</p>
) : null}
<table>
<caption>{labels.caption}</caption>
<thead>
<tr>
<th scope="col">{labels.label}</th>
<th scope="col">{labels.value}</th>
<th scope="col">{labels.page}</th>
<th scope="col">{labels.source}</th>
<th scope="col">{labels.confidence}</th>
<th scope="col">{labels.decision}</th>
<th scope="col">{labels.actions}</th>
</tr>
</thead>
<tbody>
{lines.map((line) => (
<tr key={line.lineId}>
{editing === line.lineId ? (
<td colSpan={7}>
<form onSubmit={(event) => save(line, event)}>
<div className="kit-field">
<label htmlFor={`label-${line.lineId}`}>{labels.label}</label>
<input
id={`label-${line.lineId}`}
name="label"
defaultValue={line.label}
required
maxLength={200}
/>
</div>
<div className="kit-field">
<label htmlFor={`value-${line.lineId}`}>{labels.value}</label>
<input
id={`value-${line.lineId}`}
name="value"
defaultValue={line.value}
required
maxLength={2000}
/>
</div>
<button type="submit" disabled={busy}>
{labels.save}
</button>
<button type="button" disabled={busy} onClick={() => setEditing(undefined)}>
{labels.cancel}
</button>
</form>
</td>
) : (
<>
<th scope="row">{line.label}</th>
<td>
{line.value}
{line.edited ? ` (${labels.edited})` : ""}
</td>
<td>{line.page}</td>
<td>
<EvidencePanel
evidence={{
fileId,
page: line.page,
quote: line.quote,
...(line.box ? { box: line.box } : {}),
}}
label={labels.sourceOnPage.replace("{page}", String(line.page))}
pageLabel={labels.pageWord}
/>
<small>{labels.proposedBy.replace("{model}", `${line.model} ${line.promptVersion}`)}</small>
{line.blockId ? <small>{` ${labels.block.replace("{id}", line.blockId)}`}</small> : null}
</td>
<td>{new Intl.NumberFormat(locale, { style: "percent" }).format(line.confidence)}</td>
<td>
{labels.states[line.status]}
{line.verification === "needs_review" ? (
<strong>{` ${line.note === "scanned_quote" ? labels.scannedQuote : labels.needsReview}`}</strong>
) : null}
</td>
<td>
{line.status === "pending" ? (
<>
<button type="button" disabled={busy} onClick={() => approve(line)}>
{labels.approve.replace("{label}", line.label)}
</button>
<button type="button" disabled={busy} onClick={() => setEditing(line.lineId)}>
{labels.edit.replace("{label}", line.label)}
</button>
<button type="button" disabled={busy} onClick={() => reject(line)}>
{labels.reject.replace("{label}", line.label)}
</button>
</>
) : null}
</td>
</>
)}
</tr>
))}
</tbody>
</table>
</section>
)
}
+6
View File
@@ -0,0 +1,6 @@
/** The three slugs every project-scoped procedure's input carries (a URL's own segments). */
export interface ProjectSlugs {
readonly org: string
readonly workspace: string
readonly project: string
}
+34
View File
@@ -0,0 +1,34 @@
"use client"
// How this module's interactive components reach the API: through a transport the app provides.
//
// A module may not import the app (the layer rules), and a server component cannot hand a function
// to a client one, so the seam is a context. `apps/web/src/ui/transport.tsx` is a client component
// that provides the app's own fetch-based `call` (CSRF token, problem+json errors) and wraps the
// module's pages; the components below ask for it. This is the "data facade" of spec §6.3 in its
// smallest form, and it is what `useRead`/`useWrite` from `@conusai-kit/ui/data` replace.
import { createContext, type ReactElement, type ReactNode, useContext } from "react"
/** Calls a procedure by name with an input, and answers with its output or throws its refusal. */
export type Transport = (procedure: string, input: unknown) => Promise<unknown>
/** A refusal's detail for the user, extracted by the app's transport; anything else is "unknown". */
export type DescribeError = (error: unknown) => string
interface Provided {
readonly call: Transport
readonly describe: DescribeError
}
const TransportContext = createContext<Provided | undefined>(undefined)
export function TransportProvider({ call, describe, children }: Provided & { children: ReactNode }): ReactElement {
return <TransportContext.Provider value={{ call, describe }}>{children}</TransportContext.Provider>
}
export function useTransport(): Provided {
const provided = useContext(TransportContext)
if (!provided) throw new Error("documents: wrap the page in a TransportProvider (apps/web/src/ui/transport.tsx)")
return provided
}
+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>
)
}