"use client"; import { useEffect, useState, useCallback } from "react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, } from "@/components/ui/card"; import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Textarea } from "@/components/ui/textarea"; import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; const CATEGORIES = [ "Board Governance", "Management Role", "Risk Management Process", "Third-Party Risk", "Incident Disclosure", "Strategy Integration", "None/Other", ] as const; const SPECIFICITIES = [ { value: 1, label: "1 - Generic/Boilerplate" }, { value: 2, label: "2 - Domain-Adapted" }, { value: 3, label: "3 - Firm-Specific" }, { value: 4, label: "4 - Quantified-Verifiable" }, ] as const; interface QueueLabel { annotatorId: string; displayName: string; category: string; specificity: number; notes: string | null; } interface QueueItem { paragraphId: string; paragraphText: string; labels: QueueLabel[]; stage1Category: string | null; stage1Specificity: number | null; splitSeverity: number; } interface AnnotatorProgress { id: string; displayName: string; completed: number; total: number; } interface MetricsData { progress: { totalParagraphs: number; fullyLabeled: number; adjudicated: number; perAnnotator: AnnotatorProgress[]; }; agreement: { consensusRate: number; avgKappa: number; kappaMatrix: { annotators: string[]; values: number[][]; }; krippendorffsAlpha: number; perCategory: Record; }; confusionMatrix: { labels: string[]; matrix: number[][]; }; } function kappaColor(value: number): string { if (value >= 0.75) return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300"; if (value >= 0.5) return "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300"; return "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300"; } function heatmapColor(value: number, max: number): string { if (max === 0) return ""; const intensity = value / max; if (intensity > 0.7) return "bg-blue-200 dark:bg-blue-900/50"; if (intensity > 0.4) return "bg-blue-100 dark:bg-blue-900/30"; if (intensity > 0.1) return "bg-blue-50 dark:bg-blue-900/15"; return ""; } function AdjudicationQueue() { const [queue, setQueue] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedItem, setSelectedItem] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [resolveCategory, setResolveCategory] = useState(""); const [resolveSpecificity, setResolveSpecificity] = useState(""); const [resolveNotes, setResolveNotes] = useState(""); const [submitting, setSubmitting] = useState(false); const [expandedParagraphs, setExpandedParagraphs] = useState>( new Set(), ); const fetchQueue = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/adjudicate"); if (!res.ok) { const data = await res.json(); throw new Error(data.error ?? "Failed to load queue"); } const data = await res.json(); setQueue(data.queue); } catch (e) { setError(e instanceof Error ? e.message : "Unknown error"); } finally { setLoading(false); } }, []); useEffect(() => { fetchQueue(); }, [fetchQueue]); function openResolveDialog(item: QueueItem) { setSelectedItem(item); setResolveCategory(""); setResolveSpecificity(""); setResolveNotes(""); setDialogOpen(true); } async function handleSubmitResolution() { if (!selectedItem || !resolveCategory || !resolveSpecificity) return; setSubmitting(true); try { const res = await fetch("/api/adjudicate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paragraphId: selectedItem.paragraphId, finalCategory: resolveCategory, finalSpecificity: Number(resolveSpecificity), notes: resolveNotes || undefined, }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error ?? "Failed to resolve"); } setQueue((prev) => prev.filter((item) => item.paragraphId !== selectedItem.paragraphId), ); setDialogOpen(false); } catch (e) { setError(e instanceof Error ? e.message : "Unknown error"); } finally { setSubmitting(false); } } function toggleExpand(paragraphId: string) { setExpandedParagraphs((prev) => { const next = new Set(prev); if (next.has(paragraphId)) { next.delete(paragraphId); } else { next.add(paragraphId); } return next; }); } if (loading) { return ( Loading adjudication queue... ); } if (error) { return ( {error} ); } return ( <> Adjudication Queue {queue.length === 0 ? "All disagreements have been resolved." : `${queue.length} paragraph${queue.length === 1 ? "" : "s"} need${queue.length === 1 ? "s" : ""} adjudication`} {queue.length === 0 ? (

No paragraphs pending manual review. Consensus and majority agreements are auto-resolved.

) : (
{queue.map((item) => { const isExpanded = expandedParagraphs.has(item.paragraphId); const truncatedText = item.paragraphText.length > 200 && !isExpanded ? item.paragraphText.slice(0, 200) + "..." : item.paragraphText; return ( {/* Severity badge */}
= 3 ? "destructive" : "secondary" } > {item.splitSeverity >= 3 ? "3-way split" : "2-way split"} {item.paragraphId}
{/* Paragraph text */}

{truncatedText}

{item.paragraphText.length > 200 && ( )}
{/* Stage 1 reference */} {item.stage1Category && (

Stage 1: {item.stage1Category} {item.stage1Specificity !== null && ` (specificity ${item.stage1Specificity})`}

)} {/* Annotator labels side by side */} Annotator Category Specificity Notes {item.labels.map((label) => ( {label.displayName} {label.category} {label.specificity} {label.notes ?? "-"} ))}
); })}
)}
{/* Resolution Dialog */} Resolve Adjudication Select the final category and specificity for this paragraph.
{/* Category selection */}
{CATEGORIES.map((cat) => (
))}
{/* Specificity selection */}
{SPECIFICITIES.map((spec) => (
))}
{/* Notes */}