/mini and chat improvements

This commit is contained in:
legop3
2026-01-18 16:15:41 -05:00
parent df952b27e0
commit c7a639a568
7 changed files with 183 additions and 186 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-B1wXlWfv.js"></script>
<script type="module" crossorigin src="/assets/index-dT4ud4zG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BvvbAZLx.css">
</head>
<body>
+14 -2
View File
@@ -11,7 +11,7 @@ function buildKey(alert) {
return `${alert.title || 'alert'}-${alert.message}`;
}
export default function AlertFeed() {
export default function AlertFeed({ scale = 1 }) {
const { alerts } = useSession();
const [now, setNow] = useState(() => Date.now());
const latest = useMemo(() => alerts.slice(-3).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]);
@@ -31,8 +31,20 @@ export default function AlertFeed() {
if (!visible.length) return null;
const containerStyle =
scale === 1
? undefined
: {
transform: `translateX(-50%) scale(${scale})`,
transformOrigin: 'top center',
};
const containerClass =
scale === 1
? 'pointer-events-none fixed top-0.5 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-0.5'
: 'pointer-events-none fixed top-0.5 left-1/2 z-50 flex flex-col gap-0.5';
return (
<div className="pointer-events-none fixed top-0.5 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-0.5">
<div className={containerClass} style={containerStyle}>
{visible.map((toast) => (
<AlertToast key={toast.key} alert={toast.alert} />
))}
+1 -1
View File
@@ -88,7 +88,7 @@ export default function ChatMessageRow({ message }) {
{displayName(message)}
</span>
{message.roverId && (
<span className="rounded bg-slate-800 px-1 text-[0.7rem]">rover {message.roverId}</span>
<span className="rounded bg-slate-800 px-1 text-[0.7rem]">{message.roverId}</span>
)}
<span className="text-slate-100 break-words leading-tight whitespace-pre-wrap">{message.text}</span>
<span className="absolute bottom-0.5 right-1 text-[0.65rem] text-slate-400/60">
+19 -10
View File
@@ -52,6 +52,7 @@ export default function VideoTile({
driverLabel = null,
hudForceMap = false,
hudMapPosition = 'top-right',
hudLabelScale = 1,
fitParent = false,
showTurnCue = false,
turnTimerText = null,
@@ -416,6 +417,7 @@ export default function VideoTile({
mobileHud={mobileHud}
mapPosition={hudMapPosition}
turnTimerText={turnTimerText}
labelScale={hudLabelScale}
/>
<HudChatInput compact={mobileHud} />
<OvercurrentOverlay motors={overcurrentMotors} compact={mobileHud} />
@@ -548,6 +550,7 @@ function HudOverlay({
mobileHud = false,
mapPosition = 'top-right',
turnTimerText = null,
labelScale = 1,
}) {
const bumps = sensors?.bumpsAndWheelDrops || {};
const [now, setNow] = useState(() => Date.now());
@@ -569,6 +572,10 @@ function HudOverlay({
const timerPadClass = isMobile ? 'px-0.5 py-0.25' : 'px-1 py-0.5';
const telemetryPosClass = isMobile ? 'left-0.5 top-1/2' : 'left-1 top-1/2';
const labelPosClass = isMobile ? 'bottom-0.5' : 'bottom-0.5';
const labelWrapperStyle = {
transform: `translateX(-50%) scale(${labelScale})`,
transformOrigin: 'center bottom',
};
const mapSize = '240px';
const mapScale = portraitMobile ? 0.36 : isMobile ? 0.45 : 0.7;
const mapOpacity = isMobile ? 0.85 : 0.7;
@@ -630,11 +637,13 @@ function HudOverlay({
) : null}
</div>
<div
className={`absolute ${labelPosClass} left-1/2 flex -translate-x-1/2 items-center gap-1 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}
>
<span className="font-semibold text-white">{label || 'Unnamed Rover'}</span>
{driverLabel ? <span className="text-slate-300"> {driverLabel}</span> : null}
<div className={`absolute ${labelPosClass} left-1/2`} style={labelWrapperStyle}>
<div
className={`flex items-center gap-1 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}
>
<span className="font-semibold text-white">{label || 'Unnamed Rover'}</span>
{driverLabel ? <span className="text-slate-300"> {driverLabel}</span> : null}
</div>
</div>
</div>
);
@@ -655,11 +664,11 @@ function HudOverlay({
{turnTimerText}
</div>
) : null}
<div
className={`absolute ${labelPosClass} left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}
>
<span>Rover: "{label || 'Unnamed Rover'}"</span>
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
<div className={`absolute ${labelPosClass} left-1/2`} style={labelWrapperStyle}>
<div className={`flex gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}>
<span>Rover: "{label || 'Unnamed Rover'}"</span>
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
</div>
</div>
{showTopDown && variant !== 'spectator' ? (
+29 -53
View File
@@ -5,13 +5,13 @@ import { useTelemetryFrames } from '../context/TelemetryContext.jsx';
import { useVideoRequests } from '../hooks/useVideoRequests.js';
import { useRoverSnapshots } from '../hooks/useRoverSnapshots.js';
import { useSpectatorMode } from '../hooks/useSpectatorMode.js';
import { useRoomCameraSnapshots } from '../hooks/useRoomCameraSnapshots.js';
import VideoTile from '../components/VideoTile.jsx';
import ChatPanel from '../components/ChatPanel.jsx';
import AlertFeed from '../components/AlertFeed.jsx';
import useDefaultNickname from '../hooks/useDefaultNickname.js';
const ROTATE_MS = 20000;
const HARD_REFRESH_MS = 3 * 60 * 60 * 1000;
function formatDriverLabel({ roverId, session }) {
const activeDriverId = session?.activeDrivers?.[roverId] || null;
@@ -29,47 +29,42 @@ function MiniSummaryContent() {
const inLockdown = session?.mode === 'lockdown';
const frames = useTelemetryFrames();
const roster = session?.roster ?? [];
const roomCameras = session?.roomCameras || [];
const feeds = useRoomCameraSnapshots(roomCameras.map((camera) => ({ id: camera.id })), {
enabled: !inLockdown,
version: session?.mode,
});
const [index, setIndex] = useState(0);
const activeDrivers = session?.activeDrivers || {};
const driverRoster = useMemo(
() => roster.filter((rover) => activeDrivers[rover.id]),
[roster, activeDrivers],
);
const snapshotFeeds = useRoverSnapshots(
roster.map((rover) => rover.id),
driverRoster.map((rover) => rover.id),
{ enabled: !inLockdown, version: session?.mode },
);
const audioEntries = useMemo(
() =>
roster.flatMap((rover) => {
driverRoster.flatMap((rover) => {
if (!rover?.id || !rover.media?.audioPublishUrl) return [];
const id = String(rover.id);
return [{ type: 'rover', id: `${id}-audio`, key: `${id}-audio` }];
}),
[roster],
[driverRoster],
);
const audioSources = useVideoRequests(audioEntries, { enabled: !inLockdown, version: session?.mode });
const roverPool = useMemo(() => {
if (!roster.length) return [];
const withSnapshot = roster.filter((rover) => snapshotFeeds[rover.id]?.objectUrl);
return withSnapshot.length ? withSnapshot : roster;
}, [roster, snapshotFeeds]);
if (!driverRoster.length) return [];
const withSnapshot = driverRoster.filter((rover) => snapshotFeeds[rover.id]?.objectUrl);
return withSnapshot.length ? withSnapshot : driverRoster;
}, [driverRoster, snapshotFeeds]);
const rotationPool = useMemo(() => {
const items = [];
roverPool.forEach((rover) => items.push({ type: 'rover', rover }));
roomCameras.forEach((camera) => items.push({ type: 'room', camera }));
return items;
}, [roverPool, roomCameras]);
return roverPool.map((rover) => ({ type: 'rover', rover }));
}, [roverPool]);
const rotationKey = useMemo(
() =>
rotationPool
.map((entry) =>
entry.type === 'rover' ? `r:${entry.rover.id}` : `room:${entry.camera.id}`,
)
.map((entry) => `r:${entry.rover.id}`)
.join('|'),
[rotationPool],
);
@@ -88,13 +83,11 @@ function MiniSummaryContent() {
const activeEntry = rotationPool.length ? rotationPool[index % rotationPool.length] : null;
const activeRover = activeEntry?.type === 'rover' ? activeEntry.rover : null;
const activeCamera = activeEntry?.type === 'room' ? activeEntry.camera : null;
const activeSnapshot = activeRover ? snapshotFeeds[activeRover.id] || null : null;
const activeAudio = activeRover ? audioSources[`${activeRover.id}-audio`] || null : null;
const activeFrame = activeRover ? frames[activeRover.id] || null : null;
const driverLabel = activeRover ? formatDriverLabel({ roverId: activeRover.id, session }) : null;
const activeFeed = activeCamera ? feeds[activeCamera.id] || null : null;
if (inLockdown) {
return (
@@ -128,18 +121,15 @@ function MiniSummaryContent() {
layoutFormat="mobile"
hudVariant="spectator"
driverLabel={driverLabel}
hudLabelScale={3}
hudForceMap
hudMapPosition="bottom-left"
fitParent
/>
</FitViewportFrame>
) : activeCamera ? (
<FitViewportFrame>
<RoomCameraFrame camera={activeCamera} feed={activeFeed} />
</FitViewportFrame>
) : (
<div className="flex h-full w-full items-center justify-center text-sm text-slate-500">
No sources available.
{driverRoster.length ? 'No sources available.' : 'No active drivers.'}
</div>
)}
</section>
@@ -148,40 +138,26 @@ function MiniSummaryContent() {
}
export default function MiniSummaryApp() {
useEffect(() => {
if (typeof window === 'undefined') return undefined;
const timer = setTimeout(() => {
const url = new URL(window.location.href);
url.searchParams.set('refresh', Date.now().toString());
window.location.replace(url.toString());
}, HARD_REFRESH_MS);
return () => clearTimeout(timer);
}, []);
return (
<SettingsProvider>
<>
<MiniSummaryContent />
<AlertFeed />
<AlertFeed scale={3} />
</>
</SettingsProvider>
);
}
function RoomCameraFrame({ camera, feed }) {
const hasImage = feed?.objectUrl;
const connecting = feed && feed.status === 'connecting';
return (
<div className="relative h-full w-full bg-zinc-950">
{hasImage ? (
<img
src={feed.objectUrl}
alt={camera.name || camera.id}
className="h-full w-full object-cover"
draggable={false}
/>
) : (
<div className="flex h-full w-full items-center justify-center text-sm text-slate-500">
{connecting ? `Connecting to ${camera.name || camera.id}` : 'No frame yet'}
</div>
)}
<div className="absolute left-1 top-1 rounded bg-black/60 px-1 py-0.25 text-xs text-slate-200">
{camera.name || camera.id}
</div>
</div>
);
}
function ChatOverlay() {
return (
<div