Add special awards management features and fix voting/assignment issues
Special Awards: - Add delete button with confirmation dialog to award detail page - Add voting window dates (start/end) to award edit page - Add manual project eligibility management (add/remove projects) - Show eligibility method (Auto/Manual) in eligibility table - Auto-set votingStartAt when opening voting if date is in future Assignment Suggestions: - Replace toggle with proper tabs UI (Algorithm vs AI Powered) - Persist AI suggestions when navigating away (stored in database) - Show suggestion counts on tab badges - Independent refresh/start buttons per tab Round Voting: - Auto-update votingStartAt to now when activating round if date is in future - Fixes issue where round was opened but voting dates were in future Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,12 @@ 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,
|
||||
@@ -64,9 +70,125 @@ import {
|
||||
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<string>
|
||||
onToggle: (key: string) => void
|
||||
onSelectAll: () => void
|
||||
onApply: () => void
|
||||
isApplying: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedSuggestions.size === suggestions.length && suggestions.length > 0}
|
||||
onCheckedChange={onSelectAll}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{selectedSuggestions.size} of {suggestions.length} selected
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onApply}
|
||||
disabled={selectedSuggestions.size === 0 || isApplying}
|
||||
>
|
||||
{isApplying ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Apply Selected ({selectedSuggestions.size})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border max-h-[400px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12"></TableHead>
|
||||
<TableHead>Juror</TableHead>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>Score</TableHead>
|
||||
<TableHead>Reasoning</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{suggestions.map((suggestion) => {
|
||||
const key = `${suggestion.userId}-${suggestion.projectId}`
|
||||
const isSelected = selectedSuggestions.has(key)
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={key}
|
||||
className={isSelected ? 'bg-muted/50' : ''}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => onToggle(key)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{suggestion.jurorName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{suggestion.projectTitle}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
suggestion.score >= 60
|
||||
? 'default'
|
||||
: suggestion.score >= 40
|
||||
? 'secondary'
|
||||
: 'outline'
|
||||
}
|
||||
>
|
||||
{suggestion.score.toFixed(0)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
<ul className="text-xs text-muted-foreground">
|
||||
{suggestion.reasoning.map((r, i) => (
|
||||
<li key={i}>{r}</li>
|
||||
))}
|
||||
</ul>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
@@ -76,7 +198,7 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
const [manualDialogOpen, setManualDialogOpen] = useState(false)
|
||||
const [selectedJuror, setSelectedJuror] = useState<string>('')
|
||||
const [selectedProject, setSelectedProject] = useState<string>('')
|
||||
const [useAI, setUseAI] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'algorithm' | 'ai'>('algorithm')
|
||||
const [activeJobId, setActiveJobId] = useState<string | null>(null)
|
||||
|
||||
const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({ id: roundId })
|
||||
@@ -84,10 +206,9 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
const { data: stats, isLoading: loadingStats } = trpc.assignment.getStats.useQuery({ roundId })
|
||||
const { data: isAIAvailable } = trpc.assignment.isAIAvailable.useQuery()
|
||||
|
||||
// AI Assignment job queries
|
||||
// Always fetch latest AI job to check for existing results
|
||||
const { data: latestJob, refetch: refetchLatestJob } = trpc.assignment.getLatestAIAssignmentJob.useQuery(
|
||||
{ roundId },
|
||||
{ enabled: useAI }
|
||||
{ roundId }
|
||||
)
|
||||
|
||||
// Poll for job status when there's an active job
|
||||
@@ -107,21 +228,23 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
? Math.round((jobStatus.currentBatch / jobStatus.totalBatches) * 100)
|
||||
: 0
|
||||
|
||||
// Algorithmic suggestions (default)
|
||||
// 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 && !useAI }
|
||||
{ enabled: !!round }
|
||||
)
|
||||
|
||||
// AI-powered suggestions (expensive - only used after job completes)
|
||||
// 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 && useAI && !isAIJobRunning,
|
||||
enabled: !!round && (hasStoredAISuggestions || activeTab === 'ai') && !isAIJobRunning,
|
||||
staleTime: Infinity, // Never consider stale (only refetch manually)
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -129,6 +252,7 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
useEffect(() => {
|
||||
if (latestJob && (latestJob.status === 'RUNNING' || latestJob.status === 'PENDING')) {
|
||||
setActiveJobId(latestJob.id)
|
||||
setActiveTab('ai') // Switch to AI tab if a job is running
|
||||
}
|
||||
}, [latestJob])
|
||||
|
||||
@@ -150,6 +274,7 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
|
||||
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.')
|
||||
@@ -170,10 +295,9 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
reasoning: [s.reasoning],
|
||||
})) ?? []
|
||||
|
||||
// Use the appropriate suggestions based on mode
|
||||
const suggestions = useAI ? aiSuggestions : (algorithmicSuggestions ?? [])
|
||||
const loadingSuggestions = useAI ? (loadingAI || isAIJobRunning) : loadingAlgorithmic
|
||||
const refetchSuggestions = useAI ? refetchAI : refetchAlgorithmic
|
||||
// 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(
|
||||
@@ -264,21 +388,21 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
}
|
||||
|
||||
const handleSelectAllSuggestions = () => {
|
||||
if (suggestions) {
|
||||
if (selectedSuggestions.size === suggestions.length) {
|
||||
if (currentSuggestions) {
|
||||
if (selectedSuggestions.size === currentSuggestions.length) {
|
||||
setSelectedSuggestions(new Set())
|
||||
} else {
|
||||
setSelectedSuggestions(
|
||||
new Set(suggestions.map((s) => `${s.userId}-${s.projectId}`))
|
||||
new Set(currentSuggestions.map((s) => `${s.userId}-${s.projectId}`))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplySelected = async () => {
|
||||
if (!suggestions) return
|
||||
if (!currentSuggestions) return
|
||||
|
||||
const selected = suggestions.filter((s) =>
|
||||
const selected = currentSuggestions.filter((s) =>
|
||||
selectedSuggestions.has(`${s.userId}-${s.projectId}`)
|
||||
)
|
||||
|
||||
@@ -522,212 +646,192 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Smart Suggestions */}
|
||||
{/* Smart Suggestions with Tabs */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-amber-500" />
|
||||
{useAI ? 'AI Assignment Suggestions' : 'Smart Assignment Suggestions'}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{useAI
|
||||
? 'GPT-powered recommendations analyzing project descriptions and judge expertise'
|
||||
: 'Algorithmic recommendations based on tag matching and workload balance'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={useAI ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (!useAI) {
|
||||
setUseAI(true)
|
||||
setSelectedSuggestions(new Set())
|
||||
// Start AI job if no suggestions yet
|
||||
if (!aiSuggestionsRaw?.suggestions?.length && !isAIJobRunning) {
|
||||
handleStartAIJob()
|
||||
}
|
||||
} else {
|
||||
setUseAI(false)
|
||||
setSelectedSuggestions(new Set())
|
||||
}
|
||||
}}
|
||||
disabled={(!isAIAvailable && !useAI) || isAIJobRunning}
|
||||
title={!isAIAvailable ? 'OpenAI API key not configured' : undefined}
|
||||
>
|
||||
<Sparkles className={`mr-2 h-4 w-4 ${useAI ? 'text-amber-300' : ''}`} />
|
||||
{useAI ? 'AI Mode' : 'Use AI'}
|
||||
</Button>
|
||||
{useAI && !isAIJobRunning && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleStartAIJob}
|
||||
disabled={startAIJob.isPending}
|
||||
title="Run AI analysis again"
|
||||
>
|
||||
{startAIJob.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Re-analyze
|
||||
</Button>
|
||||
)}
|
||||
{!useAI && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetchSuggestions()}
|
||||
disabled={loadingSuggestions}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${loadingSuggestions ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-amber-500" />
|
||||
Smart Assignment Suggestions
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Get assignment recommendations using algorithmic matching or AI-powered analysis
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* AI Job Progress Indicator */}
|
||||
{isAIJobRunning && jobStatus && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-blue-600" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100">
|
||||
AI Assignment Analysis in Progress
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
Processing {jobStatus.totalProjects} projects in {jobStatus.totalBatches} batches
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-blue-300 text-blue-700">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
Batch {jobStatus.currentBatch} of {jobStatus.totalBatches}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-blue-700 dark:text-blue-300">
|
||||
{jobStatus.processedCount} of {jobStatus.totalProjects} projects processed
|
||||
</span>
|
||||
<span className="font-medium text-blue-900 dark:text-blue-100">
|
||||
{aiJobProgressPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={aiJobProgressPercent} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAIJobRunning ? (
|
||||
// Don't show suggestions section while AI job is running - progress is shown above
|
||||
null
|
||||
) : loadingSuggestions ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : suggestions && suggestions.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedSuggestions.size === suggestions.length}
|
||||
onCheckedChange={handleSelectAllSuggestions}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{selectedSuggestions.size} of {suggestions.length} selected
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleApplySelected}
|
||||
disabled={selectedSuggestions.size === 0 || applySuggestions.isPending}
|
||||
>
|
||||
{applySuggestions.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Tabs value={activeTab} onValueChange={(v) => {
|
||||
setActiveTab(v as 'algorithm' | 'ai')
|
||||
setSelectedSuggestions(new Set())
|
||||
}}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="algorithm" className="gap-2">
|
||||
<Cpu className="h-4 w-4" />
|
||||
Algorithm
|
||||
{algorithmicSuggestions && algorithmicSuggestions.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{algorithmicSuggestions.length}
|
||||
</Badge>
|
||||
)}
|
||||
Apply Selected ({selectedSuggestions.size})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ai" className="gap-2" disabled={!isAIAvailable && !hasStoredAISuggestions}>
|
||||
<Brain className="h-4 w-4" />
|
||||
AI Powered
|
||||
{aiSuggestions.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{aiSuggestions.length}
|
||||
</Badge>
|
||||
)}
|
||||
{isAIJobRunning && (
|
||||
<Loader2 className="h-3 w-3 animate-spin ml-1" />
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab-specific actions */}
|
||||
{activeTab === 'algorithm' ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetchAlgorithmic()}
|
||||
disabled={loadingAlgorithmic}
|
||||
>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${loadingAlgorithmic ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
{!isAIJobRunning && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleStartAIJob}
|
||||
disabled={startAIJob.isPending || !isAIAvailable}
|
||||
title={!isAIAvailable ? 'OpenAI API key not configured' : hasStoredAISuggestions ? 'Run AI analysis again' : 'Start AI analysis'}
|
||||
>
|
||||
{startAIJob.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{hasStoredAISuggestions ? 'Re-analyze' : 'Start Analysis'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Algorithm Tab Content */}
|
||||
<TabsContent value="algorithm" className="mt-0">
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
Algorithmic recommendations based on tag matching and workload balance
|
||||
</div>
|
||||
{loadingAlgorithmic ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : algorithmicSuggestions && algorithmicSuggestions.length > 0 ? (
|
||||
<SuggestionsTable
|
||||
suggestions={algorithmicSuggestions}
|
||||
selectedSuggestions={selectedSuggestions}
|
||||
onToggle={handleToggleSuggestion}
|
||||
onSelectAll={handleSelectAllSuggestions}
|
||||
onApply={handleApplySelected}
|
||||
isApplying={applySuggestions.isPending}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<CheckCircle2 className="h-12 w-12 text-green-500/50" />
|
||||
<p className="mt-2 font-medium">All projects are covered!</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No additional assignments are needed at this time
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* AI Tab Content */}
|
||||
<TabsContent value="ai" className="mt-0">
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
GPT-powered recommendations analyzing project descriptions and judge expertise
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border max-h-[400px] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12"></TableHead>
|
||||
<TableHead>Juror</TableHead>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>Score</TableHead>
|
||||
<TableHead>Reasoning</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{suggestions.map((suggestion) => {
|
||||
const key = `${suggestion.userId}-${suggestion.projectId}`
|
||||
const isSelected = selectedSuggestions.has(key)
|
||||
{/* AI Job Progress Indicator */}
|
||||
{isAIJobRunning && jobStatus && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-blue-600" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100">
|
||||
AI Assignment Analysis in Progress
|
||||
</p>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
Processing {jobStatus.totalProjects} projects in {jobStatus.totalBatches} batches
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-blue-300 text-blue-700">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
Batch {jobStatus.currentBatch} of {jobStatus.totalBatches}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-blue-700 dark:text-blue-300">
|
||||
{jobStatus.processedCount} of {jobStatus.totalProjects} projects processed
|
||||
</span>
|
||||
<span className="font-medium text-blue-900 dark:text-blue-100">
|
||||
{aiJobProgressPercent}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={aiJobProgressPercent} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={key}
|
||||
className={isSelected ? 'bg-muted/50' : ''}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => handleToggleSuggestion(key)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{suggestion.jurorName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{suggestion.projectTitle}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
suggestion.score >= 60
|
||||
? 'default'
|
||||
: suggestion.score >= 40
|
||||
? 'secondary'
|
||||
: 'outline'
|
||||
}
|
||||
>
|
||||
{suggestion.score.toFixed(0)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
<ul className="text-xs text-muted-foreground">
|
||||
{suggestion.reasoning.map((r, i) => (
|
||||
<li key={i}>{r}</li>
|
||||
))}
|
||||
</ul>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<CheckCircle2 className="h-12 w-12 text-green-500/50" />
|
||||
<p className="mt-2 font-medium">All projects are covered!</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No additional assignments are needed at this time
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isAIJobRunning ? null : loadingAI ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : aiSuggestions.length > 0 ? (
|
||||
<SuggestionsTable
|
||||
suggestions={aiSuggestions}
|
||||
selectedSuggestions={selectedSuggestions}
|
||||
onToggle={handleToggleSuggestion}
|
||||
onSelectAll={handleSelectAllSuggestions}
|
||||
onApply={handleApplySelected}
|
||||
isApplying={applySuggestions.isPending}
|
||||
/>
|
||||
) : !hasStoredAISuggestions ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Brain className="h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="mt-2 font-medium">No AI analysis yet</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Click "Start Analysis" to generate AI-powered suggestions
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleStartAIJob}
|
||||
disabled={startAIJob.isPending || !isAIAvailable}
|
||||
>
|
||||
{startAIJob.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Brain className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Start AI Analysis
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<CheckCircle2 className="h-12 w-12 text-green-500/50" />
|
||||
<p className="mt-2 font-medium">All projects are covered!</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No additional assignments are needed at this time
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user