"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; import { BookOpen, CheckCircle2 } from "lucide-react"; const CATEGORIES = [ "Board Governance", "Management Role", "Risk Management Process", "Third-Party Risk", "Incident Disclosure", "Strategy Integration", "None/Other", ] as const; const SPECIFICITY_LABELS = [ "Generic Boilerplate", "Sector-Adapted", "Firm-Specific", "Quantified-Verifiable", ] as const; interface ParagraphData { id: string; text: string; wordCount: number; paragraphIndex: number; companyName: string; ticker: string | null; filingType: string; filingDate: string; secItem: string; } interface ProgressData { completed: number; total: number; } export default function LabelPage() { const router = useRouter(); const [paragraph, setParagraph] = useState(null); const [progress, setProgress] = useState({ completed: 0, total: 0 }); const [category, setCategory] = useState(""); const [specificity, setSpecificity] = useState(0); const [notes, setNotes] = useState(""); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [done, setDone] = useState(false); const [error, setError] = useState(null); const timerStart = useRef(Date.now()); const sessionId = useRef(crypto.randomUUID()); const notesRef = useRef(null); const fetchNext = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/label"); const data = await res.json(); if (!res.ok) { setError(data.error ?? "Failed to load"); return; } if (data.redirectToWarmup) { router.push("/label/warmup"); return; } if (data.done) { setDone(true); setProgress(data.progress); return; } setParagraph(data.paragraph); setProgress(data.progress); setCategory(""); setSpecificity(0); setNotes(""); timerStart.current = Date.now(); } catch { setError("Network error"); } finally { setLoading(false); } }, [router]); useEffect(() => { fetchNext(); }, [fetchNext]); const handleSubmit = useCallback(async () => { if (!category || !specificity || !paragraph || submitting) return; const durationMs = Date.now() - timerStart.current; setSubmitting(true); try { const res = await fetch("/api/label", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paragraphId: paragraph.id, contentCategory: category, specificityLevel: specificity, notes: notes.trim() || undefined, sessionId: sessionId.current, durationMs, }), }); const data = await res.json(); if (!res.ok) { setError(data.error ?? "Failed to submit"); return; } setProgress(data.progress); // Immediately fetch next paragraph await fetchNext(); } catch { setError("Network error"); } finally { setSubmitting(false); } }, [category, specificity, paragraph, notes, submitting, fetchNext]); // Keyboard shortcuts useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.target instanceof HTMLTextAreaElement) return; if (done || loading) return; if (e.shiftKey && e.key >= "1" && e.key <= "4") { e.preventDefault(); setSpecificity(parseInt(e.key)); } else if (!e.shiftKey && e.key >= "1" && e.key <= "7") { e.preventDefault(); setCategory(CATEGORIES[parseInt(e.key) - 1]); } else if (e.key === "Enter" && category && specificity) { e.preventDefault(); handleSubmit(); } else if (e.key === "n" || e.key === "N") { // Focus notes field e.preventDefault(); notesRef.current?.focus(); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [category, specificity, done, loading, handleSubmit]); if (loading && !paragraph) { return (

Loading...

); } if (error) { return (
Error {error}
); } if (done) { return (
All Done!

You have labeled all {progress.total} assigned paragraphs. Thank you!

); } if (!paragraph) return null; return (
{/* Header bar */}
{paragraph.companyName} {paragraph.ticker ? ` (${paragraph.ticker})` : ""} {paragraph.filingType} {paragraph.filingDate} {paragraph.secItem}
Progress {() => `${progress.completed} / ${progress.total}`}
{/* Main content */}
{/* Paragraph display */}
{paragraph.text}

{paragraph.wordCount} words

{/* Labeling form */}
{/* Content Category */} Content Category {CATEGORIES.map((cat, i) => (
))}
{/* Specificity Level */} Specificity Level setSpecificity(parseInt(v))} > {SPECIFICITY_LABELS.map((label, i) => (
))}
{/* Notes */}