import { NextResponse } from "next/server"; import { db } from "@/db"; import { assignments, humanLabels, paragraphs, quizSessions, } from "@/db/schema"; import { eq, and, desc, isNull, sql } from "drizzle-orm"; import { getSession } from "@/lib/auth"; import { WARMUP_PARAGRAPHS } from "@/lib/warmup-paragraphs"; const VALID_CATEGORIES = [ "Board Governance", "Management Role", "Risk Management Process", "Third-Party Risk", "Incident Disclosure", "Strategy Integration", "None/Other", ] as const; const VALID_SPECIFICITY = [1, 2, 3, 4] as const; async function checkWarmupComplete(annotatorId: string): Promise { const [quizSession] = await db .select() .from(quizSessions) .where( and( eq(quizSessions.annotatorId, annotatorId), eq(quizSessions.passed, true), ), ) .orderBy(desc(quizSessions.startedAt)) .limit(1); if (!quizSession) return false; try { const parsed = JSON.parse(quizSession.answers); const warmupCompleted = typeof parsed === "object" && !Array.isArray(parsed) ? parsed.warmupCompleted ?? 0 : 0; return warmupCompleted >= WARMUP_PARAGRAPHS.length; } catch { return false; } } export async function GET() { const session = await getSession(); if (!session) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const warmupDone = await checkWarmupComplete(session.annotatorId); if (!warmupDone) { return NextResponse.json({ redirectToWarmup: true }); } // Find first assigned paragraph that doesn't have a label from this annotator const result = await db .select({ paragraphId: assignments.paragraphId, assignedAt: assignments.assignedAt, text: paragraphs.text, wordCount: paragraphs.wordCount, paragraphIndex: paragraphs.paragraphIndex, companyName: paragraphs.companyName, ticker: paragraphs.ticker, filingType: paragraphs.filingType, filingDate: paragraphs.filingDate, secItem: paragraphs.secItem, labelId: humanLabels.id, }) .from(assignments) .innerJoin(paragraphs, eq(assignments.paragraphId, paragraphs.id)) .leftJoin( humanLabels, and( eq(humanLabels.paragraphId, assignments.paragraphId), eq(humanLabels.annotatorId, session.annotatorId), ), ) .where(eq(assignments.annotatorId, session.annotatorId)) .orderBy(assignments.assignedAt) .limit(1000); const total = result.length; const completed = result.filter((r) => r.labelId !== null).length; const next = result.find((r) => r.labelId === null); if (!next) { return NextResponse.json({ done: true, progress: { completed, total }, }); } return NextResponse.json({ paragraph: { id: next.paragraphId, text: next.text, wordCount: next.wordCount, paragraphIndex: next.paragraphIndex, companyName: next.companyName, ticker: next.ticker, filingType: next.filingType, filingDate: next.filingDate, secItem: next.secItem, }, progress: { completed, total }, }); } export async function POST(request: Request) { const session = await getSession(); if (!session) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await request.json(); const { paragraphId, contentCategory, specificityLevel, notes, sessionId, durationMs } = body as { paragraphId?: string; contentCategory?: string; specificityLevel?: number; notes?: string; sessionId?: string; durationMs?: number; }; if (!paragraphId || !contentCategory || !specificityLevel || !sessionId) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 }, ); } if (!VALID_CATEGORIES.includes(contentCategory as (typeof VALID_CATEGORIES)[number])) { return NextResponse.json( { error: "Invalid content category" }, { status: 400 }, ); } if (!VALID_SPECIFICITY.includes(specificityLevel as (typeof VALID_SPECIFICITY)[number])) { return NextResponse.json( { error: "Invalid specificity level" }, { status: 400 }, ); } // Verify assignment exists const [assignment] = await db .select() .from(assignments) .where( and( eq(assignments.paragraphId, paragraphId), eq(assignments.annotatorId, session.annotatorId), ), ) .limit(1); if (!assignment) { return NextResponse.json( { error: "No assignment found for this paragraph" }, { status: 403 }, ); } // Check for duplicate const [existing] = await db .select({ id: humanLabels.id }) .from(humanLabels) .where( and( eq(humanLabels.paragraphId, paragraphId), eq(humanLabels.annotatorId, session.annotatorId), ), ) .limit(1); if (existing) { return NextResponse.json( { error: "Already labeled this paragraph" }, { status: 409 }, ); } await db.insert(humanLabels).values({ paragraphId, annotatorId: session.annotatorId, contentCategory, specificityLevel, notes: notes || null, sessionId, durationMs: durationMs ?? null, }); // Get updated progress const countResult = await db .select({ total: sql`count(*)`, completed: sql`count(${humanLabels.id})`, }) .from(assignments) .leftJoin( humanLabels, and( eq(humanLabels.paragraphId, assignments.paragraphId), eq(humanLabels.annotatorId, session.annotatorId), ), ) .where(eq(assignments.annotatorId, session.annotatorId)); const progress = { completed: Number(countResult[0]?.completed ?? 0), total: Number(countResult[0]?.total ?? 0), }; return NextResponse.json({ ok: true, progress }); }