feat(vision): disable auto-detection with feature flag
- Add ENABLE_AUTO_DETECTION flag (set to false) in DockedVisionFeed - Conditionally import detection modules for tree-shaking when disabled - Guard all detection processing, loops, and value handlers - Hide detection overlay when auto-detection is disabled - Remove vision toggle button from ActiveSession (no longer needed) - Clean up unused imports and code - Format fixes from biome The camera feed still works for observation mode, but the ML/CV bead detection is disabled until accuracy is improved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8fb77e8e3
commit
a5025f01bc
|
|
@ -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<number>(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 && (
|
||||
<>
|
||||
<AbacusDock
|
||||
id="practice-abacus"
|
||||
columns={calculateAbacusColumns(attempt.problem.terms)}
|
||||
interactive={true}
|
||||
showNumbers={false}
|
||||
animated={true}
|
||||
onValueChange={handleAbacusDockValueChange}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: '100%',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
marginLeft: '1.5rem',
|
||||
})}
|
||||
style={{ height: problemHeight }}
|
||||
/>
|
||||
{/* Vision mode toggle button */}
|
||||
<button
|
||||
type="button"
|
||||
data-action="toggle-vision"
|
||||
data-enabled={isVisionEnabled}
|
||||
onClick={() => setIsVisionEnabled((prev) => !prev)}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: '100%',
|
||||
bottom: 0,
|
||||
marginLeft: '1.5rem',
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
fontSize: 'xs',
|
||||
bg: isVisionEnabled ? 'green.600' : isDark ? 'gray.700' : 'gray.200',
|
||||
color: isVisionEnabled ? 'white' : isDark ? 'gray.300' : 'gray.700',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: isVisionEnabled ? 'green.500' : isDark ? 'gray.600' : 'gray.300',
|
||||
},
|
||||
})}
|
||||
title="Use camera to detect physical abacus"
|
||||
>
|
||||
<span>📷</span>
|
||||
<span>Vision</span>
|
||||
</button>
|
||||
</>
|
||||
<AbacusDock
|
||||
id="practice-abacus"
|
||||
columns={calculateAbacusColumns(attempt.problem.terms)}
|
||||
interactive={true}
|
||||
showNumbers={false}
|
||||
animated={true}
|
||||
onValueChange={handleAbacusDockValueChange}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
left: '100%',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
marginLeft: '1.5rem',
|
||||
})}
|
||||
style={{ height: problemHeight }}
|
||||
/>
|
||||
)}
|
||||
</animated.div>
|
||||
</animated.div>
|
||||
|
|
@ -2130,27 +2078,6 @@ export function ActiveSession({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Abacus Vision Bridge - floating camera panel for physical abacus detection */}
|
||||
{isVisionEnabled && currentPart.type === 'abacus' && attempt && (
|
||||
<div
|
||||
data-component="vision-panel"
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
top: '200px', // Below main nav (80px) + sub nav (~56px) + mini sub-nav (~60px)
|
||||
right: '1rem',
|
||||
zIndex: Z_INDEX.DROPDOWN, // Above content but below modals
|
||||
boxShadow: 'xl',
|
||||
borderRadius: 'xl',
|
||||
})}
|
||||
>
|
||||
<AbacusVisionBridge
|
||||
columnCount={abacusDisplayConfig.physicalAbacusColumns}
|
||||
onValueDetected={handleVisionValueDetected}
|
||||
onClose={() => setIsVisionEnabled(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session Paused Modal - rendered here as single source of truth */}
|
||||
<SessionPausedModal
|
||||
isOpen={isPaused}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useMyAbacus } from '@/contexts/MyAbacusContext'
|
||||
import { useRemoteCameraDesktop } from '@/hooks/useRemoteCameraDesktop'
|
||||
import { analyzeColumns, analysesToDigits, digitsToNumber } from '@/lib/vision/beadDetector'
|
||||
import { processVideoFrame, processImageFrame } from '@/lib/vision/frameProcessor'
|
||||
import {
|
||||
cleanupArucoDetector,
|
||||
detectMarkers,
|
||||
|
|
@ -17,6 +15,43 @@ import { VisionCameraFeed } from './VisionCameraFeed'
|
|||
import { css } from '../../../styled-system/css'
|
||||
import type { CalibrationGrid } from '@/types/vision'
|
||||
|
||||
/**
|
||||
* Feature flag: Enable automatic abacus value detection from video feed.
|
||||
*
|
||||
* When enabled:
|
||||
* - Runs CV-based bead detection on video frames
|
||||
* - Shows detected value overlay
|
||||
* - Calls setDockedValue and onValueDetected with detected values
|
||||
*
|
||||
* When disabled:
|
||||
* - Only shows the video feed (no detection)
|
||||
* - Hides the detection overlay
|
||||
* - Does not interfere with student's manual input
|
||||
*
|
||||
* Set to true when ready to work on improving detection accuracy.
|
||||
*/
|
||||
const ENABLE_AUTO_DETECTION = false
|
||||
|
||||
// Only import detection modules when auto-detection is enabled
|
||||
// This ensures the detection code is tree-shaken when disabled
|
||||
let analyzeColumns: typeof import('@/lib/vision/beadDetector').analyzeColumns
|
||||
let analysesToDigits: typeof import('@/lib/vision/beadDetector').analysesToDigits
|
||||
let digitsToNumber: typeof import('@/lib/vision/beadDetector').digitsToNumber
|
||||
let processVideoFrame: typeof import('@/lib/vision/frameProcessor').processVideoFrame
|
||||
let processImageFrame: typeof import('@/lib/vision/frameProcessor').processImageFrame
|
||||
|
||||
if (ENABLE_AUTO_DETECTION) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const beadDetector = require('@/lib/vision/beadDetector')
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const frameProcessor = require('@/lib/vision/frameProcessor')
|
||||
analyzeColumns = beadDetector.analyzeColumns
|
||||
analysesToDigits = beadDetector.analysesToDigits
|
||||
digitsToNumber = beadDetector.digitsToNumber
|
||||
processVideoFrame = frameProcessor.processVideoFrame
|
||||
processImageFrame = frameProcessor.processImageFrame
|
||||
}
|
||||
|
||||
interface DockedVisionFeedProps {
|
||||
/** Called when a stable value is detected */
|
||||
onValueDetected?: (value: number) => 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<HTMLVideoElement>(null)
|
||||
const remoteImageRef = useRef<HTMLImageElement>(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
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Detection overlay */}
|
||||
<div
|
||||
data-element="detection-overlay"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
bg: 'rgba(0, 0, 0, 0.7)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
})}
|
||||
>
|
||||
{/* Detected value */}
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: 2 })}>
|
||||
<span className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', fontFamily: 'mono' })}>
|
||||
{detectedValue !== null ? detectedValue : '---'}
|
||||
</span>
|
||||
{detectedValue !== null && (
|
||||
<span className={css({ fontSize: 'xs', color: 'gray.400' })}>
|
||||
{Math.round(confidence * 100)}%
|
||||
{/* Detection overlay - only shown when auto-detection is enabled */}
|
||||
{ENABLE_AUTO_DETECTION && (
|
||||
<div
|
||||
data-element="detection-overlay"
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
bg: 'rgba(0, 0, 0, 0.7)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
})}
|
||||
>
|
||||
{/* Detected value */}
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: 2 })}>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
fontFamily: 'mono',
|
||||
})}
|
||||
>
|
||||
{detectedValue !== null ? detectedValue : '---'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{detectedValue !== null && (
|
||||
<span className={css({ fontSize: 'xs', color: 'gray.400' })}>
|
||||
{Math.round(confidence * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stability indicator */}
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: 1 })}>
|
||||
{stability.consecutiveFrames > 0 && (
|
||||
<div className={css({ display: 'flex', gap: 0.5 })}>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
w: '6px',
|
||||
h: '6px',
|
||||
borderRadius: 'full',
|
||||
bg: i < stability.consecutiveFrames ? 'green.500' : 'gray.600',
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Stability indicator */}
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: 1 })}>
|
||||
{stability.consecutiveFrames > 0 && (
|
||||
<div className={css({ display: 'flex', gap: 0.5 })}>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
w: '6px',
|
||||
h: '6px',
|
||||
borderRadius: 'full',
|
||||
bg: i < stability.consecutiveFrames ? 'green.500' : 'gray.600',
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Disable button */}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -64,7 +64,12 @@ export function ObserverVisionFeed({ frame }: ObserverVisionFeedProps) {
|
|||
{/* Detected value */}
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: 2 })}>
|
||||
<span
|
||||
className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', fontFamily: 'mono' })}
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
fontFamily: 'mono',
|
||||
})}
|
||||
>
|
||||
{frame.detectedValue !== null ? frame.detectedValue : '---'}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ interface VisionIndicatorProps {
|
|||
* - If not configured: opens setup modal
|
||||
* - If configured: toggles vision on/off
|
||||
*/
|
||||
export function VisionIndicator({ size = 'medium', position = 'bottom-right' }: VisionIndicatorProps) {
|
||||
export function VisionIndicator({
|
||||
size = 'medium',
|
||||
position = 'bottom-right',
|
||||
}: VisionIndicatorProps) {
|
||||
const { visionConfig, isVisionSetupComplete, openVisionSetup } = useMyAbacus()
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
|
|
@ -65,8 +68,9 @@ export function VisionIndicator({ size = 'medium', position = 'bottom-right' }:
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-action="toggle-vision"
|
||||
data-vision-status={!isVisionSetupComplete ? 'not-configured' : visionConfig.enabled ? 'enabled' : 'disabled'}
|
||||
data-vision-status={
|
||||
!isVisionSetupComplete ? 'not-configured' : visionConfig.enabled ? 'enabled' : 'disabled'
|
||||
}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
title={`${statusLabel} (right-click for settings)`}
|
||||
|
|
@ -97,8 +101,7 @@ export function VisionIndicator({ size = 'medium', position = 'bottom-right' }:
|
|||
>
|
||||
{/* Camera icon */}
|
||||
<span style={{ position: 'relative' }}>
|
||||
📷
|
||||
{/* Status dot */}
|
||||
📷{/* Status dot */}
|
||||
<span
|
||||
data-element="vision-status-dot"
|
||||
className={css({
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ export function VisionSetupModal() {
|
|||
{isVisionSetupComplete && (
|
||||
<button
|
||||
type="button"
|
||||
data-action="toggle-vision"
|
||||
onClick={() => {
|
||||
setVisionEnabled(!visionConfig.enabled)
|
||||
}}
|
||||
|
|
@ -236,7 +235,9 @@ function StatusRow({
|
|||
isConfigured: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={css({ display: 'flex', justifyContent: 'space-between', alignItems: 'center' })}>
|
||||
<div
|
||||
className={css({ display: 'flex', justifyContent: 'space-between', alignItems: 'center' })}
|
||||
>
|
||||
<span className={css({ color: 'gray.400', fontSize: 'sm' })}>{label}</span>
|
||||
<span
|
||||
className={css({
|
||||
|
|
|
|||
|
|
@ -184,7 +184,14 @@ export function useRemoteCameraDesktop(): UseRemoteCameraDesktopReturn {
|
|||
|
||||
const subscribe = useCallback(
|
||||
(sessionId: string) => {
|
||||
console.log('[RemoteCameraDesktop] Subscribing to session:', sessionId, 'socket:', !!socket, 'connected:', isConnected)
|
||||
console.log(
|
||||
'[RemoteCameraDesktop] Subscribing to session:',
|
||||
sessionId,
|
||||
'socket:',
|
||||
!!socket,
|
||||
'connected:',
|
||||
isConnected
|
||||
)
|
||||
if (!socket || !isConnected) {
|
||||
console.error('[RemoteCameraDesktop] Socket not connected!')
|
||||
setError('Socket not connected')
|
||||
|
|
|
|||
|
|
@ -326,7 +326,14 @@ export function useRemoteCameraPhone(
|
|||
const connect = useCallback(
|
||||
(sessionId: string) => {
|
||||
const socket = socketRef.current
|
||||
console.log('[RemoteCameraPhone] Connecting to session:', sessionId, 'socket:', !!socket, 'connected:', isSocketConnected)
|
||||
console.log(
|
||||
'[RemoteCameraPhone] Connecting to session:',
|
||||
sessionId,
|
||||
'socket:',
|
||||
!!socket,
|
||||
'connected:',
|
||||
isSocketConnected
|
||||
)
|
||||
if (!socket || !isSocketConnected) {
|
||||
console.error('[RemoteCameraPhone] Socket not connected!')
|
||||
setError('Socket not connected')
|
||||
|
|
|
|||
|
|
@ -66,11 +66,7 @@ export interface UseSessionBroadcastResult {
|
|||
/** Send part transition complete event to observers */
|
||||
sendPartTransitionComplete: () => void
|
||||
/** Send vision frame to observers (when student has vision mode enabled) */
|
||||
sendVisionFrame: (
|
||||
imageData: string,
|
||||
detectedValue: number | null,
|
||||
confidence: number
|
||||
) => void
|
||||
sendVisionFrame: (imageData: string, detectedValue: number | null, confidence: number) => void
|
||||
}
|
||||
|
||||
export function useSessionBroadcast(
|
||||
|
|
|
|||
Loading…
Reference in New Issue