/** * NavModeSelector.jsx — Follow mode switcher. * * Publishes: /social/nav/mode (std_msgs/String) * Subscribes: /social/nav/mode (std_msgs/String) — echoed back by social_nav_node * /social/nav/status (std_msgs/String) — freeform status string */ import { useEffect, useState } from 'react'; const MODES = [ { id: 'shadow', label: 'SHADOW', icon: '👤', description: 'Follow directly behind at distance', color: 'border-blue-700 text-blue-300 bg-blue-950', activeColor: 'border-blue-400 text-blue-100 bg-blue-900 mode-active', }, { id: 'lead', label: 'LEAD', icon: '➡', description: 'Robot moves ahead, person follows', color: 'border-green-700 text-green-300 bg-green-950', activeColor: 'border-green-400 text-green-100 bg-green-900 mode-active', }, { id: 'side', label: 'SIDE', icon: '↔', description: 'Walk side-by-side', color: 'border-purple-700 text-purple-300 bg-purple-950', activeColor: 'border-purple-400 text-purple-100 bg-purple-900 mode-active', }, { id: 'orbit', label: 'ORBIT', icon: '⟳', description: 'Circle around the tracked person', color: 'border-amber-700 text-amber-300 bg-amber-950', activeColor: 'border-amber-400 text-amber-100 bg-amber-900 mode-active', }, { id: 'loose', label: 'LOOSE', icon: '⬡', description: 'Follow with generous spacing', color: 'border-teal-700 text-teal-300 bg-teal-950', activeColor: 'border-teal-400 text-teal-100 bg-teal-900 mode-active', }, { id: 'tight', label: 'TIGHT', icon: '⬟', description: 'Follow closely, minimal gap', color: 'border-red-700 text-red-300 bg-red-950', activeColor: 'border-red-400 text-red-100 bg-red-900 mode-active', }, ]; const VOICE_COMMANDS = [ { mode: 'shadow', cmd: '"shadow" / "follow me"' }, { mode: 'lead', cmd: '"lead me" / "go ahead"' }, { mode: 'side', cmd: '"stay beside"' }, { mode: 'orbit', cmd: '"orbit"' }, { mode: 'loose', cmd: '"give me space"' }, { mode: 'tight', cmd: '"stay close"' }, ]; export function NavModeSelector({ subscribe, publish }) { const [activeMode, setActiveMode] = useState(null); const [navStatus, setNavStatus] = useState(''); const [sending, setSending] = useState(null); // Subscribe to echoed mode topic useEffect(() => { const unsub = subscribe( '/social/nav/mode', 'std_msgs/String', (msg) => setActiveMode(msg.data) ); return unsub; }, [subscribe]); // Subscribe to nav status useEffect(() => { const unsub = subscribe( '/social/nav/status', 'std_msgs/String', (msg) => setNavStatus(msg.data) ); return unsub; }, [subscribe]); const handleMode = async (modeId) => { setSending(modeId); publish('/social/nav/mode', 'std_msgs/String', { data: modeId }); // Optimistic update; will be confirmed when echoed back setActiveMode(modeId); setTimeout(() => setSending(null), 800); }; return (
Tap to publish to /social/nav/mode
Voice commands are parsed by social_nav_node from{' '}
/social/speech/command.