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)
|
currentBatch Int @default(0)
|
||||||
processedCount Int @default(0)
|
processedCount Int @default(0)
|
||||||
suggestionsCount Int @default(0)
|
suggestionsCount Int @default(0)
|
||||||
|
suggestionsJson Json? @db.JsonB // Stores the AI-generated suggestions
|
||||||
errorMessage String? @db.Text
|
errorMessage String? @db.Text
|
||||||
startedAt DateTime?
|
startedAt DateTime?
|
||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,18 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
|
||||||
onProgress
|
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({
|
await prisma.assignmentJob.update({
|
||||||
where: { id: jobId },
|
where: { id: jobId },
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -120,6 +131,7 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
|
||||||
completedAt: new Date(),
|
completedAt: new Date(),
|
||||||
processedCount: projects.length,
|
processedCount: projects.length,
|
||||||
suggestionsCount: result.suggestions.length,
|
suggestionsCount: result.suggestions.length,
|
||||||
|
suggestionsJson: enrichedSuggestions,
|
||||||
fallbackUsed: result.fallbackUsed ?? false,
|
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
|
getAISuggestions: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|
@ -740,88 +752,61 @@ export const assignmentRouter = router({
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
// Get round constraints
|
// Find the latest completed job for this round
|
||||||
const round = await ctx.prisma.round.findUniqueOrThrow({
|
const completedJob = await ctx.prisma.assignmentJob.findFirst({
|
||||||
where: { id: input.roundId },
|
where: {
|
||||||
|
roundId: input.roundId,
|
||||||
|
status: 'COMPLETED',
|
||||||
|
},
|
||||||
|
orderBy: { completedAt: 'desc' },
|
||||||
select: {
|
select: {
|
||||||
requiredReviews: true,
|
suggestionsJson: true,
|
||||||
minAssignmentsPerJuror: true,
|
fallbackUsed: true,
|
||||||
maxAssignmentsPerJuror: true,
|
completedAt: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get all active jury members with their expertise and current load
|
// If we have stored suggestions, return them
|
||||||
const jurors = await ctx.prisma.user.findMany({
|
if (completedJob?.suggestionsJson) {
|
||||||
where: { role: 'JURY_MEMBER', status: 'ACTIVE' },
|
const suggestions = completedJob.suggestionsJson as Array<{
|
||||||
select: {
|
jurorId: string
|
||||||
id: true,
|
jurorName: string
|
||||||
name: true,
|
projectId: string
|
||||||
email: true,
|
projectTitle: string
|
||||||
expertiseTags: true,
|
confidenceScore: number
|
||||||
maxAssignments: true,
|
expertiseMatchScore: number
|
||||||
_count: {
|
reasoning: string
|
||||||
select: {
|
}>
|
||||||
assignments: { where: { roundId: input.roundId } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get all projects in the round
|
// Filter out suggestions for assignments that already exist
|
||||||
const projects = await ctx.prisma.project.findMany({
|
const existingAssignments = await ctx.prisma.assignment.findMany({
|
||||||
where: { roundId: input.roundId },
|
where: { roundId: input.roundId },
|
||||||
select: {
|
select: { userId: true, projectId: true },
|
||||||
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',
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
)
|
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 {
|
return {
|
||||||
success: result.success,
|
success: true,
|
||||||
suggestions: enrichedSuggestions,
|
suggestions: [],
|
||||||
fallbackUsed: result.fallbackUsed,
|
fallbackUsed: false,
|
||||||
error: result.error,
|
error: null,
|
||||||
|
generatedAt: null,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue