diff --git a/apps/web/src/components/practice/ActiveSession.tsx b/apps/web/src/components/practice/ActiveSession.tsx index 20b6a0af..86890b68 100644 --- a/apps/web/src/components/practice/ActiveSession.tsx +++ b/apps/web/src/components/practice/ActiveSession.tsx @@ -7,8 +7,6 @@ import { useMyAbacus } from '@/contexts/MyAbacusContext' import { useTheme } from '@/contexts/ThemeContext' import { getCurrentProblemInfo, - isInRetryEpoch, - needsRetryTransition, type ProblemSlot, type SessionHealth, type SessionPart, @@ -50,8 +48,6 @@ import { PracticeHelpOverlay } from './PracticeHelpOverlay' import { ProblemDebugPanel } from './ProblemDebugPanel' import { VerticalProblem } from './VerticalProblem' import type { ReceivedAbacusControl } from '@/hooks/useSessionBroadcast' -import { AbacusVisionBridge } from '../vision' -import { Z_INDEX } from '@/constants/zIndex' /** * Timing data for the current problem attempt @@ -995,9 +991,6 @@ export function ActiveSession({ // Track previous epoch to detect epoch changes const prevEpochRef = useRef(0) - // Vision mode state - for physical abacus camera detection - const [isVisionEnabled, setIsVisionEnabled] = useState(false) - // Browse mode state - isBrowseMode is controlled via props // browseIndex can be controlled (browseIndexProp + onBrowseIndexChange) or internal const [internalBrowseIndex, setInternalBrowseIndex] = useState(0) @@ -1323,17 +1316,6 @@ export function ActiveSession({ [setAnswer] ) - // Handle value detected from vision (physical abacus camera) - const handleVisionValueDetected = useCallback( - (value: number) => { - // Update the docked abacus to show the detected value - setDockedValue(value) - // Also set the answer input - setAnswer(String(value)) - }, - [setDockedValue, setAnswer] - ) - // Handle submit const handleSubmit = useCallback(async () => { // Allow submitting from inputting, awaitingDisambiguation, or helpMode @@ -1996,56 +1978,22 @@ export function ActiveSession({ {/* Abacus dock - positioned absolutely so it doesn't affect problem centering */} {/* Width 100% matches problem width, height matches problem height */} {currentPart.type === 'abacus' && !showHelpOverlay && (problemHeight ?? 0) > 0 && ( - <> - - {/* Vision mode toggle button */} - - + )} @@ -2130,27 +2078,6 @@ export function ActiveSession({ /> )} - {/* Abacus Vision Bridge - floating camera panel for physical abacus detection */} - {isVisionEnabled && currentPart.type === 'abacus' && attempt && ( -
- setIsVisionEnabled(false)} - /> -
- )} - {/* Session Paused Modal - rendered here as single source of truth */} void @@ -33,7 +68,8 @@ interface DockedVisionFeedProps { * - Shows the video feed with detection overlay */ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVisionFeedProps) { - const { visionConfig, setDockedValue, setVisionEnabled, setVisionCalibration, emitVisionFrame } = useMyAbacus() + const { visionConfig, setDockedValue, setVisionEnabled, setVisionCalibration, emitVisionFrame } = + useMyAbacus() const videoRef = useRef(null) const remoteImageRef = useRef(null) @@ -51,6 +87,7 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis const [isArucoReady, setIsArucoReady] = useState(false) const [markersFound, setMarkersFound] = useState(0) + // Stability tracking for detected values (hook must be called unconditionally) const stability = useFrameStability() // Determine camera source (must be before effects that use these) @@ -152,7 +189,14 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis markerDetectionFrameRef.current = null } } - }, [visionConfig.enabled, isLocalCamera, videoStream, isArucoReady, columnCount, setVisionCalibration]) + }, [ + visionConfig.enabled, + isLocalCamera, + videoStream, + isArucoReady, + columnCount, + setVisionCalibration, + ]) // Remote camera hook const { @@ -234,7 +278,13 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis return () => { remoteUnsubscribe() } - }, [visionConfig.enabled, isRemoteCamera, visionConfig.remoteCameraSessionId, remoteSubscribe, remoteUnsubscribe]) + }, [ + visionConfig.enabled, + isRemoteCamera, + visionConfig.remoteCameraSessionId, + remoteSubscribe, + remoteUnsubscribe, + ]) // Update loading state when remote camera connects useEffect(() => { @@ -243,8 +293,11 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis } }, [isRemoteCamera, remoteIsPhoneConnected]) - // Process local camera frames for detection + // Process local camera frames for detection (only when enabled) const processLocalFrame = useCallback(() => { + // Skip detection when feature is disabled + if (!ENABLE_AUTO_DETECTION) return + const now = performance.now() if (now - lastInferenceTimeRef.current < INFERENCE_INTERVAL_MS) { return @@ -270,8 +323,11 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis stability.pushFrame(value, minConfidence) }, [visionConfig.calibration, stability]) - // Process remote camera frames for detection + // Process remote camera frames for detection (only when enabled) useEffect(() => { + // Skip detection when feature is disabled + if (!ENABLE_AUTO_DETECTION) return + if (!isRemoteCamera || !remoteIsPhoneConnected || !remoteLatestFrame) { return } @@ -302,8 +358,11 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis stability.pushFrame(value, minConfidence) }, [isRemoteCamera, remoteIsPhoneConnected, remoteLatestFrame, columnCount, stability]) - // Local camera detection loop + // Local camera detection loop (only when enabled) useEffect(() => { + // Skip detection loop when feature is disabled + if (!ENABLE_AUTO_DETECTION) return + if (!visionConfig.enabled || !isLocalCamera || !videoStream || !visionConfig.calibration) { return } @@ -326,17 +385,32 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis animationFrameRef.current = null } } - }, [visionConfig.enabled, isLocalCamera, videoStream, visionConfig.calibration, processLocalFrame]) + }, [ + visionConfig.enabled, + isLocalCamera, + videoStream, + visionConfig.calibration, + processLocalFrame, + ]) - // Handle stable value changes + // Handle stable value changes (only when auto-detection is enabled) useEffect(() => { + // Skip value updates when feature is disabled + if (!ENABLE_AUTO_DETECTION) return + if (stability.stableValue !== null && stability.stableValue !== detectedValue) { setDetectedValue(stability.stableValue) setConfidence(stability.currentConfidence) setDockedValue(stability.stableValue) onValueDetected?.(stability.stableValue) } - }, [stability.stableValue, stability.currentConfidence, detectedValue, setDockedValue, onValueDetected]) + }, [ + stability.stableValue, + stability.currentConfidence, + detectedValue, + setDockedValue, + onValueDetected, + ]) // Broadcast vision frames to observers (5fps to save bandwidth) const BROADCAST_INTERVAL_MS = 200 @@ -530,53 +604,62 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis )} - {/* Detection overlay */} -
- {/* Detected value */} -
- - {detectedValue !== null ? detectedValue : '---'} - - {detectedValue !== null && ( - - {Math.round(confidence * 100)}% + {/* Detection overlay - only shown when auto-detection is enabled */} + {ENABLE_AUTO_DETECTION && ( +
+ {/* Detected value */} +
+ + {detectedValue !== null ? detectedValue : '---'} - )} -
+ {detectedValue !== null && ( + + {Math.round(confidence * 100)}% + + )} +
- {/* Stability indicator */} -
- {stability.consecutiveFrames > 0 && ( -
- {Array.from({ length: 3 }).map((_, i) => ( -
- ))} -
- )} + {/* Stability indicator */} +
+ {stability.consecutiveFrames > 0 && ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ )} +
-
+ )} {/* Disable button */}