Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 7x 7x 11x 1x 1x 11x 1x 1x 2x 2x 11x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 1x 1x 13x 13x 13x 13x 13x 13x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 14x 18x 18x 18x 8x 8x 8x 8x 8x 8x 16x 16x 16x 8x 8x 16x 8x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 18x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 14x 14x 14x 14x 14x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 14x | import { randomUUID } from 'crypto'
import { eq } from 'drizzle-orm'
import { join } from 'path'
import { db } from '@/db'
import { problemAttempts, worksheetAttempts } from '@/db/schema'
import { gradeWorksheetWithVision } from '@/lib/ai/gradeWorksheet'
import { updateMasteryFromGrading } from './updateMasteryProfile'
/**
* Map AI error patterns to database error types
*/
function inferErrorType(
errorPatterns: string[]
): 'computation' | 'carry' | 'borrow' | 'alignment' | 'ocr-uncertain' {
// Check for carry/borrow errors first (most specific)
if (errorPatterns.some((p) => p.toLowerCase().includes('carry'))) {
return 'carry'
}
if (errorPatterns.some((p) => p.toLowerCase().includes('borrow'))) {
return 'borrow'
}
if (errorPatterns.some((p) => p.toLowerCase().includes('align'))) {
return 'alignment'
}
if (
errorPatterns.some((p) => p.toLowerCase().includes('ocr') || p.toLowerCase().includes('read'))
) {
return 'ocr-uncertain'
}
// Default to computation error
return 'computation'
}
/**
* Process a worksheet attempt through the full grading pipeline
*
* This is the main orchestration function that:
* 1. Updates status to 'processing'
* 2. Calls GPT-5 Vision for single-pass grading (OCR + analysis)
* 3. Stores individual problem results
* 4. Updates attempt with AI analysis
* 5. TODO: Updates mastery profile
*
* This will be called by a background job queue in production.
* For MVP, it's called fire-and-forget after upload.
*/
export async function processWorksheetAttempt(attemptId: string, userId?: string) {
try {
// Update status to processing
await db
.update(worksheetAttempts)
.set({ gradingStatus: 'processing', updatedAt: new Date() })
.where(eq(worksheetAttempts.id, attemptId))
// Get attempt record
const [attempt] = await db
.select()
.from(worksheetAttempts)
.where(eq(worksheetAttempts.id, attemptId))
if (!attempt) {
throw new Error(`Attempt ${attemptId} not found`)
}
// 1. Build image path
const imagePath = join(process.cwd(), 'data', attempt.uploadedImageUrl)
// 2. Grade with GPT-5 Vision (single-pass: OCR + grading + analysis)
const gradingResult = await gradeWorksheetWithVision(imagePath, 2, userId)
// Extract data for database storage
const gradedProblems = gradingResult.problems
const aiAnalysis = {
errorPatterns: gradingResult.errorPatterns,
currentStepEstimate: gradingResult.currentStepEstimate,
suggestedStepId: gradingResult.suggestedStepId,
reasoning: gradingResult.reasoning,
feedback: gradingResult.feedback,
}
// 3. Store individual problem results
const now = new Date()
for (const problem of gradedProblems) {
// Determine regroup places
const regroupPlaces: string[] = []
if (problem.requiresRegrouping) {
// Check each place value for regrouping
const aStr = problem.operandA.toString().padStart(problem.digitCount, '0')
const bStr = problem.operandB.toString().padStart(problem.digitCount, '0')
const places = ['ones', 'tens', 'hundreds', 'thousands', 'ten-thousands']
for (let i = problem.digitCount - 1; i >= 0; i--) {
const digitA = Number.parseInt(aStr[aStr.length - 1 - i], 10)
const digitB = Number.parseInt(bStr[bStr.length - 1 - i], 10)
if (digitA + digitB >= 10) {
regroupPlaces.push(places[problem.digitCount - 1 - i])
}
}
}
await db.insert(problemAttempts).values({
id: randomUUID(),
attemptId,
userId: attempt.userId,
problemIndex: problem.index,
operandA: problem.operandA,
operandB: problem.operandB,
operator: 'addition',
correctAnswer: problem.correctAnswer,
studentAnswer: problem.studentAnswer,
isCorrect: problem.isCorrect,
errorType: problem.isCorrect ? null : inferErrorType(aiAnalysis.errorPatterns),
digitCount: problem.digitCount,
requiresRegrouping: problem.requiresRegrouping,
regroupsInPlaces: regroupPlaces.length > 0 ? JSON.stringify(regroupPlaces) : null,
createdAt: now,
})
}
// 4. Update attempt with results
const correctCount = gradedProblems.filter((p) => p.isCorrect).length
await db
.update(worksheetAttempts)
.set({
gradingStatus: 'completed',
gradedAt: now,
totalProblems: gradedProblems.length,
correctCount,
accuracy: correctCount / gradedProblems.length,
errorPatterns: JSON.stringify(aiAnalysis.errorPatterns),
suggestedStepId: aiAnalysis.suggestedStepId,
aiFeedback: aiAnalysis.feedback,
aiResponseRaw: JSON.stringify(aiAnalysis),
updatedAt: now,
})
.where(eq(worksheetAttempts.id, attemptId))
// 5. Update mastery profile
const masteryResult = await updateMasteryFromGrading(attempt.userId, gradingResult)
console.log(
`Mastery update for ${masteryResult.stepId}: ${masteryResult.mastered ? 'MASTERED' : 'in progress'}`
)
return { success: true, attemptId, mastered: masteryResult.mastered }
} catch (error) {
console.error('Grading failed:', error)
// Extract error message
const errorMessage = error instanceof Error ? error.message : 'Unknown error during grading'
// Mark as failed with error message
await db
.update(worksheetAttempts)
.set({
gradingStatus: 'failed',
errorMessage,
updatedAt: new Date(),
})
.where(eq(worksheetAttempts.id, attemptId))
throw error
}
}
|