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
// 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])

View File

@ -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()
}
},

View File

@ -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<string | null>(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) => {