Fix AI suggestions not displaying after job completion
Build and Push Docker Image / build (push) Successful in 9m26s Details

BREAKING CHANGE: AI assignment job now stores suggestions in database

- Add suggestionsJson column to AssignmentJob table
- Store enriched suggestions when job completes
- Update getAISuggestions to retrieve stored suggestions instead of regenerating
- Filter out already-assigned pairs from stored suggestions

Previously, the background job generated suggestions but discarded them,
and getAISuggestions tried to regenerate from scratch (causing infinite loading).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-05 14:38:43 +01:00
parent e3e3fa9da4
commit 3abfccb22a
3 changed files with 73 additions and 76 deletions

View File

@ -0,0 +1,11 @@
-- Add suggestionsJson column to AssignmentJob to store AI-generated suggestions
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'AssignmentJob' AND column_name = 'suggestionsJson'
) THEN
ALTER TABLE "AssignmentJob" ADD COLUMN "suggestionsJson" JSONB;
END IF;
END $$;

View File

@ -1105,6 +1105,7 @@ model AssignmentJob {
currentBatch Int @default(0)
processedCount Int @default(0)
suggestionsCount Int @default(0)
suggestionsJson Json? @db.JsonB // Stores the AI-generated suggestions
errorMessage String? @db.Text
startedAt DateTime?
completedAt DateTime?

View File

@ -112,7 +112,18 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
onProgress
)
// Mark job as completed
// Enrich suggestions with names for storage
const enrichedSuggestions = result.suggestions.map((s) => {
const juror = jurors.find((j) => j.id === s.jurorId)
const project = projects.find((p) => p.id === s.projectId)
return {
...s,
jurorName: juror?.name || juror?.email || 'Unknown',
projectTitle: project?.title || 'Unknown',
}
})
// Mark job as completed and store suggestions
await prisma.assignmentJob.update({
where: { id: jobId },
data: {
@ -120,6 +131,7 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
completedAt: new Date(),
processedCount: projects.length,
suggestionsCount: result.suggestions.length,
suggestionsJson: enrichedSuggestions,
fallbackUsed: result.fallbackUsed ?? false,
},
})
@ -730,7 +742,7 @@ export const assignmentRouter = router({
}),
/**
* Get AI-powered assignment suggestions
* Get AI-powered assignment suggestions (retrieves from completed job)
*/
getAISuggestions: adminProcedure
.input(
@ -740,88 +752,61 @@ export const assignmentRouter = router({
})
)
.query(async ({ ctx, input }) => {
// Get round constraints
const round = await ctx.prisma.round.findUniqueOrThrow({
where: { id: input.roundId },
// Find the latest completed job for this round
const completedJob = await ctx.prisma.assignmentJob.findFirst({
where: {
roundId: input.roundId,
status: 'COMPLETED',
},
orderBy: { completedAt: 'desc' },
select: {
requiredReviews: true,
minAssignmentsPerJuror: true,
maxAssignmentsPerJuror: true,
suggestionsJson: true,
fallbackUsed: true,
completedAt: true,
},
})
// Get all active jury members with their expertise and current load
const jurors = await ctx.prisma.user.findMany({
where: { role: 'JURY_MEMBER', status: 'ACTIVE' },
select: {
id: true,
name: true,
email: true,
expertiseTags: true,
maxAssignments: true,
_count: {
select: {
assignments: { where: { roundId: input.roundId } },
},
},
},
})
// If we have stored suggestions, return them
if (completedJob?.suggestionsJson) {
const suggestions = completedJob.suggestionsJson as Array<{
jurorId: string
jurorName: string
projectId: string
projectTitle: string
confidenceScore: number
expertiseMatchScore: number
reasoning: string
}>
// Get all projects in the round
const projects = await ctx.prisma.project.findMany({
where: { roundId: input.roundId },
select: {
id: true,
title: true,
description: true,
tags: true,
teamName: true,
_count: { select: { assignments: true } },
},
})
// Get existing assignments
const existingAssignments = await ctx.prisma.assignment.findMany({
where: { roundId: input.roundId },
select: { userId: true, projectId: true },
})
const constraints = {
requiredReviewsPerProject: round.requiredReviews,
minAssignmentsPerJuror: round.minAssignmentsPerJuror,
maxAssignmentsPerJuror: round.maxAssignmentsPerJuror,
existingAssignments: existingAssignments.map((a) => ({
jurorId: a.userId,
projectId: a.projectId,
})),
}
// Use AI or fallback based on input and availability
let result
if (input.useAI) {
result = await generateAIAssignments(jurors, projects, constraints)
} else {
result = generateFallbackAssignments(jurors, projects, constraints)
}
// Enrich suggestions with user and project names for display
const enrichedSuggestions = await Promise.all(
result.suggestions.map(async (s) => {
const juror = jurors.find((j) => j.id === s.jurorId)
const project = projects.find((p) => p.id === s.projectId)
return {
...s,
jurorName: juror?.name || juror?.email || 'Unknown',
projectTitle: project?.title || 'Unknown',
}
// Filter out suggestions for assignments that already exist
const existingAssignments = await ctx.prisma.assignment.findMany({
where: { roundId: input.roundId },
select: { userId: true, projectId: true },
})
)
const assignmentSet = new Set(
existingAssignments.map((a) => `${a.userId}-${a.projectId}`)
)
const filteredSuggestions = suggestions.filter(
(s) => !assignmentSet.has(`${s.jurorId}-${s.projectId}`)
)
return {
success: true,
suggestions: filteredSuggestions,
fallbackUsed: completedJob.fallbackUsed,
error: null,
generatedAt: completedJob.completedAt,
}
}
// No completed job with suggestions - return empty
return {
success: result.success,
suggestions: enrichedSuggestions,
fallbackUsed: result.fallbackUsed,
error: result.error,
success: true,
suggestions: [],
fallbackUsed: false,
error: null,
generatedAt: null,
}
}),