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 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2026-01-14 18:32:39 -06:00
parent 4733149497
commit cecd1e93e2
3 changed files with 86 additions and 15 deletions

View File

@ -582,6 +582,7 @@ export function PracticeClient({ studentId, player, initialSession }: PracticeCl
// Handle part transition complete - called when transition screen finishes // Handle part transition complete - called when transition screen finishes
// This is where we trigger game break (after "put away abacus" message is shown) // This is where we trigger game break (after "put away abacus" message is shown)
const handlePartTransitionComplete = useCallback(() => { const handlePartTransitionComplete = useCallback(() => {
console.log('[PracticeClient] handlePartTransitionComplete called, pendingGameBreak:', pendingGameBreak)
// First, broadcast to observers // First, broadcast to observers
sendPartTransitionComplete() sendPartTransitionComplete()
@ -591,6 +592,8 @@ export function PracticeClient({ studentId, player, initialSession }: PracticeCl
setGameBreakStartTime(Date.now()) setGameBreakStartTime(Date.now())
setShowGameBreak(true) setShowGameBreak(true)
setPendingGameBreak(false) setPendingGameBreak(false)
} else {
console.log('[PracticeClient] No pending game break, continuing to practice')
} }
}, [sendPartTransitionComplete, pendingGameBreak]) }, [sendPartTransitionComplete, pendingGameBreak])

View File

@ -1361,11 +1361,23 @@ export function ActiveSession({
useEffect(() => { useEffect(() => {
const prevIndex = prevPartIndexRef.current 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 part index changed and we have a valid next part
if (currentPartIndex !== prevIndex && currentPartIndex < parts.length) { if (currentPartIndex !== prevIndex && currentPartIndex < parts.length) {
const prevPart = prevIndex < parts.length ? parts[prevIndex] : null const prevPart = prevIndex < parts.length ? parts[prevIndex] : null
const nextPart = parts[currentPartIndex] const nextPart = parts[currentPartIndex]
console.log('[ActiveSession] Triggering part transition screen:', {
previousPartType: prevPart?.type ?? null,
nextPartType: nextPart.type,
})
// Trigger transition screen // Trigger transition screen
const startTime = Date.now() const startTime = Date.now()
setTransitionData({ setTransitionData({
@ -1385,6 +1397,7 @@ export function ActiveSession({
// Handle transition screen completion (countdown finished or user skipped) // Handle transition screen completion (countdown finished or user skipped)
const handleTransitionComplete = useCallback(() => { const handleTransitionComplete = useCallback(() => {
console.log('[ActiveSession] Part transition complete, calling onPartTransitionComplete')
setIsInPartTransition(false) setIsInPartTransition(false)
setTransitionData(null) setTransitionData(null)
// Broadcast transition complete to observers // Broadcast transition complete to observers
@ -1591,6 +1604,15 @@ export function ActiveSession({
const nextSlotIndex = currentSlotIndex + 1 const nextSlotIndex = currentSlotIndex + 1
const nextSlot = currentPart?.slots[nextSlotIndex] 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) { if (nextSlot && currentPart && isCorrect) {
// Has next problem - animate transition // Has next problem - animate transition
if (!nextSlot.problem) { if (!nextSlot.problem) {
@ -1602,9 +1624,11 @@ export function ActiveSession({
// Mark that we need to apply centering offset in useLayoutEffect // Mark that we need to apply centering offset in useLayoutEffect
needsCenteringOffsetRef.current = true needsCenteringOffsetRef.current = true
console.log('[ActiveSession] Starting transition to next problem')
startTransition(nextSlot.problem, nextSlotIndex) startTransition(nextSlot.problem, nextSlotIndex)
} else { } else {
// End of part or incorrect - clear to loading // End of part or incorrect - clear to loading
console.log('[ActiveSession] Calling clearToLoading (end of part or incorrect)')
clearToLoading() clearToLoading()
} }
}, },

View File

@ -662,8 +662,9 @@ export function useInteractionPhase(
// ========================================================================== // ==========================================================================
// React to activeProblem changes (single source of truth) // React to activeProblem changes (single source of truth)
// ========================================================================== // ==========================================================================
// This effect watches for activeProblem.key changes and loads the new problem. // This effect handles two cases:
// This eliminates the need for synchronization effects in the parent component. // 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<string | null>(null) const prevActiveProblemKeyRef = useRef<string | null>(null)
const hasMountedRef = useRef(false) const hasMountedRef = useRef(false)
@ -673,35 +674,78 @@ export function useInteractionPhase(
hasMountedRef.current = true hasMountedRef.current = true
// Initialize the ref with current key // Initialize the ref with current key
prevActiveProblemKeyRef.current = activeProblem?.key ?? null prevActiveProblemKeyRef.current = activeProblem?.key ?? null
console.log('[useInteractionPhase] Initial mount, key:', activeProblem?.key)
return return
} }
// If no active problem, nothing to do // If no active problem, nothing to do
if (!activeProblem) { if (!activeProblem) {
console.log('[useInteractionPhase] No active problem - cannot load. Current phase:', phase.phase)
prevActiveProblemKeyRef.current = null prevActiveProblemKeyRef.current = null
return return
} }
const prevKey = prevActiveProblemKeyRef.current const prevKey = prevActiveProblemKeyRef.current
const currentKey = activeProblem.key const currentKey = activeProblem.key
const keyChanged = prevKey !== currentKey
// Key hasn't changed - no action needed console.log('[useInteractionPhase] Effect running:', {
if (prevKey === currentKey) { 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 return
} }
// Key changed - load the new problem immediately // Case 2: Key changed - handle redo mode or session advancement
// Note: We don't check phase here because this is for redo mode / direct navigation if (keyChanged) {
// which should immediately switch problems (no animation) console.log('[useInteractionPhase] Key changed:', { prevKey, currentKey })
const newAttempt = createAttemptInput(
activeProblem.problem,
activeProblem.slotIndex,
activeProblem.partIndex
)
setPhase({ phase: 'inputting', attempt: newAttempt })
prevActiveProblemKeyRef.current = currentKey // CRITICAL: Don't interrupt normal progression flow
}, [activeProblem]) // 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( const handleDigit = useCallback(
(digit: string) => { (digit: string) => {