mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
chat history, better turn queue updates
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user