move UI stuff around, add message sound

This commit is contained in:
legop3
2025-11-21 04:55:30 -05:00
parent 2baecc90ff
commit b0ca95807c
12 changed files with 80 additions and 58 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-DuG6d4XG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BH17i8qZ.css">
<script type="module" crossorigin src="/assets/index-Bw29mQyW.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-vVHPhuT9.css">
</head>
<body>
<div id="root"></div>
+5 -5
View File
@@ -11,7 +11,7 @@ const RATE_LIMIT_WINDOW_MS = 8000;
const RATE_LIMIT_MAX = 5;
const rateBuckets = new Map(); // socketId -> [timestamps]
const PROFANITY_LIST = ['fuck', 'shit', 'bitch', 'cunt', 'nigger', 'nigga', 'asshole', 'dick'];
const PROFANITY_LIST = ['bitch', 'cunt', 'nigger', 'nigga', 'asshole', 'dick'];
const DUPLICATE_WINDOW_MS = 15000;
const lastMessageBySocket = new Map(); // socketId -> { text, ts }
@@ -95,10 +95,10 @@ function handleIncoming({ text } = {}, socket, cb = () => {}) {
cb({ error: 'Message blocked' });
return;
}
if (isDuplicate(socket.id, clean)) {
cb({ error: 'Duplicate message' });
return;
}
// if (isDuplicate(socket.id, clean)) {
// cb({ error: 'Duplicate message' });
// return;
// }
if (isKeymash(clean)) {
cb({ error: 'Message looks like spam' });
return;
Binary file not shown.
Binary file not shown.
+19 -11
View File
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useChat } from '../context/ChatContext.jsx';
import { useSession } from '../context/SessionContext.jsx';
@@ -30,9 +30,15 @@ export default function ChatPanel() {
const [draft, setDraft] = useState('');
const [sending, setSending] = useState(false);
const canChat = session?.role !== 'spectator';
const listRef = useRef(null);
const sorted = useMemo(() => messages.slice(-200), [messages]);
useEffect(() => {
if (!listRef.current) return;
listRef.current.scrollTop = listRef.current.scrollHeight;
}, [sorted]);
async function handleSend(event) {
event.preventDefault();
if (!canChat) return;
@@ -56,7 +62,7 @@ export default function ChatPanel() {
<span>Chat</span>
<span className="text-xs text-slate-500">{sorted.length}</span>
</div> */}
<div className="surface h-48 overflow-y-auto space-y-0.5">
<div className="surface h-48 overflow-y-auto space-y-0.25" ref={listRef}>
{sorted.length === 0 ? (
<p className="text-sm text-slate-500">No messages yet.</p>
) : (
@@ -66,16 +72,18 @@ export default function ChatPanel() {
return (
<div
key={msg.id}
className={`surface-muted text-sm ${isAdmin ? 'border border-amber-400/30' : ''}`}
className={`surface-muted text-sm flex items-start gap-1 ${
isAdmin ? 'border border-amber-400/30' : ''
}`}
>
<div className="flex items-center gap-1 text-[0.75rem] text-slate-400">
<span>{formatTime(msg.ts)}</span>
<span className={`font-semibold ${roleColors(msg.role)}`}>{displayName(msg)}</span>
{msg.roverId && (
<span className="rounded bg-slate-800 px-1 text-[0.7rem]">rover {msg.roverId}</span>
)}
</div>
<p className="text-slate-100 break-words">{msg.text}</p>
<span className="text-[0.75rem] text-slate-400">{formatTime(msg.ts)}</span>
<span className={`font-semibold text-[0.85rem] ${roleColors(msg.role)}`}>
{displayName(msg)}
</span>
{msg.roverId && (
<span className="rounded bg-slate-800 px-1 text-[0.7rem]">rover {msg.roverId}</span>
)}
<span className="text-slate-100 break-words leading-tight">{msg.text}</span>
</div>
);
})
+8 -8
View File
@@ -100,7 +100,7 @@ export default function UserListPanel() {
<span>Users</span>
<span className="text-xs text-slate-500">{sorted.length}</span>
</div>
<div className="surface h-48 overflow-y-auto space-y-0.5">
<div className="surface h-48 overflow-y-auto space-y-0.25">
{sorted.length === 0 ? (
<p className="text-sm text-slate-500">Waiting for users</p>
) : (
@@ -110,14 +110,14 @@ export default function UserListPanel() {
return (
<div
key={user.socketId}
className="surface-muted flex items-center justify-between gap-0.5 text-sm"
className="surface-muted flex items-center gap-1 text-sm"
>
<div>
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
<p className="text-[0.7rem] text-slate-400">
{user.roverId ? `Driving ${user.roverId}` : 'No rover'}
</p>
</div>
<p className={`font-semibold ${roleColors(user.role)}`}>{formatLabel(user, selfId)}</p>
{user.roverId ? (
<span className="rounded bg-slate-800 px-1 text-[0.7rem]">rover {user.roverId}</span>
) : (
<span className="text-[0.7rem] text-slate-500">no rover</span>
)}
{isAdmin && (
<span className="rounded bg-amber-500/30 px-1 text-[0.7rem] text-amber-200">
Admin
+15 -19
View File
@@ -157,25 +157,22 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
controls={false}
className="h-full w-full object-contain"
/>
<HudOverlay frame={telemetryFrame} label={label}/>
<HudOverlay frame={telemetryFrame} label={label} status={renderedStatus} />
<OvercurrentOverlay motors={overcurrentMotors} />
<LowBatteryOverlay charge={batteryCharge} config={batteryConfig} />
</div>
<BatteryBar charge={batteryCharge} config={batteryConfig} status={renderedStatus} />
<BatteryBar charge={batteryCharge} config={batteryConfig} />
</div>
);
}
function BatteryBar({ charge, config, status }) {
function BatteryBar({ charge, config }) {
const full = config?.Full;
const warn = config?.Warn;
const urgent = config?.Urgent ?? null;
if (charge == null || full == null || warn == null) {
return (
<div className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>{status}</span>
</div>
<p className="text-xs text-slate-500">Battery telemetry unavailable</p>
</div>
);
@@ -192,22 +189,18 @@ function BatteryBar({ charge, config, status }) {
const barClass = depleted ? 'bg-red-500 animate-pulse' : warnTriggered ? 'bg-amber-400' : 'bg-emerald-500';
return (
<div className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>{status}</span>
</div>
<div className="relative h-4 w-full bg-zinc-900 flex">
<div className={`h-full transition-[width] ${barClass}`} style={{ width: `${percentDisplay}%` }}>
<span className="inset-0 flex items-center justify-center text-xs font-semibold text-black/80">
Battery {percentText}
</span>
</div>
</div>
</div>
);
}
function HudOverlay({ frame, label }) {
function HudOverlay({ frame, label, status }) {
const sensors = frame?.sensors;
const bumps = sensors?.bumpsAndWheelDrops || {};
const [now, setNow] = useState(() => Date.now());
@@ -221,18 +214,21 @@ function HudOverlay({ frame, label }) {
return (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 px-0.5 py-0.5 text-xs text-slate-100">
<span>You are driving: {label || 'Unnamed Rover'}</span>
<div className="absolute left-1 top-1 bg-black/70 px-1 py-0.5 text-[0.65rem] font-medium text-slate-100">
<span>Status: {status}</span>
</div>
<div className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 px-0.5 py-0.5 text-slate-100">
<span>Rover: "{label || 'Unnamed Rover'}"</span>
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
</div>
{/* bump and wheel drops bar */}
<div className="absolute top-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/70 px-0.5 py-0.5 text-[0.6rem] font-medium text-slate-200">
<div className={`px-0.5 py-0.5 ${bumps.bumpLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left bump</div>
<div className={`px-0.5 py-0.5 ${bumps.wheelDropLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left wheel drop</div>
<div className={`px-0.5 py-0.5 ${bumps.wheelDropRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right wheel drop</div>
<div className={`px-0.5 py-0.5 ${bumps.bumpRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right bump</div>
<div className="absolute top-0.5 left-1/2 flex -translate-x-1/2 gap-1 bg-black/70 px-0.5 py-0.5 text-[0.6rem] font-medium text-slate-200">
<div className={`px-1 py-0.5 ${bumps.bumpLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left bump</div>
<div className={`px-1 py-0.5 ${bumps.wheelDropLeft ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Left wheel drop</div>
<div className={`px-1 py-0.5 ${bumps.wheelDropRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right wheel drop</div>
<div className={`px-1 py-0.5 ${bumps.bumpRight ? 'bg-red-600 text-white animate-pulse' : 'text-slate-500'}`}>Right bump</div>
</div>
</div>
);
@@ -281,4 +277,4 @@ function LowBatteryOverlay({ charge, config }) {
</div>
</div>
);
}
}
+19 -1
View File
@@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useSocket } from './SocketContext.jsx';
import { useSession } from './SessionContext.jsx';
import messageSound from '../assets/message.mp3';
const ChatContext = createContext({
messages: [],
@@ -21,16 +22,33 @@ export function ChatProvider({ children }) {
const [messages, setMessages] = useState([]);
const [isChatFocused, setIsChatFocused] = useState(false);
const inputRef = useRef(null);
const audioRef = useRef(null);
useEffect(() => {
audioRef.current = new Audio(messageSound);
audioRef.current.load();
}, []);
const playSound = useCallback(() => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = 0;
audio.play().catch(() => {});
}, []);
useEffect(() => {
function handleMessage(payload = {}) {
setMessages((prev) => [...prev.slice(-99), payload]);
if (payload?.socketId && session?.socketId && payload.socketId === session.socketId) {
return;
}
playSound();
}
socket.on('chat:message', handleMessage);
return () => {
socket.off('chat:message', handleMessage);
};
}, [socket]);
}, [playSound, session?.socketId, socket]);
const sendMessage = useCallback(
(text) =>