// One service snapshot supplies this entire card. Favorites never leave settings. import { useEffect, useState } from 'react'; import { useSessionSelector } from '../../context/SessionContext.jsx'; import { isFeatureEnabled } from '../../lib/features.js'; import { useSocket } from '../../context/SocketContext.jsx'; import { useSettingsNamespace } from '../../settings/index.js'; import CardFrame from '../CardFrame/index.jsx'; import { fieldClass } from './constants.js'; const display = (value) => value == null ? 'Unavailable' : String(value); const bytes = (value) => value == null ? 'Unavailable' : `${(value / 1024 ** 2).toFixed(1)} MiB`; const time = (value) => value ? new Date(value).toLocaleString() : 'Unavailable'; const age = (now, value) => value ? `${Math.max(0, (now - value) / 1000).toFixed(1)} s` : 'Unavailable'; function Row({ label, children }) { return
{label}
{children}
; } function Group({ title, children }) { return

{title}

{children}
; } const actionNames = { motors: 'Brushes / vacuum', servo: 'Camera tilt', headlight: 'Headlight', laser: 'Laser', horn: 'Horn', peripheral: 'Accessory', song: 'Beeper', raw: 'Recorded control' }; function WheelMeter({ label, speed }) { const known = Number.isFinite(speed); const value = known ? Math.max(-500, Math.min(500, speed)) : 0; return
{label}{known ? `${value} mm/s` : 'Waiting'}
ReverseForward
; } export default function VipRoverLearningCard() { const enabled = useSessionSelector((state) => isFeatureEnabled(state, 'roverLearning') && Boolean(state.session?.isVerified)); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId || null); if (!enabled) return null; return ; } function LearningCard({ roverId }) { const socket = useSocket(); const [snapshot, setSnapshot] = useState(null); const [selectedId, setSelectedId] = useState(''); const [detailsOpen, setDetailsOpen] = useState(false); const [thresholdDraft, setThresholdDraft] = useState(null); const [copyStatus, setCopyStatus] = useState(''); const [working, setWorking] = useState(false); const [error, setError] = useState(''); const { value: settings, save } = useSettingsNamespace('roverLearning', { favorites: [] }); const favorites = Array.isArray(settings?.favorites) ? settings.favorites : []; useEffect(() => { let active = true; const receive = (state) => { if (active) setSnapshot(state); }; const subscribe = () => { if (!socket.connected) return; socket.timeout(5000).emit('roverLearning:subscribe', { roverId, live: detailsOpen }, (failure, response) => { if (!failure && response?.state) receive(response.state); }); }; const disconnected = () => receive(null); socket.on('roverLearning:state', receive); socket.on('connect', subscribe); socket.on('disconnect', disconnected); subscribe(); // A config enable can mount this card before async service startup has // attached handlers. Resubscribe periodically without another data API. const timer = setInterval(subscribe, 10000); return () => { active = false; clearInterval(timer); socket.off('roverLearning:state', receive); socket.off('connect', subscribe); socket.off('disconnect', disconnected); socket.emit('roverLearning:unsubscribe'); }; }, [socket, roverId, detailsOpen]); const state = snapshot?.roverId === roverId ? snapshot : null; const models = [...(state?.models || [])].sort((a, b) => Number(favorites.includes(b.id)) - Number(favorites.includes(a.id)) || Number(b.id === 'latest') - Number(a.id === 'latest') || b.createdAt - a.createdAt); const chosen = models.find((model) => model.id === selectedId) || models.find((model) => model.compatible) || models[0]; const session = state?.session; const training = state?.training; const recording = state?.recording; const limits = state?.limits; const now = state?.updatedAt || 0; const act = (action, value) => { setWorking(true); setError(''); socket.timeout(10000).emit('roverLearning:action', { action, value, roverId, modelId: chosen?.id }, (failure, response) => { setWorking(false); if (action === 'threshold') setThresholdDraft(null); if (failure || response?.error) setError(response?.error || 'The server did not respond. Check the controller status before trying again.'); else if (response?.state) setSnapshot(response.state); }); }; const commitThreshold = (event) => { const value = Number(event.currentTarget.value); if (!working && state?.control?.canAdjust && value !== session?.threshold) act('threshold', value); }; const copyDiagnostics = async () => { try { if (!state?.diagnostics) return; await navigator.clipboard.writeText(JSON.stringify(state.diagnostics)); setCopyStatus('Diagnostics copied'); } catch { setCopyStatus('Clipboard unavailable. Select and copy the report below.'); } }; const favorite = () => { if (!chosen) return; save((current) => { const ids = Array.isArray(current?.favorites) ? current.favorites : []; return { ...current, favorites: ids.includes(chosen.id) ? ids.filter((id) => id !== chosen.id) : [...ids, chosen.id] }; }); }; return

{session ? `${session.modelName} · ${session.status}` : state ? 'Stopped' : 'Waiting for rover learning status…'}

{state?.control?.canSnapshot &&
} {session &&

{session.predictionStatus || 'Waiting for fresh video and sensors'}

{session.proposals?.map((proposal) => { const submission = session.submissions?.find((item) => item.command.type === proposal.command.type && (proposal.command.type !== 'peripheral' || (item.command.peripheral?.id === proposal.command.peripheral?.id && item.command.peripheral?.control === proposal.command.peripheral?.control))); const status = proposal.reason === 'Proposed' && submission ? submission.result : proposal.reason; return
{actionNames[proposal.command.type] || proposal.command.type}{proposal.command.type === 'peripheral' ? ` · ${proposal.command.peripheral?.id} / ${proposal.command.peripheral?.control}` : ''}{proposal.score.toFixed(2)}

{status === 'issued' ? 'Submitted' : status}

; })}
} {state?.control?.reason &&

{state.control.reason}

}

Recording: {recording ? recording.paused ? 'paused for disk space' : `${recording.sessions.filter((item) => item.video === 'Recording human driving').length} driving · ${recording.sessions.filter((item) => item.video === 'Dock lead-in buffer').length} dock buffer(s)` : 'unavailable'} · Training: {training?.status || 'unavailable'}

{(error || state?.catalogError) &&

{error || state.catalogError}

}
setDetailsOpen(event.currentTarget.open)}> System details
{copyStatus}
{copyStatus.startsWith('Clipboard unavailable') &&