"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, Clock, Pause } from "lucide-react"; import { useActiveTimer } from "@/hooks/use-active-timer"; function formatTime(ms: number): string { const totalSeconds = Math.floor(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${seconds.toString().padStart(2, "0")}`; } 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", "Domain-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 timer = useActiveTimer(); 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(""); timer.reset(); } catch { setError("Network error"); } finally { setLoading(false); } }, [router, timer.reset]); useEffect(() => { fetchNext(); }, [fetchNext]); const handleSubmit = useCallback(async () => { if (!category || !specificity || !paragraph || submitting) return; const durationMs = timer.totalMs; const activeMs = timer.activeMs; 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, activeMs, }), }); 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; const specKeys: Record = { q: 1, w: 2, e: 3, r: 4 }; if (specKeys[e.key.toLowerCase()]) { e.preventDefault(); setSpecificity(specKeys[e.key.toLowerCase()]); } 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}
{timer.isIdle ? ( ) : ( )} {formatTime(timer.activeMs)} {timer.isIdle && ( idle )}
Progress {() => `${progress.completed} / ${progress.total}`}
{/* Floating codebook button */} {/* 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) => (
))}
{/* Submit button */} {/* Notes */}