diff --git a/apps/web/src/app/practice/[studentId]/PracticeClient.tsx b/apps/web/src/app/practice/[studentId]/PracticeClient.tsx index 20a20f8b..f9515d54 100644 --- a/apps/web/src/app/practice/[studentId]/PracticeClient.tsx +++ b/apps/web/src/app/practice/[studentId]/PracticeClient.tsx @@ -582,6 +582,7 @@ export function PracticeClient({ studentId, player, initialSession }: PracticeCl // Handle part transition complete - called when transition screen finishes // This is where we trigger game break (after "put away abacus" message is shown) const handlePartTransitionComplete = useCallback(() => { + console.log('[PracticeClient] handlePartTransitionComplete called, pendingGameBreak:', pendingGameBreak) // First, broadcast to observers sendPartTransitionComplete() @@ -591,6 +592,8 @@ export function PracticeClient({ studentId, player, initialSession }: PracticeCl setGameBreakStartTime(Date.now()) setShowGameBreak(true) setPendingGameBreak(false) + } else { + console.log('[PracticeClient] No pending game break, continuing to practice') } }, [sendPartTransitionComplete, pendingGameBreak]) diff --git a/apps/web/src/components/practice/ActiveSession.tsx b/apps/web/src/components/practice/ActiveSession.tsx index 08180485..8ffc1cb5 100644 --- a/apps/web/src/components/practice/ActiveSession.tsx +++ b/apps/web/src/components/practice/ActiveSession.tsx @@ -1361,11 +1361,23 @@ export function ActiveSession({ useEffect(() => { const prevIndex = prevPartIndexRef.current + console.log('[ActiveSession] Part transition effect:', { + prevIndex, + currentPartIndex, + partsLength: parts.length, + willTriggerTransition: currentPartIndex !== prevIndex && currentPartIndex < parts.length, + }) + // If part index changed and we have a valid next part if (currentPartIndex !== prevIndex && currentPartIndex < parts.length) { const prevPart = prevIndex < parts.length ? parts[prevIndex] : null const nextPart = parts[currentPartIndex] + console.log('[ActiveSession] Triggering part transition screen:', { + previousPartType: prevPart?.type ?? null, + nextPartType: nextPart.type, + }) + // Trigger transition screen const startTime = Date.now() setTransitionData({ @@ -1385,6 +1397,7 @@ export function ActiveSession({ // Handle transition screen completion (countdown finished or user skipped) const handleTransitionComplete = useCallback(() => { + console.log('[ActiveSession] Part transition complete, calling onPartTransitionComplete') setIsInPartTransition(false) setTransitionData(null) // Broadcast transition complete to observers @@ -1591,6 +1604,15 @@ export function ActiveSession({ const nextSlotIndex = currentSlotIndex + 1 const nextSlot = currentPart?.slots[nextSlotIndex] + console.log('[ActiveSession] Post-feedback timeout fired:', { + isCorrect, + currentSlotIndex, + nextSlotIndex, + hasNextSlot: !!nextSlot, + hasCurrentPart: !!currentPart, + willTransition: !!(nextSlot && currentPart && isCorrect), + }) + if (nextSlot && currentPart && isCorrect) { // Has next problem - animate transition if (!nextSlot.problem) { @@ -1602,9 +1624,11 @@ export function ActiveSession({ // Mark that we need to apply centering offset in useLayoutEffect needsCenteringOffsetRef.current = true + console.log('[ActiveSession] Starting transition to next problem') startTransition(nextSlot.problem, nextSlotIndex) } else { // End of part or incorrect - clear to loading + console.log('[ActiveSession] Calling clearToLoading (end of part or incorrect)') clearToLoading() } }, diff --git a/apps/web/src/components/practice/hooks/useInteractionPhase.ts b/apps/web/src/components/practice/hooks/useInteractionPhase.ts index 4a12b078..737c911a 100644 --- a/apps/web/src/components/practice/hooks/useInteractionPhase.ts +++ b/apps/web/src/components/practice/hooks/useInteractionPhase.ts @@ -662,8 +662,9 @@ export function useInteractionPhase( // ========================================================================== // React to activeProblem changes (single source of truth) // ========================================================================== - // This effect watches for activeProblem.key changes and loads the new problem. - // This eliminates the need for synchronization effects in the parent component. + // This effect handles two cases: + // 1. activeProblem.key changes (redo mode, direct navigation, session plan advances) + // 2. Phase becomes 'loading' (need to reload current problem after incorrect answer or part transition) const prevActiveProblemKeyRef = useRef(null) const hasMountedRef = useRef(false) @@ -673,35 +674,78 @@ export function useInteractionPhase( hasMountedRef.current = true // Initialize the ref with current key prevActiveProblemKeyRef.current = activeProblem?.key ?? null + console.log('[useInteractionPhase] Initial mount, key:', activeProblem?.key) return } // If no active problem, nothing to do if (!activeProblem) { + console.log('[useInteractionPhase] No active problem - cannot load. Current phase:', phase.phase) prevActiveProblemKeyRef.current = null return } const prevKey = prevActiveProblemKeyRef.current const currentKey = activeProblem.key + const keyChanged = prevKey !== currentKey - // Key hasn't changed - no action needed - if (prevKey === currentKey) { + console.log('[useInteractionPhase] Effect running:', { + prevKey, + currentKey, + keyChanged, + currentPhase: phase.phase, + }) + + // Case 1: Phase is 'loading' - need to load the problem + // This happens after incorrect answers or when returning from part transitions + if (phase.phase === 'loading') { + console.log('[useInteractionPhase] Phase is loading - loading problem:', { + problemAnswer: activeProblem.problem.answer, + slotIndex: activeProblem.slotIndex, + partIndex: activeProblem.partIndex, + key: activeProblem.key, + }) + const newAttempt = createAttemptInput( + activeProblem.problem, + activeProblem.slotIndex, + activeProblem.partIndex + ) + setPhase({ phase: 'inputting', attempt: newAttempt }) + prevActiveProblemKeyRef.current = currentKey + console.log('[useInteractionPhase] Successfully loaded problem, phase now inputting') return } - // Key changed - load the new problem immediately - // Note: We don't check phase here because this is for redo mode / direct navigation - // which should immediately switch problems (no animation) - const newAttempt = createAttemptInput( - activeProblem.problem, - activeProblem.slotIndex, - activeProblem.partIndex - ) - setPhase({ phase: 'inputting', attempt: newAttempt }) + // Case 2: Key changed - handle redo mode or session advancement + if (keyChanged) { + console.log('[useInteractionPhase] Key changed:', { prevKey, currentKey }) - prevActiveProblemKeyRef.current = currentKey - }, [activeProblem]) + // CRITICAL: Don't interrupt normal progression flow + // If we're in showingFeedback, submitting, or transitioning, the normal flow + // (startTransition → completeTransition) handles advancing to the next problem. + const isInProgressionFlow = + phase.phase === 'showingFeedback' || + phase.phase === 'submitting' || + phase.phase === 'transitioning' + + if (isInProgressionFlow) { + console.log('[useInteractionPhase] Key changed but in progression flow, deferring') + // Still update the ref so we track the change + prevActiveProblemKeyRef.current = currentKey + return + } + + // Key changed and we're not in progression flow - load the new problem immediately + console.log('[useInteractionPhase] Loading new problem from key change') + const newAttempt = createAttemptInput( + activeProblem.problem, + activeProblem.slotIndex, + activeProblem.partIndex + ) + setPhase({ phase: 'inputting', attempt: newAttempt }) + prevActiveProblemKeyRef.current = currentKey + } + }, [activeProblem, phase.phase]) const handleDigit = useCallback( (digit: string) => {