Fix AI suggestions not displaying after job completion
Build and Push Docker Image / build (push) Successful in 9m26s
Details
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:
parent
e3e3fa9da4
commit
3abfccb22a
|
|
@ -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 $$;
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// 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 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',
|
||||
}
|
||||
})
|
||||
const filteredSuggestions = suggestions.filter(
|
||||
(s) => !assignmentSet.has(`${s.jurorId}-${s.projectId}`)
|
||||
)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
suggestions: enrichedSuggestions,
|
||||
fallbackUsed: result.fallbackUsed,
|
||||
error: result.error,
|
||||
success: true,
|
||||
suggestions: filteredSuggestions,
|
||||
fallbackUsed: completedJob.fallbackUsed,
|
||||
error: null,
|
||||
generatedAt: completedJob.completedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// No completed job with suggestions - return empty
|
||||
return {
|
||||
success: true,
|
||||
suggestions: [],
|
||||
fallbackUsed: false,
|
||||
error: null,
|
||||
generatedAt: null,
|
||||
}
|
||||
}),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue