duration_seconds); $position = max(0, min($positionSeconds, $lesson->duration_seconds)); $watched = round(($position / $duration) * 100, 2); /** @var LessonProgress $progress */ $progress = LessonProgress::updateOrCreate( ['user_id' => $student->id, 'lesson_id' => $lesson->id], [ 'last_position_seconds' => $position, 'watched_percentage' => (int) min(100, $watched), 'completed_at' => $watched >= self::COMPLETION_THRESHOLD ? now() : null, ] ); return $progress; } /** * @param array $submittedAnswers * @return array */ public function recordQuizAttempt(User $student, Quiz $quiz, array $submittedAnswers): array { /** @var Collection $questions */ $questions = Question::with('options') ->where('quiz_id', $quiz->id) ->whereIn('id', collect($submittedAnswers)->pluck('question_id')) ->get() ->keyBy('id'); /** @var QuizAttempt $attempt */ $attempt = $quiz->attempts()->create([ 'user_id' => $student->id, 'score' => 0, ]); $correctCount = 0; $results = []; foreach ($submittedAnswers as $entry) { /** @var Question|null $question */ $question = $questions->get($entry['question_id']); if ($question === null) { continue; } $selectedOptionId = $entry['selected_option_id'] ?? null; /** @var QuestionOption|null $option */ $option = $selectedOptionId !== null ? $question->options->firstWhere('id', $selectedOptionId) : null; $isCorrect = $option !== null && $option->is_correct; if ($isCorrect) { $correctCount++; } $attempt->answers()->create([ 'question_id' => $question->id, 'selected_option_id' => $option?->id, 'is_correct' => $isCorrect, 'time_spent_seconds' => $entry['time_spent_seconds'] ?? 0, ]); if ($question->concept_id !== null) { $this->updateMastery($student->id, (int) $question->concept_id, $isCorrect); } $results[] = [ 'question_id' => $question->id, 'is_correct' => $isCorrect, 'explanation' => $question->explanation, ]; } $total = count($results); $score = $total > 0 ? (int) round(($correctCount / $total) * 100) : 0; $attempt->update(['score' => $score]); return [ 'attempt_id' => $attempt->id, 'score' => $score, 'total_questions' => $total, 'correct_answers' => $correctCount, 'results' => $results, ]; } private function updateMastery(int $userId, int $conceptId, bool $isCorrect): ConceptMastery { /** @var ConceptMastery $mastery */ $mastery = ConceptMastery::firstOrNew(['user_id' => $userId, 'concept_id' => $conceptId]); if (! $mastery->exists) { $mastery->correct_count = 0; $mastery->wrong_count = 0; $mastery->mastery_score = 0; } if ($mastery->correct_count + $mastery->wrong_count === 0) { $mastery->mastery_score = $isCorrect ? 100 : 0; } else { $previous = (float) $mastery->mastery_score / 100; $outcome = $isCorrect ? 1.0 : 0.0; $mastery->mastery_score = round((($previous * self::MASTERY_DECAY) + ($outcome * (1 - self::MASTERY_DECAY))) * 100, 2); } $isCorrect ? $mastery->correct_count++ : $mastery->wrong_count++; $mastery->save(); return $mastery; } /** * Collect the student's aggregated performance snapshot for AI analysis. * * @return array */ public function buildPerformanceSnapshot(User $student): array { $masteryRows = $student->conceptMastery() ->with('concept:id,name,remedial_lesson_id') ->orderBy('mastery_score') ->limit(15) ->get(); $recentWrongAnswers = Answer::query() ->where('is_correct', false) ->whereHas('attempt', fn ($query) => $query->where('user_id', $student->id)) ->with('question:id,body,concept_id') ->latest() ->limit(10) ->get(); $progressStats = $student->lessonProgress() ->selectRaw('COUNT(*) as lessons_started, COALESCE(AVG(watched_percentage), 0) as avg_watched') ->first(); return [ 'student_grade' => $student->grade, 'track' => $student->track, 'lessons_started' => (int) ($progressStats?->lessons_started ?? 0), 'average_watch_percentage' => round((float) ($progressStats?->avg_watched ?? 0), 1), 'concept_mastery' => $masteryRows->map(fn (ConceptMastery $row): array => [ 'concept_id' => $row->concept_id, 'name' => $row->concept?->name, 'mastery_score' => (float) $row->mastery_score, 'correct' => $row->correct_count, 'wrong' => $row->wrong_count, ])->all(), 'recent_wrong_answers' => $recentWrongAnswers->map(fn (Answer $answer): array => [ 'question_body' => $answer->question?->body, ])->all(), ]; } }