From cecd1e93e2cf7ce77d597d64f1694563a9c9b055 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Wed, 14 Jan 2026 18:32:39 -0600 Subject: [PATCH] fix(practice): handle loading phase in activeProblem effect The useInteractionPhase effect now handles two cases: 1. activeProblem.key changes (redo mode, navigation, session advances) 2. Phase becomes 'loading' (after incorrect answer or part transition) Previously, only key changes triggered problem loading, which caused "Loading next problem..." to persist after incorrect answers since clearToLoading() sets phase to 'loading' without changing the key. Also adds debug logging for part transitions and game breaks. Co-Authored-By: Claude Opus 4.5 --- .../practice/[studentId]/PracticeClient.tsx | 3 + .../src/components/practice/ActiveSession.tsx | 24 ++++++ .../practice/hooks/useInteractionPhase.ts | 74 +++++++++++++++---- 3 files changed, 86 insertions(+), 15 deletions(-) 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) => {