chat history, better turn queue updates

This commit is contained in:
legop3
2026-01-15 02:55:46 -05:00
parent 23fe03ca54
commit 37927d82af
20 changed files with 585 additions and 36 deletions
+49 -22
View File
@@ -1,4 +1,5 @@
import { useMemo, useState } from 'react';
import { useCommandPipeline } from '../controls/commandPipeline.js';
import { useSession } from '../context/SessionContext.jsx';
import RoverRoster from './RoverRoster.jsx';
@@ -9,8 +10,11 @@ const MODES = [
{ key: 'lockdown', label: 'Lockdown' },
];
const IR_SHOT_CODE = 200;
export default function AdminPanel() {
const { session, lockRover, setMode, requestControl } = useSession();
const pipeline = useCommandPipeline();
const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
const [lockStates, setLockStates] = useState({});
const health = session?.health || null;
@@ -48,6 +52,17 @@ export default function AdminPanel() {
}
};
const handleIrShot = (roverId) => {
pipeline.emitCommand(
{
type: 'ir',
data: { ir: { code: IR_SHOT_CODE } },
},
null,
roverId,
);
};
const lockMap = useMemo(() => {
const map = {};
roster.forEach((rover) => {
@@ -56,39 +71,51 @@ export default function AdminPanel() {
return map;
}, [roster, lockStates]);
if (!isAdmin) return null;
return (
<section className="panel-section space-y-0.5 text-base">
<div className="flex items-center justify-between gap-0.5 text-sm">
<span>Admin controls</span>
<select value={currentMode} onChange={handleModeChange} className="field-input text-sm">
{MODES.map((mode) => (
<option key={mode.key} value={mode.key}>
{mode.label}
</option>
))}
</select>
</div>
{isAdmin ? (
<div className="flex items-center justify-between gap-0.5 text-sm">
<span>Admin controls</span>
<select value={currentMode} onChange={handleModeChange} className="field-input text-sm">
{MODES.map((mode) => (
<option key={mode.key} value={mode.key}>
{mode.label}
</option>
))}
</select>
</div>
) : (
<div className="flex items-center justify-between gap-0.5 text-sm">
<span>Rover controls</span>
<span className="panel-muted text-xs">Limited access</span>
</div>
)}
<RoverRoster
roster={roster}
renderActions={(rover) => (
<div className="flex flex-wrap gap-0.5 text-xs">
<button
type="button"
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
className="button-dark"
>
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
</button>
<button type="button" onClick={() => handleForceControl(rover.id)} className="button-dark">
Force
{isAdmin ? (
<>
<button
type="button"
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
className="button-dark"
>
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
</button>
<button type="button" onClick={() => handleForceControl(rover.id)} className="button-dark">
Force
</button>
</>
) : null}
<button type="button" onClick={() => handleIrShot(rover.id)} className="button-dark">
IR Shot
</button>
</div>
)}
/>
<ReplaySnapshotHealth health={health} />
{isAdmin ? <ReplaySnapshotHealth health={health} /> : null}
</section>
);
}
+18
View File
@@ -50,6 +50,24 @@ export function ChatProvider({ children }) {
};
}, [playSound, session?.socketId, socket]);
useEffect(() => {
function handleInit(payload = []) {
if (!Array.isArray(payload)) return;
setMessages((prev) => {
if (prev.length === 0) {
return payload.slice(-100);
}
const seen = new Set(payload.map((entry) => entry?.id));
const merged = [...payload, ...prev.filter((entry) => entry?.id && !seen.has(entry.id))];
return merged.slice(-100);
});
}
socket.on('chat:init', handleInit);
return () => {
socket.off('chat:init', handleInit);
};
}, [socket]);
const sendMessage = useCallback(
(text, tts = null) =>
new Promise((resolve, reject) => {
+4 -3
View File
@@ -32,9 +32,10 @@ export function useCommandPipeline() {
}, [rosterEntry]);
const emitCommand = useCallback(
(payload, cb) => {
if (!roverId) return;
socket.emit('command', { roverId, ...payload }, cb);
(payload, cb, targetRoverId) => {
const roverTarget = targetRoverId ?? roverId;
if (!roverTarget) return;
socket.emit('command', { roverId: roverTarget, ...payload }, cb);
},
[socket, roverId],
);