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 | import { randomUUID } from 'crypto' import { writeFile } from 'fs/promises' import { type NextRequest, NextResponse } from 'next/server' import { join } from 'path' import { db } from '@/db' import { worksheetAttempts } from '@/db/schema' import { processWorksheetAttempt } from '@/lib/grading/processAttempt' import { getUserId } from '@/lib/viewer' import { withAuth } from '@/lib/auth/withAuth' /** * Trigger processing in background without blocking response * In production, this would be a proper job queue */ function processAttemptInBackground(attemptId: string, userId?: string) { // Fire and forget - don't await processWorksheetAttempt(attemptId, userId).catch((error) => { console.error(`Background processing failed for ${attemptId}:`, error) }) } /** * Upload API endpoint for worksheet images * * Teachers upload photos of completed paper worksheets. * This endpoint stores the image and creates a database record * for background AI grading. * * Optional: sessionId can be provided to group multiple uploads together * (used for QR code batch upload workflow) */ export const POST = withAuth(async (request) => { try { // Get viewer ID from session (works for both authenticated users and guests) const userId = await getUserId() const formData = await request.formData() const file = formData.get('image') as File | null if (!file) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }) } // Validate file type if (!file.type.startsWith('image/')) { return NextResponse.json({ error: 'File must be an image' }, { status: 400 }) } // Generate unique ID for this attempt const attemptId = randomUUID() const extension = file.name.split('.').pop() || 'jpg' const filename = `${attemptId}.${extension}` // Save to local storage (MVP) - will move to Cloudflare R2 later const uploadDir = join(process.cwd(), 'data', 'uploads') const filepath = join(uploadDir, filename) const bytes = await file.arrayBuffer() await writeFile(filepath, Buffer.from(bytes)) // Get optional metadata from form data const worksheetId = formData.get('worksheetId') as string | null const sessionId = formData.get('sessionId') as string | null // For batch QR uploads const operatorInput = formData.get('operator') as string | null const digitCountInput = formData.get('digitCount') as string | null const problemCountInput = formData.get('problemCount') as string | null // Parse metadata with defaults const operator = operatorInput || 'addition' const digitCount = digitCountInput ? parseInt(digitCountInput, 10) : 2 const problemCount = problemCountInput ? parseInt(problemCountInput, 10) : 20 // Create database record const now = new Date() await db.insert(worksheetAttempts).values({ id: attemptId, userId, uploadedImageUrl: `/uploads/${filename}`, worksheetId, sessionId, // Optional: groups uploads from same QR code session operator: operator as 'addition' | 'subtraction' | 'mixed', digitCount, problemCount, gradingStatus: 'pending', createdAt: now, updatedAt: now, }) // For MVP: Process immediately (not truly async) // In production, this should be queued to a background job // For now, we'll trigger processing but not wait for it processAttemptInBackground(attemptId, userId) return NextResponse.json({ attemptId, sessionId, // Echo back for client tracking status: 'pending', message: 'Upload successful. Grading will begin shortly.', }) } catch (error) { console.error('Upload error:', error) return NextResponse.json( { error: 'Upload failed', details: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 } ) } }) |