diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx
index f17e641..577890c 100644
--- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx
+++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx
@@ -186,7 +186,8 @@ export default function RoundDetailPage() {
})
},
onError: (err) => {
- toast.error(`AI generation failed: ${err.message}`)
+ toast.error(`AI generation failed: ${err.message}`, { duration: 15000 })
+ console.error('[AI Assignment]', err)
},
})
const [exportOpen, setExportOpen] = useState(false)
@@ -1725,6 +1726,19 @@ export default function RoundDetailPage() {
)}
+ {aiAssignmentMutation.error && !aiAssignmentMutation.isPending && (
+
+
+
+
+ AI generation failed
+
+
+ {aiAssignmentMutation.error.message}
+
+
+
+ )}
{aiAssignmentMutation.data && !aiAssignmentMutation.isPending && (
diff --git a/src/server/routers/roundAssignment.ts b/src/server/routers/roundAssignment.ts
index ae4aa6c..e66c009 100644
--- a/src/server/routers/roundAssignment.ts
+++ b/src/server/routers/roundAssignment.ts
@@ -152,6 +152,7 @@ export const roundAssignmentRouter = router({
}
// Call AI service
+ console.log(`[AI Assignment Router] Starting for ${projects.length} projects, ${jurors.length} jurors, ${input.requiredReviews} reviews/project, max ${maxPerJuror}/juror`)
const result = await generateAIAssignments(
jurors,
projects,
@@ -159,6 +160,7 @@ export const roundAssignmentRouter = router({
ctx.user.id,
input.roundId,
)
+ console.log(`[AI Assignment Router] Got ${result.suggestions.length} suggestions, success=${result.success}, fallback=${result.fallbackUsed}`)
// Filter out COI pairs and already-assigned pairs
const existingPairSet = new Set(existingAssignments.map((a) => `${a.userId}:${a.projectId}`))
diff --git a/src/server/services/ai-assignment.ts b/src/server/services/ai-assignment.ts
index a027b51..b49cca5 100644
--- a/src/server/services/ai-assignment.ts
+++ b/src/server/services/ai-assignment.ts
@@ -173,9 +173,9 @@ async function processAssignmentBatch(
try {
// Calculate maxTokens based on expected assignments
- // ~150 tokens per assignment JSON object
+ // ~150 tokens per assignment JSON object, capped at 12000
const expectedAssignments = batchProjects.length * constraints.requiredReviewsPerProject
- const estimatedTokens = Math.max(4000, expectedAssignments * 200 + 500)
+ const estimatedTokens = Math.min(12000, Math.max(4000, expectedAssignments * 200 + 500))
const params = buildCompletionParams(model, {
messages: [
@@ -359,13 +359,33 @@ function buildBatchPrompt(
const expectedTotal = projects.length * constraints.requiredReviewsPerProject
+ // Instead of full existing assignment list, send per-juror current load counts
+ // This keeps the prompt shorter as batches accumulate
+ const jurorCurrentLoad: Record = {}
+ for (const a of constraints.existingAssignments) {
+ const anonId = jurorIdMap.get(a.jurorId)
+ if (anonId) jurorCurrentLoad[anonId] = (jurorCurrentLoad[anonId] || 0) + 1
+ }
+
+ // Also track which projects in this batch already have assignments
+ const projectExistingReviewers: Record = {}
+ for (const a of constraints.existingAssignments) {
+ const anonProjectId = projectIdMap.get(a.projectId)
+ const anonJurorId = jurorIdMap.get(a.jurorId)
+ if (anonProjectId && anonJurorId) {
+ if (!projectExistingReviewers[anonProjectId]) projectExistingReviewers[anonProjectId] = []
+ projectExistingReviewers[anonProjectId].push(anonJurorId)
+ }
+ }
+
return `JURORS: ${JSON.stringify(jurors)}
PROJECTS: ${JSON.stringify(projects)}
REVIEWS_PER_PROJECT: ${constraints.requiredReviewsPerProject} (each project MUST get exactly ${constraints.requiredReviewsPerProject} different jurors)
MAX_PER_JUROR: ${constraints.maxAssignmentsPerJuror || 'unlimited'} (HARD LIMIT — never exceed)${jurorLimitsStr}${targetStr}
-EXISTING: ${JSON.stringify(anonymousExisting)}
-EXPECTED_OUTPUT_SIZE: approximately ${expectedTotal} assignment objects (${projects.length} projects × ${constraints.requiredReviewsPerProject} reviewers each)
-IMPORTANT: Every project must appear ${constraints.requiredReviewsPerProject} times with ${constraints.requiredReviewsPerProject} DIFFERENT juror_ids. Pick the least-loaded jurors first. Never exceed a juror's max.
+CURRENT_JUROR_LOAD: ${JSON.stringify(jurorCurrentLoad)} (add these to currentAssignmentCount to get true total)
+ALREADY_ASSIGNED: ${JSON.stringify(projectExistingReviewers)} (do NOT assign these juror-project pairs again)
+EXPECTED_OUTPUT: ${expectedTotal} assignment objects (${projects.length} projects × ${constraints.requiredReviewsPerProject} reviewers)
+IMPORTANT: Every project must appear ${constraints.requiredReviewsPerProject} times with ${constraints.requiredReviewsPerProject} DIFFERENT juror_ids. Pick the least-loaded jurors first.
Return JSON: {"assignments": [...]}`
}