import { useEffect, useCallback, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; import CloseIcon from '@mui/icons-material/Close'; import { useLesson } from '../hooks/useLesson'; import { useSpeech } from '../hooks/useSpeech'; import { getLesson } from '../content/loader'; import { apiClient } from '../api/client'; import { ProgressBar } from '../components/layout/ProgressBar'; import { HeartsDisplay } from '../components/common/HeartsDisplay'; import { MultipleChoice } from '../components/exercises/MultipleChoice'; import { Translation, checkTranslation } from '../components/exercises/Translation'; import { MatchingPairs } from '../components/exercises/MatchingPairs'; import { FillInTheBlank } from '../components/exercises/FillInTheBlank'; import { SpeakExercise } from '../components/exercises/SpeakExercise'; import { CorrectBanner } from '../components/feedback/CorrectBanner'; import { IncorrectBanner } from '../components/feedback/IncorrectBanner'; import { SpeechBubble } from '../components/mascot/SpeechBubble'; import type { Exercise } from '../types/lesson'; function getCorrectAnswer(exercise: Exercise): string { switch (exercise.type) { case 'multiple-choice': return exercise.choices[exercise.correctIndex]; case 'translation': return exercise.acceptedAnswers[0]; case 'matching-pairs': return exercise.pairs.map((p) => `${p.left} = ${p.right}`).join(', '); case 'fill-in-the-blank': return exercise.blank; case 'speak': return exercise.acceptedAnswers[0]; } } function isAnswerCorrect(exercise: Exercise, answer: unknown): boolean { switch (exercise.type) { case 'multiple-choice': return answer === exercise.correctIndex; case 'translation': return checkTranslation( (answer as string) || '', exercise.acceptedAnswers, ); case 'matching-pairs': { const pairs = answer as Array<[string, string]> | null; return pairs != null && pairs.length === exercise.pairs.length; } case 'fill-in-the-blank': { const input = ((answer as string) || '').trim().toLowerCase(); return input === exercise.blank.trim().toLowerCase(); } case 'speak': { const spoken = ((answer as string) || '').trim().toLowerCase(); return exercise.acceptedAnswers.some( (a) => a.trim().toLowerCase() === spoken, ); } } } export function LessonPage() { const { topicId, lessonId } = useParams<{ topicId: string; lessonId: string; }>(); const navigate = useNavigate(); const { state, setExercises, restoreState, selectAnswer, checkAnswer, nextExercise, } = useLesson(); const { speak } = useSpeech(); const lesson = useMemo( () => (topicId && lessonId ? getLesson(topicId, lessonId) : null), [topicId, lessonId], ); // Load lesson exercises and restore saved state if available useEffect(() => { if (!lesson || !topicId || !lessonId) return; interface SavedState { current_index: number; hearts: number; correct_count: number; } apiClient .get(`/api/lesson-state/${topicId}/${lessonId}`) .then((saved) => { if (saved && saved.current_index > 0) { restoreState( lesson.exercises, saved.current_index, saved.hearts, saved.correct_count, ); } else { setExercises(lesson.exercises); } }) .catch(() => { // If fetching saved state fails, just start fresh setExercises(lesson.exercises); }); }, [lesson, topicId, lessonId, setExercises, restoreState]); const currentExercise: Exercise | undefined = state.exercises[state.currentIndex]; // Auto-play pronunciation when a new task is loaded useEffect(() => { if (!currentExercise || state.isChecked) return; let textToSpeak: string | null = null; if (currentExercise.type === 'speak') { textToSpeak = currentExercise.phrase; } else if (currentExercise.promptAudio) { textToSpeak = currentExercise.audioText ?? currentExercise.prompt; } if (textToSpeak) { speak(textToSpeak); } // Only trigger when the exercise index changes, not on every re-render // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.currentIndex, state.exercises]); const handleCheck = useCallback(() => { if (!currentExercise) return; const correct = isAnswerCorrect(currentExercise, state.selectedAnswer); checkAnswer(correct); }, [currentExercise, state.selectedAnswer, checkAnswer]); const handleContinue = useCallback(() => { if (!topicId || !lessonId) return; nextExercise(); // Save progress after advancing to next exercise. // Use current state values which reflect the CHECK_ANSWER result. const nextIndex = state.currentIndex + 1; const isLast = nextIndex >= state.exercises.length; if (!isLast) { apiClient .put('/api/lesson-state', { topic_id: topicId, lesson_id: lessonId, current_index: nextIndex, hearts: state.hearts, correct_count: state.correctCount, }) .catch(() => { // Save failed silently - not critical }); } }, [nextExercise, topicId, lessonId, state.currentIndex, state.exercises.length, state.hearts, state.correctCount]); // Navigate to review when finished useEffect(() => { if (state.isFinished && lesson) { navigate('/review', { state: { correctCount: state.correctCount, total: state.exercises.length, xp: lesson.xpReward, topicId: lesson.topicId, lessonId: lesson.id, hearts: state.hearts, }, replace: true, }); } }, [state.isFinished, state.correctCount, state.exercises.length, state.hearts, lesson, navigate]); if (!lesson) { return ( Lesson not found ); } if (state.exercises.length === 0) { return ( ); } if (!currentExercise) return null; const hasAnswer = state.selectedAnswer != null && state.selectedAnswer !== ''; const showLowHearts = state.hearts > 0 && state.hearts <= 2 && !state.isChecked; const lowHeartMsg = state.hearts === 1 ? 'Kaya mo pa yan!' : 'Konting tiis pa!'; return ( {/* Top bar: close, progress, hearts */} navigate('/home')} sx={{ color: 'text.secondary' }} > {/* Rambi encouragement when hearts are low */} {showLowHearts && ( )} {/* Exercise area */} {currentExercise.type === 'multiple-choice' && ( selectAnswer(index)} /> )} {currentExercise.type === 'translation' && ( selectAnswer(text)} /> )} {currentExercise.type === 'matching-pairs' && ( ) : null } isChecked={state.isChecked} onSelect={(pairs) => selectAnswer(pairs)} /> )} {currentExercise.type === 'fill-in-the-blank' && ( selectAnswer(text)} /> )} {currentExercise.type === 'speak' && ( selectAnswer(text)} /> )} {/* Bottom: Check button (Continue is now inside feedback banners) */} {!state.isChecked && ( )} {/* Feedback banners with embedded Continue button */} {state.isChecked && state.isCorrect === true && ( )} {state.isChecked && state.isCorrect === false && ( )} ); }