Fix AI filtering bugs, add special award shortlist integration
Part 1 - Bug Fixes:
- Fix toProjectWithRelations() stripping file fields needed by AI (detectedLang, textContent, etc.)
- Fix parseAIData() reading flat when aiScreeningJson is nested under rule ID
- Fix getAIConfidenceScore() with same nesting issue (always returned 0)
Part 2 - Special Award Track Integration:
- Add shortlistSize to SpecialAward, qualityScore/shortlisted/confirmed fields to AwardEligibility
- Add specialAwardId to Round for award-owned rounds
- Update AI eligibility service to return qualityScore (0-100) for ranking
- Update eligibility job with filteringRoundId scoping and auto-shortlist top N
- Add 8 new specialAward router procedures (listForRound, runEligibilityForRound,
listShortlist, toggleShortlisted, confirmShortlist, listRounds, createRound, deleteRound)
- Create award-shortlist.tsx component with ranked table, shortlist checkboxes, confirm dialog
- Add "Special Award Tracks" section to filtering dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:38:31 +01:00
'use client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Progress } from '@/components/ui/progress'
import { Skeleton } from '@/components/ui/skeleton'
import {
AlertDialog ,
AlertDialogAction ,
AlertDialogCancel ,
AlertDialogContent ,
AlertDialogDescription ,
AlertDialogFooter ,
AlertDialogHeader ,
AlertDialogTitle ,
AlertDialogTrigger ,
} from '@/components/ui/alert-dialog'
import {
Collapsible ,
CollapsibleContent ,
CollapsibleTrigger ,
} from '@/components/ui/collapsible'
import {
Award ,
ChevronDown ,
ChevronUp ,
Loader2 ,
CheckCircle2 ,
Play ,
Star ,
Trophy ,
2026-02-17 19:53:20 +01:00
AlertTriangle ,
Fix AI filtering bugs, add special award shortlist integration
Part 1 - Bug Fixes:
- Fix toProjectWithRelations() stripping file fields needed by AI (detectedLang, textContent, etc.)
- Fix parseAIData() reading flat when aiScreeningJson is nested under rule ID
- Fix getAIConfidenceScore() with same nesting issue (always returned 0)
Part 2 - Special Award Track Integration:
- Add shortlistSize to SpecialAward, qualityScore/shortlisted/confirmed fields to AwardEligibility
- Add specialAwardId to Round for award-owned rounds
- Update AI eligibility service to return qualityScore (0-100) for ranking
- Update eligibility job with filteringRoundId scoping and auto-shortlist top N
- Add 8 new specialAward router procedures (listForRound, runEligibilityForRound,
listShortlist, toggleShortlisted, confirmShortlist, listRounds, createRound, deleteRound)
- Create award-shortlist.tsx component with ranked table, shortlist checkboxes, confirm dialog
- Add "Special Award Tracks" section to filtering dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:38:31 +01:00
} from 'lucide-react'
type AwardShortlistProps = {
awardId : string
roundId : string
awardName : string
criteriaText? : string | null
eligibilityMode : string
shortlistSize : number
jobStatus? : string | null
jobTotal? : number | null
jobDone? : number | null
}
export function AwardShortlist ( {
awardId ,
roundId ,
awardName ,
criteriaText ,
eligibilityMode ,
shortlistSize ,
jobStatus ,
jobTotal ,
jobDone ,
} : AwardShortlistProps ) {
const [ expanded , setExpanded ] = useState ( false )
const utils = trpc . useUtils ( )
const isRunning = jobStatus === 'PENDING' || jobStatus === 'PROCESSING'
const { data : shortlist , isLoading : isLoadingShortlist } = trpc . specialAward . listShortlist . useQuery (
{ awardId , perPage : 100 } ,
{ enabled : expanded && ! isRunning }
)
const { data : jobPoll } = trpc . specialAward . getEligibilityJobStatus . useQuery (
{ awardId } ,
{ enabled : isRunning , refetchInterval : 3000 }
)
const runMutation = trpc . specialAward . runEligibilityForRound . useMutation ( {
onSuccess : ( ) = > {
toast . success ( 'Eligibility evaluation started' )
utils . specialAward . getEligibilityJobStatus . invalidate ( { awardId } )
utils . specialAward . listForRound . invalidate ( { roundId } )
} ,
onError : ( err ) = > toast . error ( ` Failed: ${ err . message } ` ) ,
} )
const toggleMutation = trpc . specialAward . toggleShortlisted . useMutation ( {
onSuccess : ( data ) = > {
utils . specialAward . listShortlist . invalidate ( { awardId } )
utils . specialAward . listForRound . invalidate ( { roundId } )
toast . success ( data . shortlisted ? 'Added to shortlist' : 'Removed from shortlist' )
} ,
onError : ( err ) = > toast . error ( ` Failed: ${ err . message } ` ) ,
} )
2026-02-17 19:53:20 +01:00
const { data : awardRounds } = trpc . specialAward . listRounds . useQuery (
{ awardId } ,
{ enabled : expanded && eligibilityMode === 'SEPARATE_POOL' }
)
const hasAwardRounds = ( awardRounds ? . length ? ? 0 ) > 0
Fix AI filtering bugs, add special award shortlist integration
Part 1 - Bug Fixes:
- Fix toProjectWithRelations() stripping file fields needed by AI (detectedLang, textContent, etc.)
- Fix parseAIData() reading flat when aiScreeningJson is nested under rule ID
- Fix getAIConfidenceScore() with same nesting issue (always returned 0)
Part 2 - Special Award Track Integration:
- Add shortlistSize to SpecialAward, qualityScore/shortlisted/confirmed fields to AwardEligibility
- Add specialAwardId to Round for award-owned rounds
- Update AI eligibility service to return qualityScore (0-100) for ranking
- Update eligibility job with filteringRoundId scoping and auto-shortlist top N
- Add 8 new specialAward router procedures (listForRound, runEligibilityForRound,
listShortlist, toggleShortlisted, confirmShortlist, listRounds, createRound, deleteRound)
- Create award-shortlist.tsx component with ranked table, shortlist checkboxes, confirm dialog
- Add "Special Award Tracks" section to filtering dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:38:31 +01:00
const confirmMutation = trpc . specialAward . confirmShortlist . useMutation ( {
onSuccess : ( data ) = > {
utils . specialAward . listShortlist . invalidate ( { awardId } )
utils . specialAward . listForRound . invalidate ( { roundId } )
toast . success (
` Confirmed ${ data . confirmedCount } projects ` +
( data . routedCount > 0 ? ` — ${ data . routedCount } routed to award track ` : '' )
)
} ,
onError : ( err ) = > toast . error ( ` Failed: ${ err . message } ` ) ,
} )
const currentJobStatus = jobPoll ? . eligibilityJobStatus ? ? jobStatus
const currentJobDone = jobPoll ? . eligibilityJobDone ? ? jobDone
const currentJobTotal = jobPoll ? . eligibilityJobTotal ? ? jobTotal
const jobProgress = currentJobTotal && currentJobTotal > 0
? Math . round ( ( ( currentJobDone ? ? 0 ) / currentJobTotal ) * 100 )
: 0
const shortlistedCount = shortlist ? . eligibilities ? . filter ( ( e ) = > e . shortlisted ) . length ? ? 0
return (
< Collapsible open = { expanded } onOpenChange = { setExpanded } >
< div className = "border rounded-lg" >
< CollapsibleTrigger asChild >
< button className = "w-full flex items-center justify-between p-4 hover:bg-muted/50 transition-colors text-left" >
< div className = "flex items-center gap-3" >
< Trophy className = "h-5 w-5 text-amber-600" / >
< div >
< h4 className = "font-semibold text-sm" > { awardName } < / h4 >
{ criteriaText && (
< p className = "text-xs text-muted-foreground line-clamp-1 max-w-md" >
{ criteriaText }
< / p >
) }
< / div >
< / div >
< div className = "flex items-center gap-3" >
< Badge variant = "outline" className = { eligibilityMode === 'SEPARATE_POOL'
? 'bg-purple-50 text-purple-700 border-purple-200'
: 'bg-blue-50 text-blue-700 border-blue-200'
} >
{ eligibilityMode === 'SEPARATE_POOL' ? 'Separate Pool' : 'Main Pool' }
< / Badge >
{ currentJobStatus === 'COMPLETED' && (
< Badge variant = "outline" className = "bg-green-50 text-green-700 border-green-200" >
< CheckCircle2 className = "h-3 w-3 mr-1" / >
Evaluated
< / Badge >
) }
{ expanded ? < ChevronUp className = "h-4 w-4" / > : < ChevronDown className = "h-4 w-4" / > }
< / div >
< / button >
< / CollapsibleTrigger >
< CollapsibleContent >
< div className = "border-t p-4 space-y-4" >
{ /* Job controls */ }
< div className = "flex items-center justify-between" >
< div className = "text-sm text-muted-foreground" >
Evaluate PASSED projects against this award & apos ; s criteria
< / div >
< Button
size = "sm"
onClick = { ( ) = > runMutation . mutate ( { awardId , roundId } ) }
disabled = { runMutation . isPending || isRunning }
>
{ isRunning ? (
< > < Loader2 className = "h-4 w-4 mr-2 animate-spin" / > Processing . . . < / >
) : runMutation . isPending ? (
< > < Loader2 className = "h-4 w-4 mr-2 animate-spin" / > Starting . . . < / >
) : currentJobStatus === 'COMPLETED' ? (
< > < Play className = "h-4 w-4 mr-2" / > Re - evaluate < / >
) : (
< > < Play className = "h-4 w-4 mr-2" / > Run Eligibility < / >
) }
< / Button >
< / div >
{ /* Progress bar */ }
{ isRunning && currentJobTotal && currentJobTotal > 0 && (
< div className = "space-y-1" >
< Progress value = { jobProgress } className = "h-2" / >
< p className = "text-xs text-muted-foreground text-right" >
{ currentJobDone ? ? 0 } / { currentJobTotal } projects
< / p >
< / div >
) }
{ /* Shortlist table */ }
{ expanded && currentJobStatus === 'COMPLETED' && (
< >
{ isLoadingShortlist ? (
< div className = "space-y-2" >
{ [ 1 , 2 , 3 ] . map ( ( i ) = > < Skeleton key = { i } className = "h-12 w-full" / > ) }
< / div >
) : shortlist && shortlist . eligibilities . length > 0 ? (
< div className = "space-y-3" >
< div className = "flex items-center justify-between" >
< p className = "text-sm font-medium" >
{ shortlist . total } eligible projects
{ shortlistedCount > 0 && (
< span className = "text-muted-foreground ml-1" >
( { shortlistedCount } shortlisted )
< / span >
) }
< / p >
{ shortlistedCount > 0 && (
< AlertDialog >
< AlertDialogTrigger asChild >
< Button size = "sm" variant = "default" >
< CheckCircle2 className = "h-4 w-4 mr-2" / >
Confirm Shortlist ( { shortlistedCount } )
< / Button >
< / AlertDialogTrigger >
< AlertDialogContent >
< AlertDialogHeader >
< AlertDialogTitle > Confirm Shortlist < / AlertDialogTitle >
2026-02-17 19:53:20 +01:00
< AlertDialogDescription asChild >
< div className = "space-y-2" >
< p >
{ eligibilityMode === 'SEPARATE_POOL'
? ` This will confirm ${ shortlistedCount } projects for the " ${ awardName } " award track. Projects will be routed to the award's rounds for separate evaluation. `
: ` This will confirm ${ shortlistedCount } projects as eligible for the " ${ awardName } " award. Projects remain in the main competition pool. `
}
< / p >
{ eligibilityMode === 'SEPARATE_POOL' && ! hasAwardRounds && (
< div className = "flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300" >
< AlertTriangle className = "h-4 w-4 mt-0.5 shrink-0" / >
< p className = "text-sm" >
No award rounds have been created yet . Projects will be confirmed but < strong > not routed < / strong > to an evaluation track . Create rounds on the award page first .
< / p >
< / div >
) }
< / div >
Fix AI filtering bugs, add special award shortlist integration
Part 1 - Bug Fixes:
- Fix toProjectWithRelations() stripping file fields needed by AI (detectedLang, textContent, etc.)
- Fix parseAIData() reading flat when aiScreeningJson is nested under rule ID
- Fix getAIConfidenceScore() with same nesting issue (always returned 0)
Part 2 - Special Award Track Integration:
- Add shortlistSize to SpecialAward, qualityScore/shortlisted/confirmed fields to AwardEligibility
- Add specialAwardId to Round for award-owned rounds
- Update AI eligibility service to return qualityScore (0-100) for ranking
- Update eligibility job with filteringRoundId scoping and auto-shortlist top N
- Add 8 new specialAward router procedures (listForRound, runEligibilityForRound,
listShortlist, toggleShortlisted, confirmShortlist, listRounds, createRound, deleteRound)
- Create award-shortlist.tsx component with ranked table, shortlist checkboxes, confirm dialog
- Add "Special Award Tracks" section to filtering dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:38:31 +01:00
< / AlertDialogDescription >
< / AlertDialogHeader >
< AlertDialogFooter >
< AlertDialogCancel > Cancel < / AlertDialogCancel >
< AlertDialogAction
onClick = { ( ) = > confirmMutation . mutate ( { awardId } ) }
disabled = { confirmMutation . isPending }
>
{ confirmMutation . isPending ? 'Confirming...' : 'Confirm' }
< / AlertDialogAction >
< / AlertDialogFooter >
< / AlertDialogContent >
< / AlertDialog >
) }
< / div >
< div className = "border rounded-md overflow-hidden" >
< table className = "w-full text-sm" >
< thead className = "bg-muted/50" >
< tr >
< th className = "px-3 py-2 text-left w-8" > # < / th >
< th className = "px-3 py-2 text-left" > Project < / th >
< th className = "px-3 py-2 text-left w-24" > Score < / th >
< th className = "px-3 py-2 text-left w-32" > Reasoning < / th >
< th className = "px-3 py-2 text-center w-20" > Shortlist < / th >
< / tr >
< / thead >
< tbody >
{ shortlist . eligibilities . map ( ( e , i ) = > {
const reasoning = ( e . aiReasoningJson as Record < string , unknown > ) ? . reasoning as string | undefined
return (
< tr key = { e . id } className = { ` border-t ${ e . shortlisted ? 'bg-amber-50/50' : '' } ` } >
< td className = "px-3 py-2 text-muted-foreground font-mono" >
{ i + 1 }
< / td >
< td className = "px-3 py-2" >
< div >
< p className = "font-medium" > { e . project . title } < / p >
< p className = "text-xs text-muted-foreground" >
{ e . project . teamName || e . project . country || e . project . competitionCategory || '—' }
< / p >
< / div >
< / td >
< td className = "px-3 py-2" >
< div className = "flex items-center gap-2" >
< Progress
value = { e . qualityScore ? ? 0 }
className = "h-2 w-16"
/ >
< span className = "text-xs font-mono font-medium" >
{ e . qualityScore ? ? 0 }
< / span >
< / div >
< / td >
< td className = "px-3 py-2" >
{ reasoning ? (
< p className = "text-xs text-muted-foreground line-clamp-2" title = { reasoning } >
{ reasoning }
< / p >
) : (
< span className = "text-xs text-muted-foreground" > — < / span >
) }
< / td >
< td className = "px-3 py-2 text-center" >
< Checkbox
checked = { e . shortlisted }
onCheckedChange = { ( ) = >
toggleMutation . mutate ( { awardId , projectId : e.project.id } )
}
disabled = { toggleMutation . isPending }
/ >
< / td >
< / tr >
)
} ) }
< / tbody >
< / table >
< / div >
< / div >
) : (
< p className = "text-sm text-muted-foreground text-center py-4" >
No eligible projects found
< / p >
) }
< / >
) }
{ /* Not yet evaluated */ }
{ expanded && ! currentJobStatus && (
< p className = "text-sm text-muted-foreground text-center py-4" >
Click & quot ; Run Eligibility & quot ; to evaluate projects against this award & apos ; s criteria
< / p >
) }
{ /* Failed */ }
{ currentJobStatus === 'FAILED' && (
< p className = "text-sm text-red-600 text-center py-2" >
Eligibility evaluation failed . Try again .
< / p >
) }
< / div >
< / CollapsibleContent >
< / div >
< / Collapsible >
)
}