"use client"; /** * v1.18.9 — a single proposed Lab-OCR row on the review screen. * * Mandatory human review: each row is shown for per-row confirm/edit/discard. * The checkbox confirms; the inline fields edit analyte / value-or-valueText / * unit / reference range / date. Server-computed hints surface as calm badges: * a new-vs-existing-biomarker hint, a duplicate warning (the row defaults to * unchecked when flagged), and a low-confidence flag per the model's self-score. * * The no-alarming-colour ethos holds: an out-of-range or duplicate row is not * painted red — these are informative `secondary`/`outline` badges, same weight * as the in-range state. */ import { useId } from "react"; import { AlertCircle, FilePlus2, Link2, Ruler, TriangleAlert, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { DateField } from "@/components/ui/date-field"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useTranslations } from "@/lib/i18n/context"; import type { OcrReviewRow } from "./ocr-review-types"; /** Below this per-field confidence the field is flagged for a second look. */ const CONFIDENCE_THRESHOLD = 0.6; export function OcrRowEditor({ row, onChange, }: { row: OcrReviewRow; onChange: (next: OcrReviewRow) => void; }) { const { t } = useTranslations(); const fieldId = useId(); const isQualitative = row.valueText !== null && row.valueText !== undefined; const lowValueConfidence = !isQualitative && row.confidence.value < CONFIDENCE_THRESHOLD; const valueUnreadable = !isQualitative && row.value === null; // The model self-scores each field it transcribed. Surface a single calm // per-row flag when ANY tracked field came back below the threshold so a // shaky read is visible at a glance — including on qualitative rows, which // have no value-confidence footnote of their own. Informative, not alarming. const lowRowConfidence = row.confidence.analyte < CONFIDENCE_THRESHOLD || row.confidence.unit < CONFIDENCE_THRESHOLD || (isQualitative ? row.confidence.value < CONFIDENCE_THRESHOLD : lowValueConfidence); // v1.18.10 (#5) — a NEW numeric biomarker mints its catalog reference range // from THIS extracted row. The range is therefore load-bearing and worth a // second look. Surface the range confidence prominently for new numeric // markers, and call it out explicitly when the model was unsure (or set no // bounds at all) so the user verifies the range it will be judged against. const isNewNumericMarker = !isQualitative && row.biomarkerMatch === "new"; const lowRangeConfidence = row.confidence.range < CONFIDENCE_THRESHOLD; const hasBounds = row.referenceLow !== null || row.referenceHigh !== null; const flagRange = isNewNumericMarker && (lowRangeConfidence || !hasBounds); return (
{/* The checkbox is the PRIMARY per-row confirm on a touch-first OCR review screen, so it needs a ≥44px hit area on coarse pointers. The wrapping label supplies the touch target (and forwards the click to the control) without enlarging the 20px glyph or bloating the row on desktop, where the negative margins collapse the padding back. */}
onChange({ ...row, analyte: e.target.value })} aria-label={t("labs.ocr.analyteLabel")} className="font-medium" />
{row.biomarkerMatch === "existing" ? ( {t("labs.ocr.linksExisting", { name: row.analyte })} ) : ( {t("labs.ocr.newBiomarker")} )} {row.duplicateOf ? ( {t("labs.ocr.duplicateWarning")} ) : null} {lowRowConfidence ? ( {t("labs.ocr.lowConfidenceBadge")} ) : null}
{isQualitative ? (
onChange({ ...row, valueText: e.target.value })} />
) : ( <>
{ const raw = e.target.value.trim(); const parsed = raw === "" ? null : Number(raw); onChange({ ...row, value: parsed !== null && Number.isFinite(parsed) ? parsed : null, }); }} />
onChange({ ...row, unit: e.target.value })} />
)}
onChange({ ...row, takenAt: value || null })} />
{/* v1.18.10 (#5) — for a NEW numeric biomarker the reference range below becomes the marker's catalog range. Make that consequence visible, and flag low range-confidence / a missing range so the user verifies it before saving. A calm `secondary` badge, not an alarm. */} {isNewNumericMarker ? (
{flagRange ? t("labs.ocr.rangeVerify") : t("labs.ocr.rangeFromScan")}
) : null} {!isQualitative ? (
{ const raw = e.target.value.trim(); const parsed = raw === "" ? null : Number(raw); onChange({ ...row, referenceLow: parsed !== null && Number.isFinite(parsed) ? parsed : null, }); }} />
{ const raw = e.target.value.trim(); const parsed = raw === "" ? null : Number(raw); onChange({ ...row, referenceHigh: parsed !== null && Number.isFinite(parsed) ? parsed : null, }); }} />
) : null} {valueUnreadable ? (

{t("labs.ocr.valueUnreadable")}

) : lowValueConfidence ? (

{t("labs.ocr.lowConfidence")}

) : null}
); }