All files / web/src/db/schema worksheet-attempts.ts

100% Statements 177/177
100% Branches 3/3
100% Functions 1/1
100% Lines 177/177

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 165 166 167 168 169 170 171 172 173 174 175 176 177 1782x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 40x 40x 40x 40x 40x 40x 40x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 2x 2x 2x 2x 2x 2x  
import { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { users } from './users'
import { worksheetMastery } from './worksheet-mastery'
 
/**
 * Worksheet attempts table - tracks uploaded completed worksheets
 *
 * Teachers upload photos of students' completed worksheets.
 * AI grades them and provides feedback on mastery progress.
 */
export const worksheetAttempts = sqliteTable(
  'worksheet_attempts',
  {
    /** Unique identifier (UUID) */
    id: text('id').primaryKey(),
 
    /** User ID of the student (can be guest or authenticated user) */
    userId: text('user_id').notNull(),
 
    /** URL to uploaded image in storage */
    uploadedImageUrl: text('uploaded_image_url').notNull(),
 
    /** Optional: Link to worksheet if generated from our system */
    worksheetId: text('worksheet_id'),
 
    /** Optional: Upload session ID for grouping batch uploads (QR code workflow) */
    sessionId: text('session_id'),
 
    // Worksheet metadata (from OCR or user input)
    /** Operator type */
    operator: text('operator', {
      enum: ['addition', 'subtraction', 'mixed'],
    }).notNull(),
 
    /** Number of digits in problems (1-5) */
    digitCount: integer('digit_count').notNull(),
 
    /** Total number of problems on worksheet */
    problemCount: integer('problem_count').notNull(),
 
    // Grading status
    /** Current grading status */
    gradingStatus: text('grading_status', {
      enum: ['pending', 'processing', 'completed', 'failed'],
    })
      .notNull()
      .default('pending'),
 
    /** When grading was completed */
    gradedAt: integer('graded_at', { mode: 'timestamp' }),
 
    /** Error message if grading failed */
    errorMessage: text('error_message'),
 
    /** Total number of problems detected/graded */
    totalProblems: integer('total_problems'),
 
    /** Number of correct answers */
    correctCount: integer('correct_count'),
 
    /** Overall accuracy (0.0-1.0) */
    accuracy: real('accuracy'),
 
    // AI analysis results
    /** JSON array of error patterns detected (e.g., ["carry-tens", "borrow-hundreds"]) */
    errorPatterns: text('error_patterns'),
 
    /** Recommended progression step ID */
    suggestedStepId: text('suggested_step_id'),
 
    /** Natural language feedback from AI */
    aiFeedback: text('ai_feedback'),
 
    /** Full AI response (for debugging) */
    aiResponseRaw: text('ai_response_raw'),
 
    /** Timestamp of creation */
    createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
 
    /** Timestamp of last update */
    updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
  },
  (table) => ({
    // Index for finding user's attempts
    userIdx: index('worksheet_attempts_user_idx').on(table.userId),
    // Index for finding pending/processing attempts
    statusIdx: index('worksheet_attempts_status_idx').on(table.gradingStatus),
    // Index for finding attempts by session (for batch QR uploads)
    sessionIdx: index('worksheet_attempts_session_idx').on(table.sessionId),
  })
)
 
/**
 * Problem attempts table - tracks individual problem results
 *
 * Each row represents one problem from a worksheet attempt.
 * Allows granular analysis of which problem types cause difficulty.
 */
export const problemAttempts = sqliteTable(
  'problem_attempts',
  {
    /** Unique identifier (UUID) */
    id: text('id').primaryKey(),
 
    /** Parent worksheet attempt */
    attemptId: text('attempt_id')
      .notNull()
      .references(() => worksheetAttempts.id, { onDelete: 'cascade' }),
 
    /** User ID (denormalized for queries, can be guest or authenticated user) */
    userId: text('user_id').notNull(),
 
    // Problem details
    /** Position on worksheet (0-based) */
    problemIndex: integer('problem_index').notNull(),
 
    /** First operand */
    operandA: integer('operand_a').notNull(),
 
    /** Second operand */
    operandB: integer('operand_b').notNull(),
 
    /** Operator */
    operator: text('operator', { enum: ['addition', 'subtraction'] }).notNull(),
 
    /** Correct answer */
    correctAnswer: integer('correct_answer').notNull(),
 
    // Student's work (from OCR)
    /** Student's written answer */
    studentAnswer: integer('student_answer'),
 
    /** JSON array of carry/borrow digits detected */
    studentWorkDigits: text('student_work_digits'),
 
    // Grading
    /** Whether answer is correct */
    isCorrect: integer('is_correct', { mode: 'boolean' }).notNull(),
 
    /** Type of error if incorrect */
    errorType: text('error_type', {
      enum: ['computation', 'carry', 'borrow', 'alignment', 'ocr-uncertain'],
    }),
 
    // Problem characteristics (for analysis)
    /** Number of digits in operands */
    digitCount: integer('digit_count').notNull(),
 
    /** Whether this problem requires regrouping */
    requiresRegrouping: integer('requires_regrouping', {
      mode: 'boolean',
    }).notNull(),
 
    /** JSON array of place values that regroup (e.g., ["ones", "tens"]) */
    regroupsInPlaces: text('regroups_in_places'),
 
    /** Timestamp of creation */
    createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
  },
  (table) => ({
    // Index for finding problems by attempt
    attemptIdx: index('problem_attempts_attempt_idx').on(table.attemptId),
    // Index for finding user's problem history
    userIdx: index('problem_attempts_user_idx').on(table.userId),
    // Composite index for analyzing specific problem types
    problemTypeIdx: index('problem_attempts_type_idx').on(
      table.userId,
      table.digitCount,
      table.requiresRegrouping
    ),
  })
)
 
export type WorksheetAttempt = typeof worksheetAttempts.$inferSelect
export type NewWorksheetAttempt = typeof worksheetAttempts.$inferInsert
export type ProblemAttempt = typeof problemAttempts.$inferSelect
export type NewProblemAttempt = typeof problemAttempts.$inferInsert