"use client" import { Alert, Button, Field, Input, Label, Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@conusai-kit/ui/primitives" // 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 { describeError, useDataClient } from "@conusai-kit/ui/data" import { type FormEvent, type ReactElement, useEffect, useState } from "react" import type { LineView } from "../contract/index.js" import { REVIEW_PROCEDURES } from "../contract/review.js" import type { ProjectSlugs } from "./slugs.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, onDecided, }: { slugs: ProjectSlugs lines: readonly LineView[] labels: ReviewLabels /** 0.97 as "97 %": the locale's own number format. */ locale: string fileId: string /** Reads the live document again: its lines, its counts and what the export holds. */ onDecided: () => Promise }): ReactElement { const client = useDataClient() // Keep the actions disabled until hydration attaches handlers, so an early click cannot disappear. const [interactive, setInteractive] = useState(false) useEffect(() => setInteractive(true), []) 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 reads the document again: the lines, the counts and the export follow it. */ const run = async (work: () => Promise): Promise => { setBusy(true) setProblem(undefined) try { await work() await onDecided() setEditing(undefined) setBusy(false) } catch (error) { setProblem(labels.failed.replace("{detail}", describeError(error))) setBusy(false) } } const approve = (line: LineView) => run(() => client.call(REVIEW_PROCEDURES.approve, { ...slugs, proposalId: line.lineId, expectedVersion: line.expectedVersion, }), ) const reject = (line: LineView) => run(() => client.call(REVIEW_PROCEDURES.reject, { ...slugs, proposalId: line.lineId })) const approveAll = () => run(async () => { for (let at = 0; at < approvable.length; at += BATCH) { await client.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(() => client.call("documents.line.edit", { ...slugs, lineId: line.lineId, expectedVersion: line.expectedVersion, 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} {labels.caption} {labels.label} {labels.value} {labels.page} {labels.source} {labels.confidence} {labels.decision} {labels.actions} {lines.map((line) => ( {editing === line.lineId ? (
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} )}
))}
) }