import * as React from 'react'; import { ClipboardCheck } from 'lucide-react'; import { cn } from '../../lib/utils'; /** One guardrail/dimension the reviewer scores (key drives storage, label is shown). */ export interface ReviewDimension { key: string; label: string; } /** * A reviewer's structured judgement of an AI-generated draft: an overall verdict * plus per-guardrail scores/notes and a free-text overall note. Drives the * downstream AI revise loop. */ export interface ReviewScore { /** Overall verdict — the primary signal for the revise loop. */ verdict?: 'approve' | 'revise' | 'reject'; /** Per-dimension score (1–5) + optional note, keyed by dimension key. */ dimensions?: Record; /** Free-text overall note. */ overallNote?: string; } /** * The default guardrail dimensions, mirroring a content-guardrail judge. Callers * can override with any dimension set — the scorecard renders whatever it's given, * so it stays generic (this is one sensible instance, not the design driver). */ export const DEFAULT_REVIEW_DIMENSIONS: ReviewDimension[] = [ { key: 'political-sensitivity', label: 'Political sensitivity' }, { key: 'source-reliability', label: 'Source reliability' }, { key: 'recency', label: 'Recency' }, { key: 'tone-and-voice', label: 'Tone and voice' }, { key: 'accuracy', label: 'Accuracy' }, ]; type Verdict = NonNullable; /** Redundant (color + label) tone per verdict — never rely on color alone. */ const VERDICT_TONE: Record = { approve: { label: 'Approve', bg: 'bg-emerald-600', text: 'text-white' }, revise: { label: 'Revise', bg: 'bg-amber-500', text: 'text-white' }, reject: { label: 'Reject', bg: 'bg-red-600', text: 'text-white' }, }; const VERDICTS: Verdict[] = ['approve', 'revise', 'reject']; const SCORE_VALUES = [1, 2, 3, 4, 5]; export interface ReviewScorecardProps { /** Controlled value — the reviewer's current judgement. */ value: ReviewScore; /** Called with the next value on every change (immutable). */ onChange: (next: ReviewScore) => void; /** The dimensions to score. Defaults to DEFAULT_REVIEW_DIMENSIONS. */ dimensions?: ReviewDimension[]; /** Render values as read-only (no inputs). */ readOnly?: boolean; title?: string; className?: string; } /** A 1–5 score picker (clickable dots), read-only when the scorecard is. */ function ScorePicker({ label, value, onChange, readOnly, }: { label: string; value?: number; onChange: (score: number) => void; readOnly?: boolean; }) { return (
{SCORE_VALUES.map((n) => { const filled = value != null && n <= value; if (readOnly) { return (
); } /** * A controlled scorecard for a reviewer's structured judgement of an AI-generated * draft (768w.16.10.4). Presentational + bring-your-own-data: it renders a verdict * segmented control, one scored row per guardrail dimension, and an overall note — * emitting the next ReviewScore (immutably) on every change. It never fetches or * persists; the app owns the data and the downstream revise loop. */ export function ReviewScorecard({ value, onChange, dimensions = DEFAULT_REVIEW_DIMENSIONS, readOnly, title = 'Review', className, }: ReviewScorecardProps) { const setDimension = (key: string, patch: { score?: number; note?: string }) => { const prev = value.dimensions?.[key] ?? {}; onChange({ ...value, dimensions: { ...value.dimensions, [key]: { ...prev, ...patch } }, }); }; return (
{/* Header + verdict */}
{readOnly ? ( value.verdict ? ( {VERDICT_TONE[value.verdict].label} ) : ( No verdict ) ) : (
{VERDICTS.map((v) => { const tone = VERDICT_TONE[v]; const selected = value.verdict === v; return ( ); })}
)}
{/* Per-dimension rows */}
{dimensions.map((dim) => { const dimVal = value.dimensions?.[dim.key] ?? {}; return (
{dim.label} setDimension(dim.key, { score })} readOnly={readOnly} />
{readOnly ? ( dimVal.note ?

{dimVal.note}

: null ) : ( setDimension(dim.key, { note: e.target.value })} aria-label={`${dim.label} note`} placeholder="Add a note…" className="w-full rounded-md border px-2 py-1 text-xs" /> )}
); })}
{/* Overall note */}
Overall note {readOnly ? (

{value.overallNote || }

) : (