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:
parent
4733149497
commit
cecd1e93e2
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue