/** * App.jsx — Saltybot Social Dashboard root component. * * Layout: * [TopBar: connection config + pipeline state badge] * [TabNav: Status | Faces | Conversation | Personality | Navigation] * [TabContent] */ import { useState, useCallback } from 'react'; import { useRosbridge } from './hooks/useRosbridge.js'; import { StatusPanel } from './components/StatusPanel.jsx'; import { FaceGallery } from './components/FaceGallery.jsx'; import { ConversationLog } from './components/ConversationLog.jsx'; import { PersonalityTuner } from './components/PersonalityTuner.jsx'; import { NavModeSelector } from './components/NavModeSelector.jsx'; const TABS = [ { id: 'status', label: 'Status', icon: '⬤' }, { id: 'faces', label: 'Faces', icon: '◉' }, { id: 'conversation', label: 'Conversation', icon: '◌' }, { id: 'personality', label: 'Personality', icon: '◈' }, { id: 'navigation', label: 'Navigation', icon: '◫' }, ]; const DEFAULT_WS_URL = 'ws://localhost:9090'; function ConnectionBar({ url, setUrl, connected, error }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(url); const handleApply = () => { setUrl(draft); setEditing(false); }; return (
{/* Connection dot */}
{editing ? (
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleApply(); if (e.key === 'Escape') setEditing(false); }} autoFocus className="bg-gray-900 border border-cyan-800 rounded px-2 py-0.5 text-cyan-300 w-52 focus:outline-none" />
) : ( )}
); } export default function App() { const [wsUrl, setWsUrl] = useState(DEFAULT_WS_URL); const [activeTab, setActiveTab] = useState('status'); const { connected, error, subscribe, publish, callService, setParam } = useRosbridge(wsUrl); // Memoized publish for NavModeSelector (avoids recreating on every render) const publishFn = useCallback( (name, type, data) => publish(name, type, data), [publish] ); return (
{/* ── Top Bar ── */}
⚡ SALTYBOT SOCIAL DASHBOARD
{/* ── Tab Nav ── */} {/* ── Content ── */}
{activeTab === 'status' && ( )} {activeTab === 'faces' && ( )} {activeTab === 'conversation' && ( )} {activeTab === 'personality' && ( )} {activeTab === 'navigation' && ( )}
{/* ── Footer ── */}
); }