'use client' import { Suspense, use, useState, useEffect } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Progress } from '@/components/ui/progress' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@/components/ui/tabs' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { ArrowLeft, Users, FileText, CheckCircle2, Clock, AlertCircle, Sparkles, Loader2, Plus, Trash2, RefreshCw, UserPlus, Cpu, Brain, } from 'lucide-react' import { toast } from 'sonner' // Suggestion type for both algorithm and AI suggestions interface Suggestion { userId: string jurorName: string projectId: string projectTitle: string score: number reasoning: string[] } // Reusable table component for displaying suggestions function SuggestionsTable({ suggestions, selectedSuggestions, onToggle, onSelectAll, onApply, isApplying, }: { suggestions: Suggestion[] selectedSuggestions: Set onToggle: (key: string) => void onSelectAll: () => void onApply: () => void isApplying: boolean }) { return (
0} onCheckedChange={onSelectAll} /> {selectedSuggestions.size} of {suggestions.length} selected
Juror Project Score Reasoning {suggestions.map((suggestion) => { const key = `${suggestion.userId}-${suggestion.projectId}` const isSelected = selectedSuggestions.has(key) return ( onToggle(key)} /> {suggestion.jurorName} {suggestion.projectTitle} = 60 ? 'default' : suggestion.score >= 40 ? 'secondary' : 'outline' } > {suggestion.score.toFixed(0)}
    {suggestion.reasoning.map((r, i) => (
  • {r}
  • ))}
) })}
) } interface PageProps { params: Promise<{ id: string }> } function AssignmentManagementContent({ roundId }: { roundId: string }) { const [selectedSuggestions, setSelectedSuggestions] = useState>(new Set()) const [manualDialogOpen, setManualDialogOpen] = useState(false) const [selectedJuror, setSelectedJuror] = useState('') const [selectedProject, setSelectedProject] = useState('') const [activeTab, setActiveTab] = useState<'algorithm' | 'ai'>('algorithm') const [activeJobId, setActiveJobId] = useState(null) const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({ id: roundId }) const { data: assignments, isLoading: loadingAssignments } = trpc.assignment.listByRound.useQuery({ roundId }) const { data: stats, isLoading: loadingStats } = trpc.assignment.getStats.useQuery({ roundId }) const { data: isAIAvailable } = trpc.assignment.isAIAvailable.useQuery() // Always fetch latest AI job to check for existing results const { data: latestJob, refetch: refetchLatestJob } = trpc.assignment.getLatestAIAssignmentJob.useQuery( { roundId } ) // Poll for job status when there's an active job const { data: jobStatus } = trpc.assignment.getAIAssignmentJobStatus.useQuery( { jobId: activeJobId! }, { enabled: !!activeJobId, refetchInterval: activeJobId ? 2000 : false, } ) // Start AI assignment job mutation const startAIJob = trpc.assignment.startAIAssignmentJob.useMutation() const isAIJobRunning = jobStatus?.status === 'RUNNING' || jobStatus?.status === 'PENDING' const aiJobProgressPercent = jobStatus?.totalBatches ? Math.round((jobStatus.currentBatch / jobStatus.totalBatches) * 100) : 0 // Check if there's a completed AI job with stored suggestions const hasStoredAISuggestions = latestJob?.status === 'COMPLETED' && latestJob?.suggestionsCount > 0 // Algorithmic suggestions (always fetch for algorithm tab) const { data: algorithmicSuggestions, isLoading: loadingAlgorithmic, refetch: refetchAlgorithmic } = trpc.assignment.getSuggestions.useQuery( { roundId }, { enabled: !!round } ) // AI-powered suggestions - fetch if there are stored results OR if AI tab is active const { data: aiSuggestionsRaw, isLoading: loadingAI, refetch: refetchAI } = trpc.assignment.getAISuggestions.useQuery( { roundId, useAI: true }, { enabled: !!round && (hasStoredAISuggestions || activeTab === 'ai') && !isAIJobRunning, staleTime: Infinity, // Never consider stale (only refetch manually) refetchOnWindowFocus: false, refetchOnReconnect: false, } ) // Set active job from latest job on load useEffect(() => { if (latestJob && (latestJob.status === 'RUNNING' || latestJob.status === 'PENDING')) { setActiveJobId(latestJob.id) setActiveTab('ai') // Switch to AI tab if a job is running } }, [latestJob]) // Handle job completion useEffect(() => { if (jobStatus?.status === 'COMPLETED') { toast.success( `AI Assignment complete: ${jobStatus.suggestionsCount} suggestions generated${jobStatus.fallbackUsed ? ' (using fallback algorithm)' : ''}` ) setActiveJobId(null) refetchLatestJob() refetchAI() } else if (jobStatus?.status === 'FAILED') { toast.error(`AI Assignment failed: ${jobStatus.errorMessage || 'Unknown error'}`) setActiveJobId(null) refetchLatestJob() } }, [jobStatus?.status, jobStatus?.suggestionsCount, jobStatus?.fallbackUsed, jobStatus?.errorMessage, refetchLatestJob, refetchAI]) const handleStartAIJob = async () => { try { setActiveTab('ai') // Switch to AI tab when starting const result = await startAIJob.mutateAsync({ roundId }) setActiveJobId(result.jobId) toast.info('AI Assignment job started. Progress will update automatically.') } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to start AI assignment' ) } } // Normalize AI suggestions to match algorithmic format const aiSuggestions = aiSuggestionsRaw?.suggestions?.map((s) => ({ userId: s.jurorId, jurorName: s.jurorName, projectId: s.projectId, projectTitle: s.projectTitle, score: Math.round(s.confidenceScore * 100), reasoning: [s.reasoning], })) ?? [] // Use the appropriate suggestions based on active tab const currentSuggestions = activeTab === 'ai' ? aiSuggestions : (algorithmicSuggestions ?? []) const isLoadingCurrentSuggestions = activeTab === 'ai' ? (loadingAI || isAIJobRunning) : loadingAlgorithmic // Get available jurors for manual assignment const { data: availableJurors } = trpc.user.getJuryMembers.useQuery( { roundId }, { enabled: manualDialogOpen } ) // Get projects in this round for manual assignment const { data: roundProjects } = trpc.project.list.useQuery( { roundId, perPage: 500 }, { enabled: manualDialogOpen } ) const utils = trpc.useUtils() const deleteAssignment = trpc.assignment.delete.useMutation({ onSuccess: () => { utils.assignment.listByRound.invalidate({ roundId }) utils.assignment.getStats.invalidate({ roundId }) }, }) const applySuggestions = trpc.assignment.applySuggestions.useMutation({ onSuccess: () => { utils.assignment.listByRound.invalidate({ roundId }) utils.assignment.getStats.invalidate({ roundId }) utils.assignment.getSuggestions.invalidate({ roundId }) utils.assignment.getAISuggestions.invalidate({ roundId }) setSelectedSuggestions(new Set()) }, }) const createAssignment = trpc.assignment.create.useMutation({ onSuccess: () => { utils.assignment.listByRound.invalidate({ roundId }) utils.assignment.getStats.invalidate({ roundId }) utils.assignment.getSuggestions.invalidate({ roundId }) setManualDialogOpen(false) setSelectedJuror('') setSelectedProject('') toast.success('Assignment created successfully') }, onError: (error) => { toast.error(error.message || 'Failed to create assignment') }, }) const handleCreateManualAssignment = () => { if (!selectedJuror || !selectedProject) { toast.error('Please select both a juror and a project') return } createAssignment.mutate({ userId: selectedJuror, projectId: selectedProject, roundId, }) } if (loadingRound || loadingAssignments) { return } if (!round) { return (

Round Not Found

) } const handleToggleSuggestion = (key: string) => { setSelectedSuggestions((prev) => { const newSet = new Set(prev) if (newSet.has(key)) { newSet.delete(key) } else { newSet.add(key) } return newSet }) } const handleSelectAllSuggestions = () => { if (currentSuggestions) { if (selectedSuggestions.size === currentSuggestions.length) { setSelectedSuggestions(new Set()) } else { setSelectedSuggestions( new Set(currentSuggestions.map((s) => `${s.userId}-${s.projectId}`)) ) } } } const handleApplySelected = async () => { if (!currentSuggestions) return const selected = currentSuggestions.filter((s) => selectedSuggestions.has(`${s.userId}-${s.projectId}`) ) await applySuggestions.mutateAsync({ roundId, assignments: selected.map((s) => ({ userId: s.userId, projectId: s.projectId, reasoning: s.reasoning.join('; '), })), }) } // Group assignments by project const assignmentsByProject = assignments?.reduce((acc, assignment) => { const projectId = assignment.project.id if (!acc[projectId]) { acc[projectId] = { project: assignment.project, assignments: [], } } acc[projectId].assignments.push(assignment) return acc }, {} as Record) || {} return (
{/* Header */}
{round.program.name} / {round.name}

Manage Judge Assignments

{/* Manual Assignment Button */} Create Manual Assignment Assign a jury member to evaluate a specific project
{/* Juror Select */}
{selectedJuror && availableJurors && (

{(() => { const juror = availableJurors.find(j => j.id === selectedJuror) if (!juror) return null const available = (juror.maxAssignments ?? 10) - juror.currentAssignments return `${available} assignment slot${available !== 1 ? 's' : ''} available` })()}

)}
{/* Project Select */}
{/* Stats */} {stats && (
Total Judge Assignments
{stats.totalAssignments}
Completed
{stats.completedAssignments}

{stats.completionPercentage}% complete

Projects Covered
{stats.projectsWithFullCoverage}/{stats.totalProjects}

{stats.coveragePercentage}% have {round.requiredReviews}+ reviews

Jury Members
{stats.juryMembersAssigned}

assigned to projects

)} {/* Coverage Progress */} {stats && ( Project Coverage {stats.projectsWithFullCoverage} of {stats.totalProjects} projects have at least {round.requiredReviews} reviewers assigned )} {/* Smart Suggestions with Tabs */} Smart Assignment Suggestions Get assignment recommendations using algorithmic matching or AI-powered analysis { setActiveTab(v as 'algorithm' | 'ai') setSelectedSuggestions(new Set()) }}>
Algorithm {algorithmicSuggestions && algorithmicSuggestions.length > 0 && ( {algorithmicSuggestions.length} )} AI Powered {aiSuggestions.length > 0 && ( {aiSuggestions.length} )} {isAIJobRunning && ( )} {/* Tab-specific actions */} {activeTab === 'algorithm' ? ( ) : (
{!isAIJobRunning && ( )}
)}
{/* Algorithm Tab Content */}
Algorithmic recommendations based on tag matching and workload balance
{loadingAlgorithmic ? (
) : algorithmicSuggestions && algorithmicSuggestions.length > 0 ? ( ) : (

All projects are covered!

No additional assignments are needed at this time

)}
{/* AI Tab Content */}
GPT-powered recommendations analyzing project descriptions and judge expertise
{/* AI Job Progress Indicator */} {isAIJobRunning && jobStatus && (

AI Assignment Analysis in Progress

Processing {jobStatus.totalProjects} projects in {jobStatus.totalBatches} batches

Batch {jobStatus.currentBatch} of {jobStatus.totalBatches}
{jobStatus.processedCount} of {jobStatus.totalProjects} projects processed {aiJobProgressPercent}%
)} {isAIJobRunning ? null : loadingAI ? (
) : aiSuggestions.length > 0 ? ( ) : !hasStoredAISuggestions ? (

No AI analysis yet

Click "Start Analysis" to generate AI-powered suggestions

) : (

All projects are covered!

No additional assignments are needed at this time

)}
{/* Current Judge Assignments */} Current Judge Assignments View and manage existing project-to-judge assignments {Object.keys(assignmentsByProject).length > 0 ? (
{Object.entries(assignmentsByProject).map( ([projectId, { project, assignments: projectAssignments }]) => (

{project.title}

{projectAssignments.length} reviewer {projectAssignments.length !== 1 ? 's' : ''} {projectAssignments.length >= round.requiredReviews && ( Full coverage )}
{projectAssignments.map((assignment) => (
{assignment.user.name || assignment.user.email} {assignment.evaluation?.status === 'SUBMITTED' ? ( Submitted ) : assignment.evaluation?.status === 'DRAFT' ? ( In Progress ) : ( Pending )}
Remove Assignment? This will remove {assignment.user.name || assignment.user.email} from evaluating this project. This action cannot be undone. Cancel deleteAssignment.mutate({ id: assignment.id }) } className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Remove
))}
) )}
) : (

No Judge Assignments Yet

Use the smart suggestions above or manually assign judges to projects

)}
) } function AssignmentsSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
) } export default function AssignmentManagementPage({ params }: PageProps) { const { id } = use(params) return ( }> ) }