Special awards: Rounds tab UI, auto-filter threshold, remove auto-tag rules
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
- Add Rounds tab to award detail page with create/list/delete functionality - Add "Entry point" badge on first award round (confirmShortlist routes here) - Fix round detail back-link to navigate to parent award when specialAwardId set - Filter award rounds out of competition round list - Add specialAwardId to competition getById round select - Warn on confirmShortlist when no award rounds exist (SEPARATE_POOL mode) - Remove auto-tag rules from award config, edit page, router, and AI service - Fix competitionId not passed when creating awards from competition context - Add AUTO_FILTER quality threshold to AI filtering dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
Play,
|
||||
Star,
|
||||
Trophy,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react'
|
||||
|
||||
type AwardShortlistProps = {
|
||||
@@ -92,6 +93,12 @@ export function AwardShortlist({
|
||||
onError: (err) => toast.error(`Failed: ${err.message}`),
|
||||
})
|
||||
|
||||
const { data: awardRounds } = trpc.specialAward.listRounds.useQuery(
|
||||
{ awardId },
|
||||
{ enabled: expanded && eligibilityMode === 'SEPARATE_POOL' }
|
||||
)
|
||||
const hasAwardRounds = (awardRounds?.length ?? 0) > 0
|
||||
|
||||
const confirmMutation = trpc.specialAward.confirmShortlist.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.specialAward.listShortlist.invalidate({ awardId })
|
||||
@@ -210,11 +217,23 @@ export function AwardShortlist({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Shortlist</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{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.`
|
||||
}
|
||||
<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>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -107,9 +107,10 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
|
||||
// AI criteria state
|
||||
const [criteriaText, setCriteriaText] = useState('')
|
||||
const [aiAction, setAiAction] = useState<'PASS' | 'REJECT' | 'FLAG'>('FLAG')
|
||||
const [aiAction, setAiAction] = useState<'PASS' | 'REJECT' | 'FLAG' | 'AUTO_FILTER'>('FLAG')
|
||||
const [batchSize, setBatchSize] = useState(20)
|
||||
const [parallelBatches, setParallelBatches] = useState(3)
|
||||
const [autoFilterThreshold, setAutoFilterThreshold] = useState(4)
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||
const [criteriaDirty, setCriteriaDirty] = useState(false)
|
||||
const criteriaLoaded = useRef(false)
|
||||
@@ -168,9 +169,10 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
if (aiRule && !criteriaLoaded.current) {
|
||||
const config = (aiRule.configJson || {}) as Record<string, unknown>
|
||||
setCriteriaText((config.criteriaText as string) || '')
|
||||
setAiAction((config.action as 'PASS' | 'REJECT' | 'FLAG') || 'FLAG')
|
||||
setAiAction((config.action as 'PASS' | 'REJECT' | 'FLAG' | 'AUTO_FILTER') || 'FLAG')
|
||||
setBatchSize((config.batchSize as number) || 20)
|
||||
setParallelBatches((config.parallelBatches as number) || 3)
|
||||
setAutoFilterThreshold((config.autoFilterThreshold as number) || 4)
|
||||
criteriaLoaded.current = true
|
||||
}
|
||||
}, [aiRule])
|
||||
@@ -269,6 +271,7 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
action: aiAction,
|
||||
batchSize,
|
||||
parallelBatches,
|
||||
...(aiAction === 'AUTO_FILTER' && { autoFilterThreshold }),
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -296,6 +299,7 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
action: aiAction,
|
||||
batchSize,
|
||||
parallelBatches,
|
||||
...(aiAction === 'AUTO_FILTER' && { autoFilterThreshold }),
|
||||
}
|
||||
|
||||
if (aiRule) {
|
||||
@@ -519,42 +523,87 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
{advancedOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-3">
|
||||
<div className="grid grid-cols-3 gap-3 rounded-lg border p-3 bg-muted/20">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Default Action</Label>
|
||||
<Select value={aiAction} onValueChange={(v) => { setAiAction(v as 'PASS' | 'REJECT' | 'FLAG'); setCriteriaDirty(true) }}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="FLAG">Flag for review</SelectItem>
|
||||
<SelectItem value="REJECT">Auto-reject failures</SelectItem>
|
||||
<SelectItem value="PASS">Auto-pass matches</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Batch Size</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
className="h-8 text-xs"
|
||||
value={batchSize}
|
||||
onChange={(e) => { setBatchSize(parseInt(e.target.value) || 20); setCriteriaDirty(true) }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Parallel Batches</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
className="h-8 text-xs"
|
||||
value={parallelBatches}
|
||||
onChange={(e) => { setParallelBatches(parseInt(e.target.value) || 3); setCriteriaDirty(true) }}
|
||||
/>
|
||||
<div className="space-y-3 rounded-lg border p-3 bg-muted/20">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Default Action</Label>
|
||||
<Select value={aiAction} onValueChange={(v) => { setAiAction(v as 'PASS' | 'REJECT' | 'FLAG' | 'AUTO_FILTER'); setCriteriaDirty(true) }}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AUTO_FILTER">Smart auto-filter</SelectItem>
|
||||
<SelectItem value="FLAG">Flag for review</SelectItem>
|
||||
<SelectItem value="REJECT">Auto-reject failures</SelectItem>
|
||||
<SelectItem value="PASS">Auto-pass matches</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Batch Size</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
className="h-8 text-xs"
|
||||
value={batchSize}
|
||||
onChange={(e) => { setBatchSize(parseInt(e.target.value) || 20); setCriteriaDirty(true) }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Parallel Batches</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
className="h-8 text-xs"
|
||||
value={parallelBatches}
|
||||
onChange={(e) => { setParallelBatches(parseInt(e.target.value) || 3); setCriteriaDirty(true) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiAction === 'AUTO_FILTER' && (
|
||||
<div className="rounded-md border border-purple-200 bg-purple-50/50 p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">Quality threshold</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Projects scoring at or below this are auto-rejected as spam/junk
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={9}
|
||||
className="h-8 w-16 text-xs text-center"
|
||||
value={autoFilterThreshold}
|
||||
onChange={(e) => {
|
||||
const v = Math.min(9, Math.max(1, parseInt(e.target.value) || 4))
|
||||
setAutoFilterThreshold(v)
|
||||
setCriteriaDirty(true)
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">/10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1.5 text-xs">
|
||||
<div className="rounded bg-red-100 border border-red-200 px-2 py-1.5 text-center">
|
||||
<span className="font-medium text-red-800">Auto-reject</span>
|
||||
<p className="text-red-600 mt-0.5">Score 1-{autoFilterThreshold} or spam</p>
|
||||
</div>
|
||||
<div className="rounded bg-amber-100 border border-amber-200 px-2 py-1.5 text-center">
|
||||
<span className="font-medium text-amber-800">Flag for review</span>
|
||||
<p className="text-amber-600 mt-0.5">Score {autoFilterThreshold + 1}+ but fails criteria</p>
|
||||
</div>
|
||||
<div className="rounded bg-green-100 border border-green-200 px-2 py-1.5 text-center">
|
||||
<span className="font-medium text-green-800">Auto-pass</span>
|
||||
<p className="text-green-600 mt-0.5">Meets criteria</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
Reference in New Issue
Block a user