feat(vision): add activeCameraSource tracking and simplify calibration UI
- Add explicit activeCameraSource field to VisionConfig to track which camera is in use (local vs phone), fixing button visibility bugs when switching between camera sources - Simplify calibration UI by removing the confusing "Auto/Manual" mode toggle, replacing with a cleaner crop status indicator - Remove calibration requirement from isVisionSetupComplete for local camera since auto-crop runs continuously when markers are detected - Update DockedVisionFeed to use activeCameraSource instead of inferring from which configs are set 🤖 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
70b363ce88
commit
1be6151bae
|
|
@ -449,7 +449,20 @@
|
|||
"Bash(apps/web/src/lib/vision/frameProcessor.ts )",
|
||||
"Bash(apps/web/src/lib/vision/beadDetector.ts )",
|
||||
"Bash(apps/web/public/models/abacus-column-classifier/model.json )",
|
||||
"Bash(.claude/settings.local.json)"
|
||||
"Bash(.claude/settings.local.json)",
|
||||
"Bash(apps/web/src/components/MyAbacus.tsx )",
|
||||
"Bash(apps/web/src/contexts/MyAbacusContext.tsx )",
|
||||
"Bash(apps/web/src/components/vision/DockedVisionFeed.tsx )",
|
||||
"Bash(apps/web/src/components/vision/VisionIndicator.tsx )",
|
||||
"Bash(apps/web/src/components/vision/VisionSetupModal.tsx)",
|
||||
"Bash(npx storybook:*)",
|
||||
"Bash(apps/web/src/hooks/usePhoneCamera.ts )",
|
||||
"Bash(apps/web/src/lib/remote-camera/session-manager.ts )",
|
||||
"Bash(apps/web/src/test/setup.ts )",
|
||||
"Bash(apps/web/src/hooks/__tests__/useRemoteCameraDesktop.test.ts )",
|
||||
"Bash(apps/web/src/hooks/__tests__/useRemoteCameraPhone.test.ts )",
|
||||
"Bash(apps/web/src/lib/remote-camera/__tests__/)",
|
||||
"Bash(packages/abacus-react/CHANGELOG.md )"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -301,7 +301,9 @@ export function AbacusDisplayDropdown({
|
|||
step="1"
|
||||
value={config.physicalAbacusColumns}
|
||||
onChange={(e) =>
|
||||
updateConfig({ physicalAbacusColumns: parseInt(e.target.value, 10) })
|
||||
updateConfig({
|
||||
physicalAbacusColumns: parseInt(e.target.value, 10),
|
||||
})
|
||||
}
|
||||
className={css({
|
||||
flex: 1,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export interface VisionConfigurationChange {
|
|||
calibration?: import('@/types/vision').CalibrationGrid | null
|
||||
/** Remote camera session ID (for phone camera) */
|
||||
remoteCameraSessionId?: string | null
|
||||
/** Active camera source (tracks which camera is in use) */
|
||||
activeCameraSource?: CameraSource | null
|
||||
}
|
||||
|
||||
export interface AbacusVisionBridgeProps {
|
||||
|
|
@ -172,23 +174,26 @@ export function AbacusVisionBridge({
|
|||
if (source === 'phone') {
|
||||
// Stop local camera
|
||||
vision.disable()
|
||||
// Clear local camera config in parent context
|
||||
onConfigurationChange?.({ cameraDeviceId: null, calibration: null })
|
||||
// Set active camera source to phone and clear local camera config
|
||||
// (but keep local config in storage for when we switch back)
|
||||
onConfigurationChange?.({ activeCameraSource: 'phone' })
|
||||
// Check for persisted session and reuse it
|
||||
const persistedSessionId = remoteGetPersistedSessionId()
|
||||
if (persistedSessionId) {
|
||||
console.log('[AbacusVisionBridge] Reusing persisted remote session:', persistedSessionId)
|
||||
setRemoteCameraSessionId(persistedSessionId)
|
||||
// Notify parent about the reused session
|
||||
onConfigurationChange?.({ remoteCameraSessionId: persistedSessionId })
|
||||
onConfigurationChange?.({
|
||||
remoteCameraSessionId: persistedSessionId,
|
||||
})
|
||||
}
|
||||
// If no persisted session, RemoteCameraQRCode will create one
|
||||
} else {
|
||||
// Switching to local camera
|
||||
// Clear remote session in context so DockedVisionFeed uses local camera
|
||||
// The session still persists in localStorage (via useRemoteCameraDesktop) for when we switch back
|
||||
// Set active camera source to local
|
||||
// The remote session still persists in localStorage (via useRemoteCameraDesktop) for when we switch back
|
||||
setRemoteCameraSessionId(null)
|
||||
onConfigurationChange?.({ remoteCameraSessionId: null })
|
||||
onConfigurationChange?.({ activeCameraSource: 'local' })
|
||||
vision.enable()
|
||||
}
|
||||
},
|
||||
|
|
@ -284,7 +289,7 @@ export function AbacusVisionBridge({
|
|||
}
|
||||
}, [vision.cameraError, onError])
|
||||
|
||||
// Notify about local camera device changes
|
||||
// Notify about local camera device changes and ensure activeCameraSource is set
|
||||
useEffect(() => {
|
||||
if (
|
||||
cameraSource === 'local' &&
|
||||
|
|
@ -292,7 +297,11 @@ export function AbacusVisionBridge({
|
|||
vision.selectedDeviceId !== lastReportedCameraRef.current
|
||||
) {
|
||||
lastReportedCameraRef.current = vision.selectedDeviceId
|
||||
onConfigurationChange?.({ cameraDeviceId: vision.selectedDeviceId })
|
||||
// Set both the camera device ID and the active camera source
|
||||
onConfigurationChange?.({
|
||||
cameraDeviceId: vision.selectedDeviceId,
|
||||
activeCameraSource: 'local',
|
||||
})
|
||||
}
|
||||
}, [cameraSource, vision.selectedDeviceId, onConfigurationChange])
|
||||
|
||||
|
|
@ -437,7 +446,10 @@ export function AbacusVisionBridge({
|
|||
const updateDimensions = () => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
setRemoteContainerDimensions({ width: rect.width, height: rect.height })
|
||||
setRemoteContainerDimensions({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -757,145 +769,170 @@ export function AbacusVisionBridge({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Calibration mode toggle (both local and phone camera) */}
|
||||
{/* Crop status - shows either marker detection or manual crop status */}
|
||||
<div
|
||||
data-element="calibration-mode"
|
||||
data-element="crop-status"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
bg: 'gray.800',
|
||||
borderRadius: 'md',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400', fontSize: 'sm' })}>Mode:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
cameraSource === 'local'
|
||||
? vision.setCalibrationMode('auto')
|
||||
: handleRemoteModeChange('auto')
|
||||
}
|
||||
className={css({
|
||||
px: 3,
|
||||
py: 1,
|
||||
fontSize: 'sm',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
bg:
|
||||
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) === 'auto'
|
||||
? 'blue.600'
|
||||
: 'gray.700',
|
||||
color: 'white',
|
||||
_hover: {
|
||||
bg:
|
||||
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) ===
|
||||
'auto'
|
||||
? 'blue.500'
|
||||
: 'gray.600',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Auto (Markers)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
cameraSource === 'local'
|
||||
? vision.setCalibrationMode('manual')
|
||||
: handleRemoteModeChange('manual')
|
||||
}
|
||||
className={css({
|
||||
px: 3,
|
||||
py: 1,
|
||||
fontSize: 'sm',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
bg:
|
||||
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) ===
|
||||
'manual'
|
||||
? 'blue.600'
|
||||
: 'gray.700',
|
||||
color: 'white',
|
||||
_hover: {
|
||||
bg:
|
||||
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) ===
|
||||
'manual'
|
||||
? 'blue.500'
|
||||
: 'gray.600',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Manual
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Marker detection status (in auto mode) */}
|
||||
{((cameraSource === 'local' && vision.calibrationMode === 'auto') ||
|
||||
(cameraSource === 'phone' && remoteCalibrationMode === 'auto')) && (
|
||||
<div
|
||||
data-element="marker-status"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
p: 2,
|
||||
bg:
|
||||
cameraSource === 'local'
|
||||
? vision.markerDetection.allMarkersFound
|
||||
? 'green.900'
|
||||
: 'gray.800'
|
||||
: remoteFrameMode === 'cropped'
|
||||
? 'green.900'
|
||||
: 'gray.800',
|
||||
borderRadius: 'md',
|
||||
transition: 'background-color 0.2s',
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: 2 })}>
|
||||
<span
|
||||
className={css({
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: 'full',
|
||||
bg:
|
||||
cameraSource === 'local'
|
||||
? vision.markerDetection.allMarkersFound
|
||||
? 'green.400'
|
||||
: 'yellow.400'
|
||||
: remoteFrameMode === 'cropped'
|
||||
? 'green.400'
|
||||
: 'yellow.400',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: 'white', fontSize: 'sm' })}>
|
||||
{cameraSource === 'local'
|
||||
? vision.markerDetection.allMarkersFound
|
||||
? 'All markers detected'
|
||||
: `Markers: ${vision.markerDetection.markersFound}/4`
|
||||
: remoteFrameMode === 'cropped'
|
||||
? 'Phone auto-cropping active'
|
||||
: 'Waiting for phone markers...'}
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
href="/create/vision-markers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{/* Manual crop indicator (if set) */}
|
||||
{((cameraSource === 'local' && vision.isCalibrated) ||
|
||||
(cameraSource === 'phone' && remoteCalibration)) && (
|
||||
<div
|
||||
className={css({
|
||||
color: 'blue.300',
|
||||
fontSize: 'xs',
|
||||
textDecoration: 'underline',
|
||||
_hover: { color: 'blue.200' },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
})}
|
||||
>
|
||||
Get markers
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: 'full',
|
||||
bg: 'blue.400',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: 'white', fontSize: 'sm' })}>Using manual crop</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (cameraSource === 'local') {
|
||||
vision.resetCalibration()
|
||||
} else {
|
||||
handleRemoteResetCalibration()
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
px: 2,
|
||||
py: 1,
|
||||
fontSize: 'xs',
|
||||
bg: 'transparent',
|
||||
color: 'gray.400',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.600',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { borderColor: 'gray.500', color: 'gray.300' },
|
||||
})}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Marker detection status (always shown when no manual crop) */}
|
||||
{!(
|
||||
(cameraSource === 'local' && vision.isCalibrated) ||
|
||||
(cameraSource === 'phone' && remoteCalibration)
|
||||
) && (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: 'full',
|
||||
bg:
|
||||
cameraSource === 'local'
|
||||
? vision.markerDetection.allMarkersFound
|
||||
? 'green.400'
|
||||
: 'yellow.400'
|
||||
: remoteFrameMode === 'cropped'
|
||||
? 'green.400'
|
||||
: 'yellow.400',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ color: 'white', fontSize: 'sm' })}>
|
||||
{cameraSource === 'local'
|
||||
? vision.markerDetection.allMarkersFound
|
||||
? 'Auto-crop active'
|
||||
: `Markers: ${vision.markerDetection.markersFound}/4`
|
||||
: remoteFrameMode === 'cropped'
|
||||
? 'Phone auto-cropping'
|
||||
: 'Waiting for markers...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', gap: 2 })}>
|
||||
<a
|
||||
href="/create/vision-markers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={css({
|
||||
color: 'blue.300',
|
||||
fontSize: 'xs',
|
||||
textDecoration: 'underline',
|
||||
_hover: { color: 'blue.200' },
|
||||
})}
|
||||
>
|
||||
Get markers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual crop button - only show when not calibrating and no manual calibration */}
|
||||
{!(
|
||||
(cameraSource === 'local' && vision.isCalibrated) ||
|
||||
(cameraSource === 'phone' && remoteCalibration)
|
||||
) &&
|
||||
!(cameraSource === 'local' ? vision.isCalibrating : remoteIsCalibrating) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (cameraSource === 'local') {
|
||||
vision.setCalibrationMode('manual')
|
||||
vision.startCalibration()
|
||||
} else {
|
||||
handleRemoteModeChange('manual')
|
||||
handleRemoteStartCalibration()
|
||||
}
|
||||
}}
|
||||
disabled={cameraSource === 'local' ? !vision.videoStream : !remoteIsPhoneConnected}
|
||||
className={css({
|
||||
px: 3,
|
||||
py: 1.5,
|
||||
fontSize: 'sm',
|
||||
bg: 'transparent',
|
||||
color: 'gray.300',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.600',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { borderColor: 'gray.500', bg: 'gray.700' },
|
||||
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
|
||||
})}
|
||||
>
|
||||
Set crop manually
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Camera feed */}
|
||||
<div ref={cameraFeedContainerRef} className={css({ position: 'relative' })}>
|
||||
|
|
@ -1168,134 +1205,6 @@ export function AbacusVisionBridge({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions (manual mode - both local and phone camera) */}
|
||||
{((cameraSource === 'local' && vision.calibrationMode === 'manual') ||
|
||||
(cameraSource === 'phone' && remoteCalibrationMode === 'manual')) && (
|
||||
<div
|
||||
data-element="actions"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
})}
|
||||
>
|
||||
{cameraSource === 'local' ? (
|
||||
/* Local camera actions */
|
||||
!vision.isCalibrated ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={vision.startCalibration}
|
||||
disabled={!videoDimensions}
|
||||
className={css({
|
||||
flex: 1,
|
||||
py: 2,
|
||||
bg: 'blue.600',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
fontWeight: 'medium',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'blue.500' },
|
||||
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
|
||||
})}
|
||||
>
|
||||
Calibrate
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={vision.startCalibration}
|
||||
className={css({
|
||||
flex: 1,
|
||||
py: 2,
|
||||
bg: 'gray.700',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'gray.600' },
|
||||
})}
|
||||
>
|
||||
Recalibrate
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={vision.resetCalibration}
|
||||
className={css({
|
||||
py: 2,
|
||||
px: 3,
|
||||
bg: 'red.700',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'red.600' },
|
||||
})}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
) : /* Phone camera actions */
|
||||
!remoteCalibration ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoteStartCalibration}
|
||||
disabled={!remoteIsPhoneConnected}
|
||||
className={css({
|
||||
flex: 1,
|
||||
py: 2,
|
||||
bg: 'blue.600',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
fontWeight: 'medium',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'blue.500' },
|
||||
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
|
||||
})}
|
||||
>
|
||||
Calibrate
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoteStartCalibration}
|
||||
className={css({
|
||||
flex: 1,
|
||||
py: 2,
|
||||
bg: 'gray.700',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'gray.600' },
|
||||
})}
|
||||
>
|
||||
Recalibrate
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoteResetCalibration}
|
||||
className={css({
|
||||
py: 2,
|
||||
px: 3,
|
||||
bg: 'red.700',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: 'md',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'red.600' },
|
||||
})}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
{cameraSource === 'local' && !vision.isCalibrated && !vision.isCalibrating && (
|
||||
<p
|
||||
|
|
@ -1305,9 +1214,7 @@ export function AbacusVisionBridge({
|
|||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{vision.calibrationMode === 'auto'
|
||||
? 'Place ArUco markers on your abacus corners for automatic detection'
|
||||
: 'Point your camera at a soroban and click Calibrate to set up detection'}
|
||||
Place ArUco markers on your abacus corners, or set the crop manually
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
@ -1334,9 +1241,7 @@ export function AbacusVisionBridge({
|
|||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{remoteCalibrationMode === 'auto'
|
||||
? 'Place ArUco markers on your abacus corners for automatic detection'
|
||||
: 'Point your phone camera at a soroban and click Calibrate to set up detection'}
|
||||
Phone auto-detects ArUco markers, or set the crop manually
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,10 +90,9 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis
|
|||
// Stability tracking for detected values (hook must be called unconditionally)
|
||||
const stability = useFrameStability()
|
||||
|
||||
// Determine camera source (must be before effects that use these)
|
||||
// Prioritize local camera if configured - remote camera only if no local camera
|
||||
const isLocalCamera = visionConfig.cameraDeviceId !== null
|
||||
const isRemoteCamera = !isLocalCamera && visionConfig.remoteCameraSessionId !== null
|
||||
// Determine camera source from explicit activeCameraSource field
|
||||
const isLocalCamera = visionConfig.activeCameraSource === 'local'
|
||||
const isRemoteCamera = visionConfig.activeCameraSource === 'phone'
|
||||
|
||||
// Load and initialize ArUco on mount (for local camera auto-calibration)
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -58,8 +58,14 @@ export function RemoteCameraReceiver({
|
|||
const [calibration, setCalibration] = useState<CalibrationGrid | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const imageRef = useRef<HTMLImageElement>(null)
|
||||
const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 })
|
||||
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 })
|
||||
const [containerDimensions, setContainerDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
const [imageDimensions, setImageDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
// Subscribe when sessionId changes
|
||||
useEffect(() => {
|
||||
|
|
@ -100,7 +106,10 @@ export function RemoteCameraReceiver({
|
|||
// Track image dimensions when it loads
|
||||
const handleImageLoad = useCallback((e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const img = e.currentTarget
|
||||
setImageDimensions({ width: img.naturalWidth, height: img.naturalHeight })
|
||||
setImageDimensions({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Create image src from base64 data
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export function VisionSetupModal() {
|
|||
setVisionCamera,
|
||||
setVisionCalibration,
|
||||
setVisionRemoteSession,
|
||||
setVisionCameraSource,
|
||||
dock,
|
||||
} = useMyAbacus()
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ export function VisionSetupModal() {
|
|||
setVisionCamera(null)
|
||||
setVisionCalibration(null)
|
||||
setVisionRemoteSession(null)
|
||||
setVisionCameraSource(null)
|
||||
setVisionEnabled(false)
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +77,14 @@ export function VisionSetupModal() {
|
|||
if (config.remoteCameraSessionId !== undefined) {
|
||||
setVisionRemoteSession(config.remoteCameraSessionId)
|
||||
}
|
||||
if (config.activeCameraSource !== undefined) {
|
||||
setVisionCameraSource(config.activeCameraSource)
|
||||
}
|
||||
}}
|
||||
// Start with phone camera selected if remote session is configured but no local camera
|
||||
// Use saved activeCameraSource if available, otherwise infer from configs
|
||||
initialCameraSource={
|
||||
visionConfig.remoteCameraSessionId && !visionConfig.cameraDeviceId ? 'phone' : 'local'
|
||||
visionConfig.activeCameraSource ??
|
||||
(visionConfig.remoteCameraSessionId && !visionConfig.cameraDeviceId ? 'phone' : 'local')
|
||||
}
|
||||
// Show enable/disable and clear buttons
|
||||
showVisionControls={true}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ import {
|
|||
} from 'react'
|
||||
import type { CalibrationGrid } from '@/types/vision'
|
||||
|
||||
/**
|
||||
* Camera source type for vision
|
||||
*/
|
||||
export type CameraSourceType = 'local' | 'phone'
|
||||
|
||||
/**
|
||||
* Configuration for abacus vision (camera-based input)
|
||||
*/
|
||||
|
|
@ -24,6 +29,8 @@ export interface VisionConfig {
|
|||
calibration: CalibrationGrid | null
|
||||
/** Remote phone camera session ID (for phone-as-camera mode) */
|
||||
remoteCameraSessionId: string | null
|
||||
/** Currently active camera source - tracks which camera is in use */
|
||||
activeCameraSource: CameraSourceType | null
|
||||
}
|
||||
|
||||
const DEFAULT_VISION_CONFIG: VisionConfig = {
|
||||
|
|
@ -31,6 +38,7 @@ const DEFAULT_VISION_CONFIG: VisionConfig = {
|
|||
cameraDeviceId: null,
|
||||
calibration: null,
|
||||
remoteCameraSessionId: null,
|
||||
activeCameraSource: null,
|
||||
}
|
||||
|
||||
const VISION_CONFIG_STORAGE_KEY = 'abacus-vision-config'
|
||||
|
|
@ -196,6 +204,8 @@ interface MyAbacusContextValue {
|
|||
setVisionCalibration: (calibration: CalibrationGrid | null) => void
|
||||
/** Set the remote camera session ID */
|
||||
setVisionRemoteSession: (sessionId: string | null) => void
|
||||
/** Set the active camera source */
|
||||
setVisionCameraSource: (source: CameraSourceType | null) => void
|
||||
/** Whether the vision setup modal is open */
|
||||
isVisionSetupOpen: boolean
|
||||
/** Open the vision setup modal */
|
||||
|
|
@ -310,12 +320,13 @@ export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [])
|
||||
|
||||
// Vision callbacks
|
||||
// Setup is complete if either:
|
||||
// - Local camera: has camera device AND calibration
|
||||
// Setup is complete if an active camera source is set and configured:
|
||||
// - Local camera: has camera device (calibration is optional - auto-crop works without it)
|
||||
// - Remote camera: has remote session ID (phone handles calibration)
|
||||
const isVisionSetupComplete =
|
||||
(visionConfig.cameraDeviceId !== null && visionConfig.calibration !== null) ||
|
||||
visionConfig.remoteCameraSessionId !== null
|
||||
visionConfig.activeCameraSource !== null &&
|
||||
((visionConfig.activeCameraSource === 'local' && visionConfig.cameraDeviceId !== null) ||
|
||||
(visionConfig.activeCameraSource === 'phone' && visionConfig.remoteCameraSessionId !== null))
|
||||
|
||||
const setVisionEnabled = useCallback((enabled: boolean) => {
|
||||
setVisionConfig((prev) => {
|
||||
|
|
@ -349,6 +360,14 @@ export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const setVisionCameraSource = useCallback((source: CameraSourceType | null) => {
|
||||
setVisionConfig((prev) => {
|
||||
const updated = { ...prev, activeCameraSource: source }
|
||||
saveVisionConfig(updated)
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
const openVisionSetup = useCallback(() => {
|
||||
setIsVisionSetupOpen(true)
|
||||
}, [])
|
||||
|
|
@ -407,6 +426,7 @@ export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
|
|||
setVisionCamera,
|
||||
setVisionCalibration,
|
||||
setVisionRemoteSession,
|
||||
setVisionCameraSource,
|
||||
isVisionSetupOpen,
|
||||
openVisionSetup,
|
||||
closeVisionSetup,
|
||||
|
|
|
|||
|
|
@ -167,10 +167,7 @@ describe('useRemoteCameraDesktop', () => {
|
|||
})
|
||||
|
||||
// Should not emit subscribe
|
||||
expect(mockSocket.emit).not.toHaveBeenCalledWith(
|
||||
'remote-camera:subscribe',
|
||||
expect.anything()
|
||||
)
|
||||
expect(mockSocket.emit).not.toHaveBeenCalledWith('remote-camera:subscribe', expect.anything())
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -273,7 +273,9 @@ export function useRemoteCameraDesktop(): UseRemoteCameraDesktopReturn {
|
|||
const unsubscribe = useCallback(() => {
|
||||
if (!socket || !currentSessionIdRef.current) return
|
||||
|
||||
socket.emit('remote-camera:leave', { sessionId: currentSessionIdRef.current })
|
||||
socket.emit('remote-camera:leave', {
|
||||
sessionId: currentSessionIdRef.current,
|
||||
})
|
||||
currentSessionIdRef.current = null
|
||||
setCurrentSessionId(null)
|
||||
// Don't clear persisted session - unsubscribe is for temporary disconnect
|
||||
|
|
@ -293,7 +295,9 @@ export function useRemoteCameraDesktop(): UseRemoteCameraDesktopReturn {
|
|||
*/
|
||||
const clearSession = useCallback(() => {
|
||||
if (socket && currentSessionIdRef.current) {
|
||||
socket.emit('remote-camera:leave', { sessionId: currentSessionIdRef.current })
|
||||
socket.emit('remote-camera:leave', {
|
||||
sessionId: currentSessionIdRef.current,
|
||||
})
|
||||
}
|
||||
currentSessionIdRef.current = null
|
||||
setCurrentSessionId(null)
|
||||
|
|
|
|||
|
|
@ -137,7 +137,10 @@ export function useRemoteCameraPhone(
|
|||
// Auto-reconnect to session if we have one
|
||||
const sessionId = sessionIdRef.current
|
||||
if (sessionId) {
|
||||
console.log('[RemoteCameraPhone] Auto-reconnecting to session after socket reconnect:', sessionId)
|
||||
console.log(
|
||||
'[RemoteCameraPhone] Auto-reconnecting to session after socket reconnect:',
|
||||
sessionId
|
||||
)
|
||||
socketInstance.emit('remote-camera:join', { sessionId })
|
||||
setIsConnected(true)
|
||||
isConnectedRef.current = true
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@ export interface RemoteCameraSession {
|
|||
phoneConnected: boolean
|
||||
/** Calibration data sent from desktop (persists for reconnects) */
|
||||
calibration?: {
|
||||
corners: { topLeft: { x: number; y: number }; topRight: { x: number; y: number }; bottomLeft: { x: number; y: number }; bottomRight: { x: number; y: number } }
|
||||
corners: {
|
||||
topLeft: { x: number; y: number }
|
||||
topRight: { x: number; y: number }
|
||||
bottomLeft: { x: number; y: number }
|
||||
bottomRight: { x: number; y: number }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +131,9 @@ export function setSessionCalibration(
|
|||
/**
|
||||
* Get calibration data from session
|
||||
*/
|
||||
export function getSessionCalibration(sessionId: string): RemoteCameraSession['calibration'] | null {
|
||||
export function getSessionCalibration(
|
||||
sessionId: string
|
||||
): RemoteCameraSession['calibration'] | null {
|
||||
const sessions = getSessions()
|
||||
const session = sessions.get(sessionId)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,102 +1,98 @@
|
|||
# [2.19.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.18.0...abacus-react-v2.19.0) (2026-01-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **vision:** add physical abacus column setting and fix remote flash toggle ([b206eb3](https://github.com/antialias/soroban-abacus-flashcards/commit/b206eb30712e4b98525a9fa2544c2b5a235a8b72))
|
||||
* **vision:** improve remote camera calibration and UX ([8846cec](https://github.com/antialias/soroban-abacus-flashcards/commit/8846cece93941a36c187abd4ecee9cc88de0c2ec))
|
||||
- **vision:** add physical abacus column setting and fix remote flash toggle ([b206eb3](https://github.com/antialias/soroban-abacus-flashcards/commit/b206eb30712e4b98525a9fa2544c2b5a235a8b72))
|
||||
- **vision:** improve remote camera calibration and UX ([8846cec](https://github.com/antialias/soroban-abacus-flashcards/commit/8846cece93941a36c187abd4ecee9cc88de0c2ec))
|
||||
|
||||
# [2.18.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.17.0...abacus-react-v2.18.0) (2026-01-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow teacher-parents to enroll their children in other classrooms ([52df7f4](https://github.com/antialias/soroban-abacus-flashcards/commit/52df7f469718128fd3d8933941ffb8d4bb8db208))
|
||||
* **bkt:** handle missing helpLevelUsed in legacy data causing NaN ([b300ed9](https://github.com/antialias/soroban-abacus-flashcards/commit/b300ed9f5cc3bfb0c7b28faafe81c80a59444998))
|
||||
* **camera:** handle race condition in camera initialization ([2a24700](https://github.com/antialias/soroban-abacus-flashcards/commit/2a24700e6cb6efe0ae35d9ebd6c428e3a1a1a736))
|
||||
* **classroom:** auto-transition tutorial→session observation + fix NaN display ([962a52d](https://github.com/antialias/soroban-abacus-flashcards/commit/962a52d7562f566e78f6272816b049bf77daa7c9))
|
||||
* **classroom:** broadcast digit-by-digit answer and correct phase indicator ([fb73e85](https://github.com/antialias/soroban-abacus-flashcards/commit/fb73e85f2daacefafa572e03c16b10fab619ea57))
|
||||
* **dashboard:** compute skill stats from session results in curriculum API ([11d4846](https://github.com/antialias/soroban-abacus-flashcards/commit/11d48465d710d0293ebf41f64b4fd0f1f03d8bf8))
|
||||
* **db:** add missing is_paused column to session_plans ([9d8b5e1](https://github.com/antialias/soroban-abacus-flashcards/commit/9d8b5e1148911f881d08d07608debaaef91609c2))
|
||||
* **db:** add missing journal entries for migrations 0041-0042 ([398603c](https://github.com/antialias/soroban-abacus-flashcards/commit/398603c75a094e28122c5ccdced5b82badc7fbfb))
|
||||
* **docker:** add canvas native deps for jsdom/vitest ([5f51bc1](https://github.com/antialias/soroban-abacus-flashcards/commit/5f51bc1871aec325feb32a0b29edabb3b6c5dd1f))
|
||||
* **docker:** override canvas with mock package for Alpine/musl ([8be1995](https://github.com/antialias/soroban-abacus-flashcards/commit/8be19958af624d22fa2c6cb48f5723f5efc820c3))
|
||||
* **docker:** skip canvas native build (optional jsdom dep) ([d717f44](https://github.com/antialias/soroban-abacus-flashcards/commit/d717f44fccb8ed2baa30499df65784a4b89c6ffc))
|
||||
* **observer:** seed results panel with full session history ([aab7469](https://github.com/antialias/soroban-abacus-flashcards/commit/aab7469d9ea87c91a0165e4c48a60ac130cdc1b2))
|
||||
* only show session stats when there are actual problems ([62aefad](https://github.com/antialias/soroban-abacus-flashcards/commit/62aefad6766ba32ad27e8ed3db621a6f77520cbe))
|
||||
* **practice:** allow teachers to create student profiles ([5fee129](https://github.com/antialias/soroban-abacus-flashcards/commit/5fee1297e1775b5e6133919d179e23b6e70b2518))
|
||||
* **practice:** always show add student FAB button ([a658414](https://github.com/antialias/soroban-abacus-flashcards/commit/a6584143ebf1f3e5b3c9f3283e690458a06beb60))
|
||||
* **practice:** real-time progress in observer modal + numeric answer comparison ([c0e63ff](https://github.com/antialias/soroban-abacus-flashcards/commit/c0e63ff68b26fd37eedd657504f7f79e5ce40a10))
|
||||
* **practice:** show active sessions for teacher's own children ([ece3197](https://github.com/antialias/soroban-abacus-flashcards/commit/ece319738b6ab1882469d79ea24b604316d28b34))
|
||||
* **practice:** use Next.js Link for student tiles + fix session observer z-index ([6def610](https://github.com/antialias/soroban-abacus-flashcards/commit/6def6108771b427e4885bebd23cecdad7a50efb0))
|
||||
* **seed:** accurate BKT simulation for developing classifications ([d5e4c85](https://github.com/antialias/soroban-abacus-flashcards/commit/d5e4c858db8866e5177b8fa2317aba42b30171e8))
|
||||
* **share:** use getShareUrl for correct production URLs ([98a69f1](https://github.com/antialias/soroban-abacus-flashcards/commit/98a69f1f80e465415edce49043e2c019a856f8e5))
|
||||
* **vision:** fix manual calibration overlay not showing on remote camera ([44dcb01](https://github.com/antialias/soroban-abacus-flashcards/commit/44dcb01473bac00c09dddbbefd77dd26b3a27817))
|
||||
* **vision:** fix remote camera calibration coordinate system ([e52f94e](https://github.com/antialias/soroban-abacus-flashcards/commit/e52f94e4b476658c41f23668d2941af1288e4ed8))
|
||||
* **vision:** swap corners diagonally for webcam orientation ([dd8efe3](https://github.com/antialias/soroban-abacus-flashcards/commit/dd8efe379d4bbcfc4b60f7c00ad6180465b7e7b6))
|
||||
|
||||
- allow teacher-parents to enroll their children in other classrooms ([52df7f4](https://github.com/antialias/soroban-abacus-flashcards/commit/52df7f469718128fd3d8933941ffb8d4bb8db208))
|
||||
- **bkt:** handle missing helpLevelUsed in legacy data causing NaN ([b300ed9](https://github.com/antialias/soroban-abacus-flashcards/commit/b300ed9f5cc3bfb0c7b28faafe81c80a59444998))
|
||||
- **camera:** handle race condition in camera initialization ([2a24700](https://github.com/antialias/soroban-abacus-flashcards/commit/2a24700e6cb6efe0ae35d9ebd6c428e3a1a1a736))
|
||||
- **classroom:** auto-transition tutorial→session observation + fix NaN display ([962a52d](https://github.com/antialias/soroban-abacus-flashcards/commit/962a52d7562f566e78f6272816b049bf77daa7c9))
|
||||
- **classroom:** broadcast digit-by-digit answer and correct phase indicator ([fb73e85](https://github.com/antialias/soroban-abacus-flashcards/commit/fb73e85f2daacefafa572e03c16b10fab619ea57))
|
||||
- **dashboard:** compute skill stats from session results in curriculum API ([11d4846](https://github.com/antialias/soroban-abacus-flashcards/commit/11d48465d710d0293ebf41f64b4fd0f1f03d8bf8))
|
||||
- **db:** add missing is_paused column to session_plans ([9d8b5e1](https://github.com/antialias/soroban-abacus-flashcards/commit/9d8b5e1148911f881d08d07608debaaef91609c2))
|
||||
- **db:** add missing journal entries for migrations 0041-0042 ([398603c](https://github.com/antialias/soroban-abacus-flashcards/commit/398603c75a094e28122c5ccdced5b82badc7fbfb))
|
||||
- **docker:** add canvas native deps for jsdom/vitest ([5f51bc1](https://github.com/antialias/soroban-abacus-flashcards/commit/5f51bc1871aec325feb32a0b29edabb3b6c5dd1f))
|
||||
- **docker:** override canvas with mock package for Alpine/musl ([8be1995](https://github.com/antialias/soroban-abacus-flashcards/commit/8be19958af624d22fa2c6cb48f5723f5efc820c3))
|
||||
- **docker:** skip canvas native build (optional jsdom dep) ([d717f44](https://github.com/antialias/soroban-abacus-flashcards/commit/d717f44fccb8ed2baa30499df65784a4b89c6ffc))
|
||||
- **observer:** seed results panel with full session history ([aab7469](https://github.com/antialias/soroban-abacus-flashcards/commit/aab7469d9ea87c91a0165e4c48a60ac130cdc1b2))
|
||||
- only show session stats when there are actual problems ([62aefad](https://github.com/antialias/soroban-abacus-flashcards/commit/62aefad6766ba32ad27e8ed3db621a6f77520cbe))
|
||||
- **practice:** allow teachers to create student profiles ([5fee129](https://github.com/antialias/soroban-abacus-flashcards/commit/5fee1297e1775b5e6133919d179e23b6e70b2518))
|
||||
- **practice:** always show add student FAB button ([a658414](https://github.com/antialias/soroban-abacus-flashcards/commit/a6584143ebf1f3e5b3c9f3283e690458a06beb60))
|
||||
- **practice:** real-time progress in observer modal + numeric answer comparison ([c0e63ff](https://github.com/antialias/soroban-abacus-flashcards/commit/c0e63ff68b26fd37eedd657504f7f79e5ce40a10))
|
||||
- **practice:** show active sessions for teacher's own children ([ece3197](https://github.com/antialias/soroban-abacus-flashcards/commit/ece319738b6ab1882469d79ea24b604316d28b34))
|
||||
- **practice:** use Next.js Link for student tiles + fix session observer z-index ([6def610](https://github.com/antialias/soroban-abacus-flashcards/commit/6def6108771b427e4885bebd23cecdad7a50efb0))
|
||||
- **seed:** accurate BKT simulation for developing classifications ([d5e4c85](https://github.com/antialias/soroban-abacus-flashcards/commit/d5e4c858db8866e5177b8fa2317aba42b30171e8))
|
||||
- **share:** use getShareUrl for correct production URLs ([98a69f1](https://github.com/antialias/soroban-abacus-flashcards/commit/98a69f1f80e465415edce49043e2c019a856f8e5))
|
||||
- **vision:** fix manual calibration overlay not showing on remote camera ([44dcb01](https://github.com/antialias/soroban-abacus-flashcards/commit/44dcb01473bac00c09dddbbefd77dd26b3a27817))
|
||||
- **vision:** fix remote camera calibration coordinate system ([e52f94e](https://github.com/antialias/soroban-abacus-flashcards/commit/e52f94e4b476658c41f23668d2941af1288e4ed8))
|
||||
- **vision:** swap corners diagonally for webcam orientation ([dd8efe3](https://github.com/antialias/soroban-abacus-flashcards/commit/dd8efe379d4bbcfc4b60f7c00ad6180465b7e7b6))
|
||||
|
||||
### Features
|
||||
|
||||
* API authorization audit + teacher enrollment UI + share codes ([d6e369f](https://github.com/antialias/soroban-abacus-flashcards/commit/d6e369f9dc9b963938ca8de4562c87f9f1b6d389))
|
||||
* **camera:** auto-start camera when opening camera modal ([f3bb0ae](https://github.com/antialias/soroban-abacus-flashcards/commit/f3bb0aee4fe23eeffc7b7099981f51ec54636a35))
|
||||
* **camera:** fullscreen modal with edge-to-edge preview ([db17c96](https://github.com/antialias/soroban-abacus-flashcards/commit/db17c96168078f2d0d723b24395096756a2f63ec))
|
||||
* **chart:** add grouped structure to chart hover tooltip ([594e22c](https://github.com/antialias/soroban-abacus-flashcards/commit/594e22c428e0a4ee4322c233f127f9250e88b5fa))
|
||||
* **chart:** improve skill classification visual hierarchy with colors and patterns ([c9518a6](https://github.com/antialias/soroban-abacus-flashcards/commit/c9518a6b9952bda60ab2663d7655092637139fec))
|
||||
* **classroom:** add active sessions API endpoint ([07f6bb7](https://github.com/antialias/soroban-abacus-flashcards/commit/07f6bb7f9cc2dfbe6da8d16361e89b698405e1c0))
|
||||
* **classroom:** add real-time enrollment/unenrollment reactivity ([a0693e9](https://github.com/antialias/soroban-abacus-flashcards/commit/a0693e90840f651094f852a6a6f523013786b322))
|
||||
* **classroom:** add session broadcast and active session indicators ([9636f7f](https://github.com/antialias/soroban-abacus-flashcards/commit/9636f7f44a71da022352c19e80f9ec147dd3af5f))
|
||||
* **classroom:** add unified add-student modal with two-column layout ([dca696a](https://github.com/antialias/soroban-abacus-flashcards/commit/dca696a29fc20a2697b491c0d2efbe036569a716))
|
||||
* **classroom:** add unified TeacherClassroomCard with auto-enrollment ([4d6adf3](https://github.com/antialias/soroban-abacus-flashcards/commit/4d6adf359ede5d17c2decd9275ba68635ee0bd4f))
|
||||
* **classroom:** complete reactivity fixes (Steps 7-11) ([2015494](https://github.com/antialias/soroban-abacus-flashcards/commit/2015494c0eca28457031aa39490d70a2af3da4df))
|
||||
* **classroom:** consolidate filter pill to single-row design ([78a63e3](https://github.com/antialias/soroban-abacus-flashcards/commit/78a63e35e39948729cbf41e6c5af4e688a506c8d))
|
||||
* **classroom:** implement enrollment system (Phase 4) ([1952a41](https://github.com/antialias/soroban-abacus-flashcards/commit/1952a412edcd04b332655199737c340a4389d174))
|
||||
* **classroom:** implement entry prompts system ([de39ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/de39ab52cc60f5782fc291246f98013ae15142ca))
|
||||
* **classroom:** implement real-time enrollment updates ([bbe0500](https://github.com/antialias/soroban-abacus-flashcards/commit/bbe0500fe9000d0d016417c1b586e9569e3eb888))
|
||||
* **classroom:** implement real-time presence with WebSocket (Phase 6) ([629bfcf](https://github.com/antialias/soroban-abacus-flashcards/commit/629bfcfc03c611cd3928bb98a67bace485ee3a7b))
|
||||
* **classroom:** implement real-time session observation (Step 3) ([2feb684](https://github.com/antialias/soroban-abacus-flashcards/commit/2feb6844a4fce48ba7a87d2a77769783c4e8b2f9))
|
||||
* **classroom:** implement real-time skill tutorial observation ([4b73879](https://github.com/antialias/soroban-abacus-flashcards/commit/4b7387905d2b050327f9b67b834d4e9dfc0b19cb))
|
||||
* **classroom:** implement teacher classroom dashboard (Phase 3) ([2202716](https://github.com/antialias/soroban-abacus-flashcards/commit/2202716f563053624dbe5c6abb969a3b0d452fd1))
|
||||
* **classroom:** implement teacher-initiated pause and fix manual pause ([ccea0f8](https://github.com/antialias/soroban-abacus-flashcards/commit/ccea0f86ac213b32cac7363f28e193b1976bd553))
|
||||
* **classroom:** implement two-way abacus sync for session observation (Step 5) ([2f7002e](https://github.com/antialias/soroban-abacus-flashcards/commit/2f7002e5759db705e213eb9f8474589c8e6149e7))
|
||||
* **classroom:** improve enrollment reactivity and UX ([77336be](https://github.com/antialias/soroban-abacus-flashcards/commit/77336bea5b5bbf16b393da13588de6e5082e818f))
|
||||
* **classroom:** integrate create student form into unified add-student modal ([da92289](https://github.com/antialias/soroban-abacus-flashcards/commit/da92289ed1ae570ff48cc28818122d4640d6c84c))
|
||||
* **classroom:** integrate Enter Classroom into StudentActionMenu ([2f1b9df](https://github.com/antialias/soroban-abacus-flashcards/commit/2f1b9df9d9d605b0c120af6961670ae84718c8d7))
|
||||
* **dashboard:** add skill progress chart with trend analysis and timing awareness ([1fc8949](https://github.com/antialias/soroban-abacus-flashcards/commit/1fc8949b0664591aa1b0cfcd7c7abd2a4c586281))
|
||||
* enable parents to observe children's practice sessions ([7b82995](https://github.com/antialias/soroban-abacus-flashcards/commit/7b829956644d369dfdfb0789a33e0b857958e84f))
|
||||
* **family:** implement parent-to-parent family code sharing (Phase 2) ([0284227](https://github.com/antialias/soroban-abacus-flashcards/commit/02842270c9278174934407a9620777589f79ee1e))
|
||||
* improve session summary header and add practice type badges ([518fe15](https://github.com/antialias/soroban-abacus-flashcards/commit/518fe153c9fc2ae2f2f7fc0ed4de27ee1c5c5646))
|
||||
* **observer:** add live active session item to history list ([91d6d6a](https://github.com/antialias/soroban-abacus-flashcards/commit/91d6d6a1b6938b559d8488fe296d562695cf16d1))
|
||||
* **observer:** add live results panel and session progress indicator ([8527f89](https://github.com/antialias/soroban-abacus-flashcards/commit/8527f892e2b300d51d83056d779474592a2fd955))
|
||||
* **observer:** implement shareable session observation links ([3ac7b46](https://github.com/antialias/soroban-abacus-flashcards/commit/3ac7b460ec0dc207a5691fbed8d539b484374fe7))
|
||||
* **practice:** add auto-rotation for captured documents ([ff79a28](https://github.com/antialias/soroban-abacus-flashcards/commit/ff79a28c657fb0a19752990e23f9bb0ced4e9343))
|
||||
* **practice:** add document adjustment UI and auto-capture ([473b7db](https://github.com/antialias/soroban-abacus-flashcards/commit/473b7dbd7cd15be511351a1fd303a0fc32b9d941))
|
||||
* **practice:** add document scanning with multi-quad tracking ([5f4f1fd](https://github.com/antialias/soroban-abacus-flashcards/commit/5f4f1fde3372e5d65d3f399216b04ab0e4c9972e))
|
||||
* **practice:** add fixed filter bar, sticky headers, and shared EmojiPicker ([0e03561](https://github.com/antialias/soroban-abacus-flashcards/commit/0e0356113ddef1ec92cd0b3fda0852d99c6067d2))
|
||||
* **practice:** add intervention system and improve skill chart hierarchy ([bf5b99a](https://github.com/antialias/soroban-abacus-flashcards/commit/bf5b99afe967c0b17765a7e6f1911d03201eed95))
|
||||
* **practice:** add mini start practice banner to QuickLook modal ([d1176da](https://github.com/antialias/soroban-abacus-flashcards/commit/d1176da9aa8bd926ca96699d1091e65f4a34d782))
|
||||
* **practice:** add Needs Attention to unified compact layout ([8727782](https://github.com/antialias/soroban-abacus-flashcards/commit/8727782e45c7ac269c4dbcc223b2a8be57be8bb2))
|
||||
* **practice:** add photo attachments for practice sessions ([9b85311](https://github.com/antialias/soroban-abacus-flashcards/commit/9b853116ecfbb19bec39923da635374963cf002c))
|
||||
* **practice:** add photo editing with rotation persistence and auto-detect ([156a0df](https://github.com/antialias/soroban-abacus-flashcards/commit/156a0dfe967a48c211be527da27c92ef8b1ab20c))
|
||||
* **practice:** add smooth fullscreen transition from QuickLook to dashboard ([cb8b0df](https://github.com/antialias/soroban-abacus-flashcards/commit/cb8b0dff676d48bcba4775c5981ac357d573ab27))
|
||||
* **practice:** add student organization with filtering and archiving ([538718a](https://github.com/antialias/soroban-abacus-flashcards/commit/538718a814402bd9c83b3c354c5a3386ff69104d))
|
||||
* **practice:** add StudentActionMenu to dashboard + fix z-index layering ([bf262e7](https://github.com/antialias/soroban-abacus-flashcards/commit/bf262e7d5305e2358d3a2464db10bc3b0866104c))
|
||||
* **practice:** compact single-student categories and UI improvements ([0e7f326](https://github.com/antialias/soroban-abacus-flashcards/commit/0e7f3265fe2de3b693c47a8a556d3e7cbc726ef4))
|
||||
* **practice:** implement measurement-based compact layout ([1656b93](https://github.com/antialias/soroban-abacus-flashcards/commit/1656b9324f6fb24a318820e04559c480c99762f5))
|
||||
* **practice:** implement retry wrong problems system ([474c4da](https://github.com/antialias/soroban-abacus-flashcards/commit/474c4da05a8d761e63a32187f5c301b57fb6aae4))
|
||||
* **practice:** parent session observation + relationship UI + error boundaries ([07484fd](https://github.com/antialias/soroban-abacus-flashcards/commit/07484fdfac3c6613a6a7709bdee25e1f8e047227))
|
||||
* **practice:** polish unified student list with keyboard nav and mobile UX ([0ba1551](https://github.com/antialias/soroban-abacus-flashcards/commit/0ba1551feaa30d8f41ec5d771c00561396b043f3))
|
||||
* **seed:** add category field to all mock student profiles ([f883fbf](https://github.com/antialias/soroban-abacus-flashcards/commit/f883fbfe233b7fb3d366062e7c156e3fc8e0e3a7))
|
||||
* **session-summary:** redesign ProblemToReview with BKT integration and animations ([430c46a](https://github.com/antialias/soroban-abacus-flashcards/commit/430c46adb929a6c0ce7c67da4b1df7d3e2846cfd))
|
||||
* **storybook:** add TeacherClassroomCard stories ([a5e5788](https://github.com/antialias/soroban-abacus-flashcards/commit/a5e5788fa96f57e0d918620e357f7920ef792b19))
|
||||
* **vision:** add AbacusVisionBridge for physical soroban detection ([47088e4](https://github.com/antialias/soroban-abacus-flashcards/commit/47088e4850c25e76fe49879587227b46f699ba91))
|
||||
* **vision:** add ArUco marker auto-calibration for abacus detection ([9e9a06f](https://github.com/antialias/soroban-abacus-flashcards/commit/9e9a06f2e4dc37d208ac19259be9b9830c7ad949))
|
||||
* **vision:** add remote phone camera support for abacus detection ([8e4975d](https://github.com/antialias/soroban-abacus-flashcards/commit/8e4975d395c4b10bc40ae2c71473fdb1a50c114c))
|
||||
|
||||
- API authorization audit + teacher enrollment UI + share codes ([d6e369f](https://github.com/antialias/soroban-abacus-flashcards/commit/d6e369f9dc9b963938ca8de4562c87f9f1b6d389))
|
||||
- **camera:** auto-start camera when opening camera modal ([f3bb0ae](https://github.com/antialias/soroban-abacus-flashcards/commit/f3bb0aee4fe23eeffc7b7099981f51ec54636a35))
|
||||
- **camera:** fullscreen modal with edge-to-edge preview ([db17c96](https://github.com/antialias/soroban-abacus-flashcards/commit/db17c96168078f2d0d723b24395096756a2f63ec))
|
||||
- **chart:** add grouped structure to chart hover tooltip ([594e22c](https://github.com/antialias/soroban-abacus-flashcards/commit/594e22c428e0a4ee4322c233f127f9250e88b5fa))
|
||||
- **chart:** improve skill classification visual hierarchy with colors and patterns ([c9518a6](https://github.com/antialias/soroban-abacus-flashcards/commit/c9518a6b9952bda60ab2663d7655092637139fec))
|
||||
- **classroom:** add active sessions API endpoint ([07f6bb7](https://github.com/antialias/soroban-abacus-flashcards/commit/07f6bb7f9cc2dfbe6da8d16361e89b698405e1c0))
|
||||
- **classroom:** add real-time enrollment/unenrollment reactivity ([a0693e9](https://github.com/antialias/soroban-abacus-flashcards/commit/a0693e90840f651094f852a6a6f523013786b322))
|
||||
- **classroom:** add session broadcast and active session indicators ([9636f7f](https://github.com/antialias/soroban-abacus-flashcards/commit/9636f7f44a71da022352c19e80f9ec147dd3af5f))
|
||||
- **classroom:** add unified add-student modal with two-column layout ([dca696a](https://github.com/antialias/soroban-abacus-flashcards/commit/dca696a29fc20a2697b491c0d2efbe036569a716))
|
||||
- **classroom:** add unified TeacherClassroomCard with auto-enrollment ([4d6adf3](https://github.com/antialias/soroban-abacus-flashcards/commit/4d6adf359ede5d17c2decd9275ba68635ee0bd4f))
|
||||
- **classroom:** complete reactivity fixes (Steps 7-11) ([2015494](https://github.com/antialias/soroban-abacus-flashcards/commit/2015494c0eca28457031aa39490d70a2af3da4df))
|
||||
- **classroom:** consolidate filter pill to single-row design ([78a63e3](https://github.com/antialias/soroban-abacus-flashcards/commit/78a63e35e39948729cbf41e6c5af4e688a506c8d))
|
||||
- **classroom:** implement enrollment system (Phase 4) ([1952a41](https://github.com/antialias/soroban-abacus-flashcards/commit/1952a412edcd04b332655199737c340a4389d174))
|
||||
- **classroom:** implement entry prompts system ([de39ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/de39ab52cc60f5782fc291246f98013ae15142ca))
|
||||
- **classroom:** implement real-time enrollment updates ([bbe0500](https://github.com/antialias/soroban-abacus-flashcards/commit/bbe0500fe9000d0d016417c1b586e9569e3eb888))
|
||||
- **classroom:** implement real-time presence with WebSocket (Phase 6) ([629bfcf](https://github.com/antialias/soroban-abacus-flashcards/commit/629bfcfc03c611cd3928bb98a67bace485ee3a7b))
|
||||
- **classroom:** implement real-time session observation (Step 3) ([2feb684](https://github.com/antialias/soroban-abacus-flashcards/commit/2feb6844a4fce48ba7a87d2a77769783c4e8b2f9))
|
||||
- **classroom:** implement real-time skill tutorial observation ([4b73879](https://github.com/antialias/soroban-abacus-flashcards/commit/4b7387905d2b050327f9b67b834d4e9dfc0b19cb))
|
||||
- **classroom:** implement teacher classroom dashboard (Phase 3) ([2202716](https://github.com/antialias/soroban-abacus-flashcards/commit/2202716f563053624dbe5c6abb969a3b0d452fd1))
|
||||
- **classroom:** implement teacher-initiated pause and fix manual pause ([ccea0f8](https://github.com/antialias/soroban-abacus-flashcards/commit/ccea0f86ac213b32cac7363f28e193b1976bd553))
|
||||
- **classroom:** implement two-way abacus sync for session observation (Step 5) ([2f7002e](https://github.com/antialias/soroban-abacus-flashcards/commit/2f7002e5759db705e213eb9f8474589c8e6149e7))
|
||||
- **classroom:** improve enrollment reactivity and UX ([77336be](https://github.com/antialias/soroban-abacus-flashcards/commit/77336bea5b5bbf16b393da13588de6e5082e818f))
|
||||
- **classroom:** integrate create student form into unified add-student modal ([da92289](https://github.com/antialias/soroban-abacus-flashcards/commit/da92289ed1ae570ff48cc28818122d4640d6c84c))
|
||||
- **classroom:** integrate Enter Classroom into StudentActionMenu ([2f1b9df](https://github.com/antialias/soroban-abacus-flashcards/commit/2f1b9df9d9d605b0c120af6961670ae84718c8d7))
|
||||
- **dashboard:** add skill progress chart with trend analysis and timing awareness ([1fc8949](https://github.com/antialias/soroban-abacus-flashcards/commit/1fc8949b0664591aa1b0cfcd7c7abd2a4c586281))
|
||||
- enable parents to observe children's practice sessions ([7b82995](https://github.com/antialias/soroban-abacus-flashcards/commit/7b829956644d369dfdfb0789a33e0b857958e84f))
|
||||
- **family:** implement parent-to-parent family code sharing (Phase 2) ([0284227](https://github.com/antialias/soroban-abacus-flashcards/commit/02842270c9278174934407a9620777589f79ee1e))
|
||||
- improve session summary header and add practice type badges ([518fe15](https://github.com/antialias/soroban-abacus-flashcards/commit/518fe153c9fc2ae2f2f7fc0ed4de27ee1c5c5646))
|
||||
- **observer:** add live active session item to history list ([91d6d6a](https://github.com/antialias/soroban-abacus-flashcards/commit/91d6d6a1b6938b559d8488fe296d562695cf16d1))
|
||||
- **observer:** add live results panel and session progress indicator ([8527f89](https://github.com/antialias/soroban-abacus-flashcards/commit/8527f892e2b300d51d83056d779474592a2fd955))
|
||||
- **observer:** implement shareable session observation links ([3ac7b46](https://github.com/antialias/soroban-abacus-flashcards/commit/3ac7b460ec0dc207a5691fbed8d539b484374fe7))
|
||||
- **practice:** add auto-rotation for captured documents ([ff79a28](https://github.com/antialias/soroban-abacus-flashcards/commit/ff79a28c657fb0a19752990e23f9bb0ced4e9343))
|
||||
- **practice:** add document adjustment UI and auto-capture ([473b7db](https://github.com/antialias/soroban-abacus-flashcards/commit/473b7dbd7cd15be511351a1fd303a0fc32b9d941))
|
||||
- **practice:** add document scanning with multi-quad tracking ([5f4f1fd](https://github.com/antialias/soroban-abacus-flashcards/commit/5f4f1fde3372e5d65d3f399216b04ab0e4c9972e))
|
||||
- **practice:** add fixed filter bar, sticky headers, and shared EmojiPicker ([0e03561](https://github.com/antialias/soroban-abacus-flashcards/commit/0e0356113ddef1ec92cd0b3fda0852d99c6067d2))
|
||||
- **practice:** add intervention system and improve skill chart hierarchy ([bf5b99a](https://github.com/antialias/soroban-abacus-flashcards/commit/bf5b99afe967c0b17765a7e6f1911d03201eed95))
|
||||
- **practice:** add mini start practice banner to QuickLook modal ([d1176da](https://github.com/antialias/soroban-abacus-flashcards/commit/d1176da9aa8bd926ca96699d1091e65f4a34d782))
|
||||
- **practice:** add Needs Attention to unified compact layout ([8727782](https://github.com/antialias/soroban-abacus-flashcards/commit/8727782e45c7ac269c4dbcc223b2a8be57be8bb2))
|
||||
- **practice:** add photo attachments for practice sessions ([9b85311](https://github.com/antialias/soroban-abacus-flashcards/commit/9b853116ecfbb19bec39923da635374963cf002c))
|
||||
- **practice:** add photo editing with rotation persistence and auto-detect ([156a0df](https://github.com/antialias/soroban-abacus-flashcards/commit/156a0dfe967a48c211be527da27c92ef8b1ab20c))
|
||||
- **practice:** add smooth fullscreen transition from QuickLook to dashboard ([cb8b0df](https://github.com/antialias/soroban-abacus-flashcards/commit/cb8b0dff676d48bcba4775c5981ac357d573ab27))
|
||||
- **practice:** add student organization with filtering and archiving ([538718a](https://github.com/antialias/soroban-abacus-flashcards/commit/538718a814402bd9c83b3c354c5a3386ff69104d))
|
||||
- **practice:** add StudentActionMenu to dashboard + fix z-index layering ([bf262e7](https://github.com/antialias/soroban-abacus-flashcards/commit/bf262e7d5305e2358d3a2464db10bc3b0866104c))
|
||||
- **practice:** compact single-student categories and UI improvements ([0e7f326](https://github.com/antialias/soroban-abacus-flashcards/commit/0e7f3265fe2de3b693c47a8a556d3e7cbc726ef4))
|
||||
- **practice:** implement measurement-based compact layout ([1656b93](https://github.com/antialias/soroban-abacus-flashcards/commit/1656b9324f6fb24a318820e04559c480c99762f5))
|
||||
- **practice:** implement retry wrong problems system ([474c4da](https://github.com/antialias/soroban-abacus-flashcards/commit/474c4da05a8d761e63a32187f5c301b57fb6aae4))
|
||||
- **practice:** parent session observation + relationship UI + error boundaries ([07484fd](https://github.com/antialias/soroban-abacus-flashcards/commit/07484fdfac3c6613a6a7709bdee25e1f8e047227))
|
||||
- **practice:** polish unified student list with keyboard nav and mobile UX ([0ba1551](https://github.com/antialias/soroban-abacus-flashcards/commit/0ba1551feaa30d8f41ec5d771c00561396b043f3))
|
||||
- **seed:** add category field to all mock student profiles ([f883fbf](https://github.com/antialias/soroban-abacus-flashcards/commit/f883fbfe233b7fb3d366062e7c156e3fc8e0e3a7))
|
||||
- **session-summary:** redesign ProblemToReview with BKT integration and animations ([430c46a](https://github.com/antialias/soroban-abacus-flashcards/commit/430c46adb929a6c0ce7c67da4b1df7d3e2846cfd))
|
||||
- **storybook:** add TeacherClassroomCard stories ([a5e5788](https://github.com/antialias/soroban-abacus-flashcards/commit/a5e5788fa96f57e0d918620e357f7920ef792b19))
|
||||
- **vision:** add AbacusVisionBridge for physical soroban detection ([47088e4](https://github.com/antialias/soroban-abacus-flashcards/commit/47088e4850c25e76fe49879587227b46f699ba91))
|
||||
- **vision:** add ArUco marker auto-calibration for abacus detection ([9e9a06f](https://github.com/antialias/soroban-abacus-flashcards/commit/9e9a06f2e4dc37d208ac19259be9b9830c7ad949))
|
||||
- **vision:** add remote phone camera support for abacus detection ([8e4975d](https://github.com/antialias/soroban-abacus-flashcards/commit/8e4975d395c4b10bc40ae2c71473fdb1a50c114c))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* reduce practice page dev bundle from 47MB to 115KB ([fd1df93](https://github.com/antialias/soroban-abacus-flashcards/commit/fd1df93a8fa320800275c135d5dd89390eb72c19))
|
||||
- reduce practice page dev bundle from 47MB to 115KB ([fd1df93](https://github.com/antialias/soroban-abacus-flashcards/commit/fd1df93a8fa320800275c135d5dd89390eb72c19))
|
||||
|
||||
# [2.17.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.16.0...abacus-react-v2.17.0) (2025-12-20)
|
||||
|
||||
|
|
|
|||
22745
pnpm-lock.yaml
22745
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue