"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> /** 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(undefined) const [busy, setBusy] = useState(false) const [problem, setProblem] = useState(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): Promise => { 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) => { 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

{labels.empty}

} return (

{approvable.length > 0 ? ( ) : null} {problem ? (

{problem}

) : null} {lines.map((line) => ( {editing === line.lineId ? ( ) : ( <> )} ))}
{labels.caption}
{labels.label} {labels.value} {labels.page} {labels.source} {labels.confidence} {labels.decision} {labels.actions}
save(line, event)}>
{line.label} {line.value} {line.edited ? ` (${labels.edited})` : ""} {line.page} {labels.proposedBy.replace("{model}", `${line.model} ${line.promptVersion}`)} {line.blockId ? {` ${labels.block.replace("{id}", line.blockId)}`} : null} {new Intl.NumberFormat(locale, { style: "percent" }).format(line.confidence)} {labels.states[line.status]} {line.verification === "needs_review" ? ( {` ${line.note === "scanned_quote" ? labels.scannedQuote : labels.needsReview}`} ) : null} {line.status === "pending" ? ( <> ) : null}
) }