"use client"; import { useState, useCallback, useEffect, useMemo } from "react"; import { ChevronDown, RefreshCw, Send, ThumbsUp, ThumbsDown, Minus, AlertCircle, CheckCircle2, Loader2, User, Tag, SlidersHorizontal, WifiOff, Key, } from "lucide-react"; import clsx from "clsx"; import { api, DIMENSION_LABELS, FALLBACK_DOMAINS, type DomainInfo, } from "@/src/utils/api"; const DIMENSIONS_BY_DOMAIN: Record = { procurement: ["accuracy", "compliance", "actionability", "clarity"], biomedical: ["accuracy", "safety", "technical_depth", "clarity"], defense_wm: ["accuracy", "technical_depth", "actionability", "safety"], halal: ["accuracy", "compliance", "ethics", "clarity"], default: ["accuracy", "safety", "actionability", "clarity"], }; type Preference = "A" | "B" | "TIE" | null; type DimensionScores = Record; export default function AnnotationView() { const [annotatorId, setAnnotatorId] = useState(""); const [apiKey, setApiKey] = useState(""); const [domains, setDomains] = useState(FALLBACK_DOMAINS); const [offline, setOffline] = useState(false); const [selectedDomain, setSelectedDomain] = useState(domains[0].id); const [selectedCategory, setSelectedCategory] = useState( domains[0].categories[0] ?? "" ); const [prompt, setPrompt] = useState(""); const [responseA, setResponseA] = useState(""); const [responseB, setResponseB] = useState(""); const [pairLoaded, setPairLoaded] = useState(false); const [preference, setPreference] = useState(null); const [notes, setNotes] = useState(""); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle"); const [submitMsg, setSubmitMsg] = useState(""); const domain = useMemo( () => domains.find((d) => d.id === selectedDomain) ?? domains[0], [domains, selectedDomain] ); const dimensions = DIMENSIONS_BY_DOMAIN[selectedDomain] || DIMENSIONS_BY_DOMAIN.default; const [scores, setScores] = useState(() => Object.fromEntries(dimensions.map((d) => [d, 3])) ); // Pull live domain list from the backend on mount; fall back silently if unreachable. useEffect(() => { let cancelled = false; api .domains() .then((d) => { if (cancelled || !Array.isArray(d) || d.length === 0) return; setDomains(d); setOffline(false); if (!d.find((x) => x.id === selectedDomain)) { setSelectedDomain(d[0].id); setSelectedCategory(d[0].categories[0] ?? ""); } }) .catch(() => setOffline(true)); return () => { cancelled = true; }; }, []); // eslint-disable-line react-hooks/exhaustive-deps const handleDomainChange = (id: string) => { setSelectedDomain(id); const d = domains.find((x) => x.id === id); setSelectedCategory(d?.categories[0] ?? ""); const dims = DIMENSIONS_BY_DOMAIN[id] || DIMENSIONS_BY_DOMAIN.default; setScores(Object.fromEntries(dims.map((dim) => [dim, 3]))); setPreference(null); setPairLoaded(false); setPrompt(""); setResponseA(""); setResponseB(""); }; const handleLoadPair = useCallback(async () => { setLoading(true); setPreference(null); setSubmitStatus("idle"); try { const pair = await api.loadPair(selectedDomain, selectedCategory || undefined); setPrompt(pair.prompt); setResponseA(pair.response_a); setResponseB(pair.response_b); if (pair.category) setSelectedCategory(pair.category); setPairLoaded(true); setOffline(false); } catch (e) { setOffline(true); setSubmitStatus("error"); setSubmitMsg("Could not reach the backend. Start the FastAPI server (`python app.py`) to load real prompts."); } finally { setLoading(false); } }, [selectedDomain, selectedCategory]); const handleSubmit = async () => { if (!preference || !annotatorId.trim() || !pairLoaded) return; setSubmitting(true); setSubmitStatus("idle"); try { await api.submitPreference( { domain: selectedDomain, category: selectedCategory || "general", prompt, response_a: responseA, response_b: responseB, preference, annotator_id: annotatorId.trim(), dimension_scores: scores, notes, }, apiKey || undefined, ); setSubmitStatus("success"); setSubmitMsg("Preference recorded. Loading next pair..."); setPreference(null); setNotes(""); // Auto-load the next pair so the annotator can keep going. handleLoadPair(); } catch (e: any) { setSubmitStatus("error"); setSubmitMsg(e?.message?.includes("401") ? "API key required or invalid." : "Failed to submit. Check your connection."); } finally { setSubmitting(false); } }; const scoreColor = (v: number) => { if (v >= 4) return "text-bp-green"; if (v >= 3) return "text-bp-blue"; if (v >= 2) return "text-bp-orange"; return "text-bp-red"; }; return (
{/* Config bar */}
{/* Annotator ID */}
setAnnotatorId(e.target.value)} placeholder="Annotator ID" className="w-36 bg-bp-dark3 border border-bp-border rounded px-2.5 py-1.5 text-xs text-bp-text placeholder:text-bp-text-disabled focus:border-bp-blue focus:outline-none transition-colors" />
setApiKey(e.target.value)} placeholder="API key (optional)" className="w-36 bg-bp-dark3 border border-bp-border rounded px-2.5 py-1.5 text-xs text-bp-text placeholder:text-bp-text-disabled focus:border-bp-blue focus:outline-none transition-colors" />
{/* Domain selector */}
{offline && (
Backend offline
)}
{/* Main content */}
{/* Prompt */}
Prompt {domain.icon} {domain.name}{selectedCategory ? ` / ${selectedCategory.replace(/_/g, " ")}` : ""}
{loading ? (
Loading prompt...
) : pairLoaded ? (

{prompt}

) : (

Click Load New Pair to fetch a prompt and two model responses for this domain.

)}
{/* Response pair */}
{(["A", "B"] as const).map((label) => { const text = label === "A" ? responseA : responseB; const selected = preference === label; return (
{label} Response {label}
{selected && ( )}
{loading ? (
Loading...
) : pairLoaded ? (

{text}

) : (

Awaiting pair…

)}
); })}
{/* Scoring + preference */}
{/* Dimension scores */}
Quality Dimensions
{dimensions.map((dim) => (
{scores[dim]}/5
setScores((s) => ({ ...s, [dim]: Number(e.target.value) })) } className="w-full" />
Poor Excellent
))}
{/* Preference + submit */}
Preference
{/* Preference buttons */}
{( [ { val: "A", label: "Prefer A", icon: ThumbsUp, color: "blue" }, { val: "TIE", label: "Tie", icon: Minus, color: "muted" }, { val: "B", label: "Prefer B", icon: ThumbsDown, color: "green" }, ] as const ).map(({ val, label, icon: Icon, color }) => { const active = preference === val; return ( ); })}
{/* Notes */}