AI-powered assignment generation with enriched data and streaming UI
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m19s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m19s
- Add aiPreview mutation with full project/juror data (bios, descriptions, documents, categories, ocean issues, countries, team sizes) - Increase AI description limit from 300 to 2000 chars for richer context - Update GPT system prompt to use all available data fields - Add mode toggle (AI default / Algorithm fallback) in assignment preview - Lift AI mutation to parent page for background generation persistence - Show visual indicator on page while AI generates (spinner + progress card) - Toast notification with "Review" action when AI completes - Staggered reveal animation for assignment results (streaming feel) - Fix assignment balance with dynamic penalty (25pts per existing assignment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { ArrowLeft, PlayCircle } from 'lucide-react'
|
||||
import { ArrowLeft, Loader2, PlayCircle, Zap } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -26,6 +27,16 @@ export default function AssignmentsDashboardPage() {
|
||||
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
|
||||
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
|
||||
|
||||
const aiAssignmentMutation = trpc.roundAssignment.aiPreview.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('AI assignments ready!', {
|
||||
action: { label: 'Review', onClick: () => setPreviewSheetOpen(true) },
|
||||
duration: 10000,
|
||||
})
|
||||
},
|
||||
onError: (err) => toast.error(`AI generation failed: ${err.message}`),
|
||||
})
|
||||
|
||||
const { data: competition, isLoading: isLoadingCompetition } = trpc.competition.getById.useQuery({
|
||||
id: competitionId,
|
||||
})
|
||||
@@ -104,11 +115,24 @@ export default function AssignmentsDashboardPage() {
|
||||
|
||||
{selectedRoundId && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => setPreviewSheetOpen(true)}>
|
||||
<PlayCircle className="mr-2 h-4 w-4" />
|
||||
Generate Assignments
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
aiAssignmentMutation.mutate({ roundId: selectedRoundId, requiredReviews })
|
||||
}}
|
||||
disabled={aiAssignmentMutation.isPending}
|
||||
>
|
||||
{aiAssignmentMutation.isPending ? (
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Generating...</>
|
||||
) : (
|
||||
<><Zap className="mr-2 h-4 w-4" />{aiAssignmentMutation.data ? 'Regenerate' : 'Generate with AI'}</>
|
||||
)}
|
||||
</Button>
|
||||
{aiAssignmentMutation.data && (
|
||||
<Button variant="outline" onClick={() => setPreviewSheetOpen(true)}>
|
||||
Review Assignments
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="coverage" className="w-full">
|
||||
@@ -170,6 +194,10 @@ export default function AssignmentsDashboardPage() {
|
||||
open={previewSheetOpen}
|
||||
onOpenChange={setPreviewSheetOpen}
|
||||
requiredReviews={requiredReviews}
|
||||
aiResult={aiAssignmentMutation.data ?? null}
|
||||
isAIGenerating={aiAssignmentMutation.isPending}
|
||||
onGenerateAI={() => aiAssignmentMutation.mutate({ roundId: selectedRoundId, requiredReviews })}
|
||||
onResetAI={() => aiAssignmentMutation.reset()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -170,6 +170,22 @@ export default function RoundDetailPage() {
|
||||
const pendingSaveRef = useRef(false)
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
|
||||
|
||||
// AI assignment generation (lifted to page level so it persists when sheet closes)
|
||||
const aiAssignmentMutation = trpc.roundAssignment.aiPreview.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('AI assignments ready!', {
|
||||
action: {
|
||||
label: 'Review',
|
||||
onClick: () => setPreviewSheetOpen(true),
|
||||
},
|
||||
duration: 10000,
|
||||
})
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`AI generation failed: ${err.message}`)
|
||||
},
|
||||
})
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
const [advanceDialogOpen, setAdvanceDialogOpen] = useState(false)
|
||||
const [aiRecommendations, setAiRecommendations] = useState<{
|
||||
@@ -1514,23 +1530,62 @@ export default function RoundDetailPage() {
|
||||
<CoverageReport roundId={roundId} requiredReviews={(config.requiredReviewsPerProject as number) || 3} />
|
||||
|
||||
{/* Generate Assignments */}
|
||||
<Card>
|
||||
<Card className={cn(aiAssignmentMutation.isPending && 'border-violet-300 shadow-violet-100 shadow-sm')}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">Assignment Generation</CardTitle>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
Assignment Generation
|
||||
{aiAssignmentMutation.isPending && (
|
||||
<Badge variant="outline" className="gap-1.5 text-violet-600 border-violet-300 animate-pulse">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
AI generating...
|
||||
</Badge>
|
||||
)}
|
||||
{aiAssignmentMutation.data && !aiAssignmentMutation.isPending && (
|
||||
<Badge variant="outline" className="gap-1 text-emerald-600 border-emerald-300">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{aiAssignmentMutation.data.stats.assignmentsGenerated} ready
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
AI-suggested jury-to-project assignments based on expertise and workload
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setPreviewSheetOpen(true)}
|
||||
disabled={projectCount === 0 || !juryGroup}
|
||||
>
|
||||
<Zap className="h-4 w-4 mr-1.5" />
|
||||
Generate Assignments
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{aiAssignmentMutation.data && !aiAssignmentMutation.isPending && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setPreviewSheetOpen(true)}
|
||||
>
|
||||
Review Assignments
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
aiAssignmentMutation.mutate({
|
||||
roundId,
|
||||
requiredReviews: (config.requiredReviewsPerProject as number) || 3,
|
||||
})
|
||||
}}
|
||||
disabled={projectCount === 0 || !juryGroup || aiAssignmentMutation.isPending}
|
||||
>
|
||||
{aiAssignmentMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-4 w-4 mr-1.5" />
|
||||
{aiAssignmentMutation.data ? 'Regenerate' : 'Generate with AI'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -1546,12 +1601,41 @@ export default function RoundDetailPage() {
|
||||
Add projects to this round first.
|
||||
</div>
|
||||
)}
|
||||
{juryGroup && projectCount > 0 && (
|
||||
{juryGroup && projectCount > 0 && !aiAssignmentMutation.isPending && !aiAssignmentMutation.data && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Click "Generate Assignments" to preview AI-suggested assignments.
|
||||
You can review and execute them from the preview sheet.
|
||||
Click "Generate with AI" to create assignments using GPT analysis of juror expertise, project descriptions, and documents. Or open the preview to use the algorithm instead.
|
||||
</p>
|
||||
)}
|
||||
{aiAssignmentMutation.isPending && (
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-violet-50 border border-violet-200 dark:bg-violet-950/20 dark:border-violet-800">
|
||||
<div className="relative">
|
||||
<div className="h-8 w-8 rounded-full border-2 border-violet-300 border-t-violet-600 animate-spin" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-violet-800 dark:text-violet-200">AI is analyzing projects and jurors...</p>
|
||||
<p className="text-xs text-violet-600 dark:text-violet-400">
|
||||
Matching expertise, reviewing bios, and balancing workloads
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{aiAssignmentMutation.data && !aiAssignmentMutation.isPending && (
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-emerald-50 border border-emerald-200 dark:bg-emerald-950/20 dark:border-emerald-800">
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-600 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-emerald-800 dark:text-emerald-200">
|
||||
{aiAssignmentMutation.data.stats.assignmentsGenerated} assignments generated
|
||||
</p>
|
||||
<p className="text-xs text-emerald-600 dark:text-emerald-400">
|
||||
{aiAssignmentMutation.data.stats.totalJurors} jurors, {aiAssignmentMutation.data.stats.totalProjects} projects
|
||||
{aiAssignmentMutation.data.fallbackUsed && ' (algorithm fallback)'}
|
||||
</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={() => setPreviewSheetOpen(true)}>
|
||||
Review & Execute
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1582,6 +1666,13 @@ export default function RoundDetailPage() {
|
||||
open={previewSheetOpen}
|
||||
onOpenChange={setPreviewSheetOpen}
|
||||
requiredReviews={(config.requiredReviewsPerProject as number) || 3}
|
||||
aiResult={aiAssignmentMutation.data ?? null}
|
||||
isAIGenerating={aiAssignmentMutation.isPending}
|
||||
onGenerateAI={() => aiAssignmentMutation.mutate({
|
||||
roundId,
|
||||
requiredReviews: (config.requiredReviewsPerProject as number) || 3,
|
||||
})}
|
||||
onResetAI={() => aiAssignmentMutation.reset()}
|
||||
/>
|
||||
|
||||
{/* CSV Export Dialog */}
|
||||
|
||||
Reference in New Issue
Block a user