'use client' import { useState, useEffect, useMemo } from 'react' import { AlertTriangle, Bot, CheckCircle2, ChevronDown, ChevronRight, Loader2, Plus, Sparkles, Tag, User, X, } from 'lucide-react' import { toast } from 'sonner' import { trpc } from '@/lib/trpc/client' import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' type EditableAssignment = { localId: string userId: string userName: string projectId: string projectTitle: string score: number reasoning: string[] matchingTags: string[] policyViolations: string[] fromIntent: boolean isManual: boolean } type AssignmentPreviewSheetProps = { roundId: string open: boolean onOpenChange: (open: boolean) => void requiredReviews?: number } export function AssignmentPreviewSheet({ roundId, open, onOpenChange, requiredReviews = 3, }: AssignmentPreviewSheetProps) { const utils = trpc.useUtils() const [assignments, setAssignments] = useState([]) const [initialized, setInitialized] = useState(false) const [expandedJurors, setExpandedJurors] = useState>(new Set()) const [addJurorId, setAddJurorId] = useState('') const [addProjectId, setAddProjectId] = useState('') // ── Queries ────────────────────────────────────────────────────────────── const { data: preview, isLoading, refetch, } = trpc.roundAssignment.preview.useQuery( { roundId, honorIntents: true, requiredReviews }, { enabled: open }, ) // Fetch round data for jury group members const { data: round } = trpc.round.getById.useQuery( { id: roundId }, { enabled: open }, ) // Fetch projects in this round const { data: projectStates } = trpc.roundEngine.getProjectStates.useQuery( { roundId }, { enabled: open }, ) // Fetch existing assignments to exclude already-assigned pairs const { data: existingAssignments } = trpc.assignment.listByStage.useQuery( { roundId }, { enabled: open }, ) const { mutate: execute, isPending: isExecuting } = trpc.roundAssignment.execute.useMutation({ onSuccess: (result) => { toast.success(`Created ${result.created} assignments`) utils.roundAssignment.coverageReport.invalidate({ roundId }) utils.roundAssignment.unassignedQueue.invalidate({ roundId }) utils.assignment.listByStage.invalidate({ roundId }) utils.roundEngine.getProjectStates.invalidate({ roundId }) onOpenChange(false) }, onError: (err) => { toast.error(err.message) }, }) // ── Initialize local state from preview ────────────────────────────────── useEffect(() => { if (preview && !initialized) { const mapped: EditableAssignment[] = preview.assignments.map( (a: any, idx: number) => ({ localId: `ai-${idx}`, userId: a.userId, userName: a.userName, projectId: a.projectId, projectTitle: a.projectTitle, score: a.score, reasoning: a.reasoning ?? [], matchingTags: a.matchingTags ?? [], policyViolations: a.policyViolations ?? [], fromIntent: a.fromIntent ?? false, isManual: false, }), ) setAssignments(mapped) // Auto-expand all jurors const jurorIds = new Set(mapped.map((a) => a.userId)) setExpandedJurors(jurorIds) setInitialized(true) } }, [preview, initialized]) // Reset when sheet closes useEffect(() => { if (!open) { setInitialized(false) setAssignments([]) setExpandedJurors(new Set()) setAddJurorId('') setAddProjectId('') } }, [open]) // ── Derived data ───────────────────────────────────────────────────────── const juryMembers = useMemo(() => { if (!round?.juryGroup?.members) return [] return round.juryGroup.members.map((m: any) => ({ userId: m.userId, name: m.user?.name ?? m.userId, role: m.role, })) }, [round]) const projects = useMemo((): Array<{ id: string; title: string; category?: string }> => { if (!projectStates) return [] return projectStates.map((ps: any) => ({ id: ps.project?.id ?? ps.projectId, title: ps.project?.title ?? ps.projectId, category: ps.project?.competitionCategory, })) }, [projectStates]) // Build set of existing assignment pairs (already committed) const existingPairs = useMemo(() => { const pairs = new Set() for (const a of existingAssignments ?? []) { pairs.add(`${a.userId}:${a.projectId}`) } return pairs }, [existingAssignments]) // Build set of current preview assignment pairs const currentPairs = useMemo(() => { const pairs = new Set() for (const a of assignments) { pairs.add(`${a.userId}:${a.projectId}`) } return pairs }, [assignments]) // Group assignments by juror const groupedByJuror = useMemo(() => { const map = new Map< string, { userId: string; userName: string; assignments: EditableAssignment[] } >() for (const a of assignments) { if (!map.has(a.userId)) { map.set(a.userId, { userId: a.userId, userName: a.userName, assignments: [] }) } map.get(a.userId)!.assignments.push(a) } return Array.from(map.values()).sort((a, b) => b.assignments.length - a.assignments.length, ) }, [assignments]) // Stats const totalAssignments = assignments.length const uniqueProjects = new Set(assignments.map((a) => a.projectId)).size const uniqueJurors = new Set(assignments.map((a) => a.userId)).size const manualCount = assignments.filter((a) => a.isManual).length const removedCount = (preview?.stats.assignmentsGenerated ?? 0) - assignments.filter((a) => !a.isManual).length // Projects available for a specific juror (not already assigned in preview or existing) const getAvailableProjectsForJuror = (userId: string) => { return projects.filter((p) => { const pairKey = `${userId}:${p.id}` return !currentPairs.has(pairKey) && !existingPairs.has(pairKey) }) } // ── Actions ────────────────────────────────────────────────────────────── const removeAssignment = (localId: string) => { setAssignments((prev) => prev.filter((a) => a.localId !== localId)) } const addAssignment = () => { if (!addJurorId || !addProjectId) { toast.error('Select both a juror and a project') return } const juror = juryMembers.find((m) => m.userId === addJurorId) const project = projects.find((p) => p.id === addProjectId) if (!juror || !project) return const pairKey = `${addJurorId}:${addProjectId}` if (currentPairs.has(pairKey) || existingPairs.has(pairKey)) { toast.error('This assignment already exists') return } setAssignments((prev) => [ ...prev, { localId: `manual-${Date.now()}`, userId: addJurorId, userName: juror.name, projectId: addProjectId, projectTitle: project.title, score: 0, reasoning: ['Manually added by admin'], matchingTags: [], policyViolations: [], fromIntent: false, isManual: true, }, ]) // Expand the juror's section setExpandedJurors((prev) => new Set([...prev, addJurorId])) setAddProjectId('') } const addProjectToJuror = (userId: string, projectId: string) => { const juror = juryMembers.find((m) => m.userId === userId) const project = projects.find((p) => p.id === projectId) if (!juror || !project) return setAssignments((prev) => [ ...prev, { localId: `manual-${Date.now()}`, userId, userName: juror.name, projectId, projectTitle: project.title, score: 0, reasoning: ['Manually added by admin'], matchingTags: [], policyViolations: [], fromIntent: false, isManual: true, }, ]) } const toggleJuror = (userId: string) => { setExpandedJurors((prev) => { const next = new Set(prev) if (next.has(userId)) next.delete(userId) else next.add(userId) return next }) } const handleExecute = () => { if (assignments.length === 0) { toast.error('No assignments to execute') return } execute({ roundId, assignments: assignments.map((a) => ({ userId: a.userId, projectId: a.projectId, })), }) } // ── Render ─────────────────────────────────────────────────────────────── return ( Assignment Preview AI Suggested Review and fine-tune before executing.
{isLoading ? (
{[1, 2, 3, 4].map((i) => ( ))}
) : preview ? ( <> {/* ── Summary stats ── */}
{[ { label: 'Assignments', value: totalAssignments }, { label: 'Projects', value: uniqueProjects }, { label: 'Jurors', value: uniqueJurors }, { label: 'Unassigned', value: Math.max(0, (preview.stats.totalProjects ?? 0) - uniqueProjects) }, ].map((stat) => (

{stat.value}

{stat.label}

))}
{/* Modification indicator */} {(manualCount > 0 || removedCount > 0) && (
{manualCount > 0 && ( {manualCount} added manually )} {removedCount > 0 && ( {removedCount} removed )}
)} {/* ── Warnings ── */} {preview.warnings && preview.warnings.length > 0 && (
{preview.warnings.map((w: string, idx: number) => (

{w}

))}
)} {/* ── Juror groups ── */}

Assignments by Juror

{groupedByJuror.length === 0 ? ( No assignments. Add some manually below. ) : ( groupedByJuror.map((group) => ( toggleJuror(group.userId)} onRemove={removeAssignment} onAddProject={(projectId) => addProjectToJuror(group.userId, projectId) } availableProjects={getAvailableProjectsForJuror(group.userId)} requiredReviews={requiredReviews} /> )) )}
{/* ── Add assignment manually ── */} Add Assignment
) : (

No preview data available

)}
) } // ─── Juror Group Card ──────────────────────────────────────────────────────── type JurorGroupProps = { group: { userId: string userName: string assignments: EditableAssignment[] } expanded: boolean onToggle: () => void onRemove: (localId: string) => void onAddProject: (projectId: string) => void availableProjects: Array<{ id: string; title: string; category?: string }> requiredReviews: number } function JurorGroup({ group, expanded, onToggle, onRemove, onAddProject, availableProjects, requiredReviews, }: JurorGroupProps) { const [inlineProjectId, setInlineProjectId] = useState('') const handleInlineAdd = () => { if (!inlineProjectId) return onAddProject(inlineProjectId) setInlineProjectId('') } const aiCount = group.assignments.filter((a) => !a.isManual).length const manualCount = group.assignments.filter((a) => a.isManual).length const avgScore = group.assignments.length > 0 ? Math.round( group.assignments.reduce((sum, a) => sum + a.score, 0) / group.assignments.length, ) : 0 return (
{group.assignments.map((a) => ( ))} {/* Inline add project */} {availableProjects.length > 0 && (
)}
) } // ─── Assignment Row ────────────────────────────────────────────────────────── function AssignmentRow({ assignment, onRemove, }: { assignment: EditableAssignment onRemove: (localId: string) => void }) { const a = assignment return (
{a.projectTitle} {a.isManual ? ( Manual ) : a.fromIntent ? ( Intent ) : null}
{/* Score + tags + reasoning */}
{!a.isManual && a.score > 0 && ( = 50 ? 'border-green-300 text-green-700' : a.score >= 25 ? 'border-amber-300 text-amber-700' : 'border-red-300 text-red-700', )} > {Math.round(a.score)}

Match Score Breakdown

    {a.reasoning.map((r, i) => (
  • • {r}
  • ))}
)} {a.matchingTags.slice(0, 3).map((tag) => ( {tag} ))} {a.matchingTags.length > 3 && ( +{a.matchingTags.length - 3} more )}
{/* Policy violations */} {a.policyViolations.length > 0 && (
{a.policyViolations.join(', ')}
)}
) }