xiaohongshu
xiaohongshu
console-frontend-review
Reviews React/TypeScript code for the depot console web application with focus on real-time rover teleoperation, state management, WebSocket communication, and 3D visualization. Use when reviewing console frontend changes, debugging teleop UI issues, optimizing rendering performance, validating W...
Full skill instructions
This skill provides comprehensive code review for the depot console React/TypeScript web application used for fleet operations and rover teleoperation.
The depot console is a React 19 web application providing real-time control and monitoring of BVR rovers. It features 3D visualization, WebSocket-based teleop, and 360° video streaming.
Technology Stack:
Architecture:
depot/console/
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx # Router setup
│ ├── store.ts # Zustand global state (single source of truth)
│ ├── components/ # React components
│ │ ├── scene/ # React Three Fiber 3D components
│ │ ├── teleop/ # Teleoperation UI panels
│ │ └── ui/ # Radix UI + CVA primitives
│ ├── views/ # Page-level components
│ ├── hooks/ # Custom React hooks
│ │ ├── useRoverConnection.ts # WebSocket teleop
│ │ ├── useVideoStream.ts # 360 video stream
│ │ ├── useGamepad.ts # Gamepad input polling
│ │ ├── useKeyboard.ts # Keyboard input handling
│ │ └── useDiscovery.ts # Rover discovery service
│ └── lib/
│ ├── types.ts # TypeScript type definitions
│ ├── protocol.ts # Binary protocol codec
│ └── utils.ts # Utility functions
Critical Files:
depot/console/src/store.ts (~400 lines)depot/console/src/lib/types.ts (~300 lines)depot/console/src/lib/protocol.ts (~200 lines)depot/console/src/hooks/useRoverConnection.ts (~250 lines)depot/console/src/hooks/useVideoStream.ts (~150 lines)depot/console/src/components/scene/Scene.tsx (~200 lines)Location: depot/console/src/store.ts
Key Points to Review:
create<ConsoleState>()set() with object spreadStore Domains:
Fleet Management:
rovers: RoverInfo[] - List of discovered roversselectedRoverId: string | null - Currently selected roverselectRover(id) - Select rover and update addressesConnection State:
roverAddress: string - WebSocket teleop address (ws://localhost:4850)videoAddress: string - WebSocket video address (ws://localhost:4851)connected: boolean - Connection statuslatencyMs: number - Round-trip latencyTelemetry (Real-time Rover State):
mode: Mode - Operational mode (Idle, Teleop, etc.)pose: Pose - Position (x, y, theta)velocity: Twist - Current velocitypower: PowerStatus - Battery voltage, currenttemperatures: TempStatus - Motor and controller tempsInput:
input: GamepadInput - Normalized gamepad/keyboard inputinputSource: InputSource - "gamepad" | "keyboard" | "none"Camera:
cameraMode: CameraMode - ThirdPerson, FirstPerson, FreeLookcameraSettings - FOV, distance, offsetVideo:
videoFrame: string | null - Blob URL for current framevideoConnected: booleanvideoFps: numberExample Pattern:
// Good: Zustand store with domain slices
export const useConsoleStore = create<ConsoleState>((set) => ({
// Fleet state
rovers: [],
selectedRoverId: null,
selectRover: (id: string) =>
set((state) => {
const rover = state.rovers.find((r) => r.id === id);
return {
selectedRoverId: id,
roverAddress: rover ? `ws://${rover.hostname}:4850` : state.roverAddress,
videoAddress: rover ? `ws://${rover.hostname}:4851` : state.videoAddress,
};
}),
// Telemetry state
mode: Mode.Disabled,
pose: { x: 0, y: 0, theta: 0 },
updateTelemetry: (telemetry: Partial<Telemetry>) =>
set((state) => ({
...state,
...telemetry, // Partial merge
})),
// Connection state
connected: false,
setConnected: (connected: boolean) => set({ connected }),
}));
Red Flags:
state.rovers.push(...))Key Points to Review:
useConsoleStore() hook to access stateExample Pattern:
// Good: Selector for specific state slice
function TelemetryPanel() {
const { mode, pose, velocity } = useConsoleStore((state) => ({
mode: state.mode,
pose: state.pose,
velocity: state.velocity,
}));
return (
<div>
<div>Mode: {ModeLabels[mode]}</div>
<div>Position: ({pose.x.toFixed(2)}, {pose.y.toFixed(2)})</div>
<div>Velocity: {velocity.linear.toFixed(2)} m/s</div>
</div>
);
}
// Bad: Selecting entire state (re-renders on any state change)
const state = useConsoleStore(); // ❌ Don't do this
Location: depot/console/src/lib/types.ts
Key Points to Review:
as const patternany types (use unknown or proper types)Example Pattern:
// Good: Const enum with type alias
export const Mode = {
Disabled: 0,
Idle: 1,
Teleop: 2,
Autonomous: 3,
EStop: 4,
Fault: 5,
} as const;
export type Mode = (typeof Mode)[keyof typeof Mode];
// Good: Label map for UI display
export const ModeLabels: Record<Mode, string> = {
[Mode.Disabled]: "Disabled",
[Mode.Idle]: "Idle",
[Mode.Teleop]: "Teleop",
[Mode.Autonomous]: "Autonomous",
[Mode.EStop]: "E-Stop",
[Mode.Fault]: "Fault",
};
// Good: Interface for objects
export interface Telemetry {
mode: Mode;
pose: Pose;
velocity: Twist;
power: PowerStatus;
temperatures: TempStatus;
}
// Good: Discriminated union for input source
export type InputSource = "gamepad" | "keyboard" | "none";
Red Flags:
type used for objects (use interface)any typesKey Points to Review:
interface keyword? operatorReact.ReactNodeExample Pattern:
// Good: Props interface
interface TelemetryPanelProps {
className?: string;
showAdvanced?: boolean;
}
export function TelemetryPanel({ className, showAdvanced = false }: TelemetryPanelProps) {
// ...
}
Location: depot/console/src/lib/protocol.ts
Message Types:
0x01-0x0F0x10-0x1F0x20-0x2FKey Points to Review:
DataView with little-endianCommand Encoding:
// Good: Binary command encoding
export function encodeTwist(twist: Twist): ArrayBuffer {
const buffer = new ArrayBuffer(25); // 1 + 3*8 bytes
const view = new DataView(buffer);
view.setUint8(0, MSG_TWIST); // Message type
view.setFloat64(1, twist.linear, true); // Little-endian f64
view.setFloat64(9, twist.angular, true);
view.setUint8(17, twist.boost ? 1 : 0);
return buffer;
}
// Bad: JSON encoding (too slow for 100Hz)
const json = JSON.stringify({ type: "twist", ...twist }); // ❌ Inefficient
Telemetry Decoding:
// Good: Binary telemetry decoding with bounds check
export function decodeTelemetry(data: ArrayBuffer): Telemetry {
if (data.byteLength < 90) {
throw new Error(`Telemetry frame too short: ${data.byteLength} bytes`);
}
const view = new DataView(data);
const type = view.getUint8(0);
if (type !== MSG_TELEMETRY) {
throw new Error(`Invalid message type: ${type}`);
}
return {
mode: view.getUint8(1),
pose: {
x: view.getFloat64(2, true),
y: view.getFloat64(10, true),
theta: view.getFloat32(18, true),
},
velocity: {
linear: view.getFloat32(22, true),
angular: view.getFloat32(26, true),
boost: view.getUint8(30) !== 0,
},
// ... more fields
};
}
See: websocket-protocols.md for complete protocol reference.
Location: depot/console/src/hooks/useRoverConnection.ts
Key Points to Review:
new WebSocket(url)onopen, onmessage, onclose, onerror"arraybuffer"Example Pattern:
// Good: WebSocket with cleanup
export function useRoverConnection() {
const [ws, setWs] = useState<WebSocket | null>(null);
const address = useConsoleStore((state) => state.roverAddress);
useEffect(() => {
const socket = new WebSocket(address);
socket.binaryType = "arraybuffer"; // Critical for binary protocol
socket.onopen = () => {
console.log("Connected to rover");
useConsoleStore.getState().setConnected(true);
};
socket.onmessage = (event: MessageEvent) => {
const telemetry = decodeTelemetry(event.data);
useConsoleStore.getState().updateTelemetry(telemetry);
};
socket.onclose = () => {
console.log("Disconnected from rover");
useConsoleStore.getState().setConnected(false);
// Reconnect after 3s
setTimeout(() => setWs(null), 3000);
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};
setWs(socket);
// Cleanup on unmount
return () => {
socket.close();
};
}, [address]);
return { ws };
}
Red Flags:
binaryType = "arraybuffer" (defaults to Blob, slower)Key Points to Review:
Example Pattern:
// Good: Command sending at 100Hz
useEffect(() => {
if (!ws || !connected) return;
const interval = setInterval(() => {
const input = useConsoleStore.getState().input;
const twist = { linear: input.linear, angular: input.angular, boost: input.boost };
const buffer = encodeTwist(twist);
ws.send(buffer);
}, 10); // 100Hz = 10ms period
return () => clearInterval(interval);
}, [ws, connected]);
// Good: Heartbeat at 10Hz
useEffect(() => {
if (!ws || !connected) return;
const interval = setInterval(() => {
const buffer = encodeHeartbeat();
ws.send(buffer);
}, 100); // 10Hz = 100ms period
return () => clearInterval(interval);
}, [ws, connected]);
Purpose: Stop sending motor commands when tab loses focus (user switches tabs).
Key Points to Review:
document.visibilityState monitoredhiddenExample Pattern:
// Good: Page visibility tracking
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
// Stop all commands
useConsoleStore.getState().setInput({
linear: 0,
angular: 0,
boost: false,
});
useConsoleStore.getState().setInputSource("none");
console.warn("Tab hidden, stopping commands");
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
}, []);
Red Flags:
Key Points to Review:
Example Pattern:
// Good: E-Stop button
function EStopButton() {
const { ws } = useRoverConnection();
const handleEStop = () => {
if (!ws) return;
const buffer = encodeEStop();
ws.send(buffer);
// Visual feedback
useConsoleStore.getState().addToast({
title: "E-Stop Activated",
variant: "destructive",
});
};
return (
<Button
variant="destructive"
size="lg"
onClick={handleEStop}
className="fixed top-4 right-4 z-50"
>
<AlertTriangle className="mr-2" />
E-STOP
</Button>
);
}
Location: depot/console/src/hooks/useGamepad.ts
Key Points to Review:
requestAnimationFrame (not event-based)Example Pattern:
// Good: Gamepad polling with deadzone
export function useGamepad() {
const [input, setInput] = useState<GamepadInput>({ linear: 0, angular: 0, boost: false });
useEffect(() => {
let frameId: number;
const poll = () => {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[0];
if (gamepad) {
const DEADZONE = 0.1;
// Left stick Y (inverted): linear
let linear = -gamepad.axes[1];
if (Math.abs(linear) < DEADZONE) linear = 0;
// Right stick X: angular
let angular = gamepad.axes[2];
if (Math.abs(angular) < DEADZONE) angular = 0;
// Button 0 (A): boost
const boost = gamepad.buttons[0].pressed;
setInput({ linear, angular, boost });
useConsoleStore.getState().setInputSource("gamepad");
}
frameId = requestAnimationFrame(poll);
};
frameId = requestAnimationFrame(poll);
return () => cancelAnimationFrame(frameId);
}, []);
return input;
}
Red Flags:
Location: depot/console/src/hooks/useKeyboard.ts
Key Points to Review:
document or windowSet or objectExample Pattern:
// Good: Keyboard input with state tracking
export function useKeyboard() {
const [keys, setKeys] = useState<Set<string>>(new Set());
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't capture if typing in input
if (e.target instanceof HTMLInputElement) return;
setKeys((prev) => new Set(prev).add(e.code));
};
const handleKeyUp = (e: KeyboardEvent) => {
setKeys((prev) => {
const next = new Set(prev);
next.delete(e.code);
return next;
});
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, []);
// Convert keys to input
const input = useMemo(() => {
let linear = 0;
let angular = 0;
if (keys.has("KeyW")) linear += 1;
if (keys.has("KeyS")) linear -= 1;
if (keys.has("KeyA")) angular += 1;
if (keys.has("KeyD")) angular -= 1;
return { linear, angular, boost: keys.has("ShiftLeft") };
}, [keys]);
if (input.linear !== 0 || input.angular !== 0) {
useConsoleStore.getState().setInputSource("keyboard");
}
return input;
}
Location: depot/console/src/components/scene/Scene.tsx
Key Points to Review:
<Canvas> wraps all Three.js components<Perf> in dev)Example Pattern:
// Good: Canvas setup with lighting
export function Scene() {
return (
<Canvas shadows camera={{ fov: 60, position: [0, 5, 10] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} castShadow />
<RoverModel />
<Ground />
<EquirectangularSky />
<CameraController />
</Canvas>
);
}
Key Points to Review:
lerp (smooth motion)Example Pattern:
// Good: Smooth position interpolation
function RoverModel() {
const pose = useConsoleStore((state) => state.pose);
const ref = useRef<THREE.Group>(null);
useFrame((state, delta) => {
if (!ref.current) return;
// Lerp position
ref.current.position.x = THREE.MathUtils.lerp(ref.current.position.x, pose.x, delta * 10);
ref.current.position.z = THREE.MathUtils.lerp(ref.current.position.z, -pose.y, delta * 10);
// Lerp rotation (handle wraparound)
const targetRot = -pose.theta;
const currentRot = ref.current.rotation.y;
const diff = ((targetRot - currentRot + Math.PI) % (2 * Math.PI)) - Math.PI;
ref.current.rotation.y += diff * delta * 10;
});
return (
<group ref={ref}>
{/* Rover geometry */}
</group>
);
}
Red Flags:
Key Points to Review:
URL.revokeObjectURL()Example Pattern:
// Good: Texture cleanup
useEffect(() => {
if (!videoFrame) return;
const texture = new THREE.TextureLoader().load(videoFrame);
return () => {
texture.dispose(); // Free GPU memory
URL.revokeObjectURL(videoFrame); // Free blob URL
};
}, [videoFrame]);
See: performance.md for optimization strategies.
Convention: PascalCase for components, camelCase for hooks/utils.
Key Points to Review:
TelemetryPanel.tsx, RoverModel.tsxuseGamepad.ts, useRoverConnection.tsutils.ts, protocol.ts.ts or .tsx)Key Points to Review:
Example Pattern:
// Good: Component structure
interface TelemetryPanelProps {
className?: string;
}
export function TelemetryPanel({ className }: TelemetryPanelProps) {
// 1. Zustand store access
const { mode, pose, velocity } = useConsoleStore((state) => ({
mode: state.mode,
pose: state.pose,
velocity: state.velocity,
}));
// 2. Local state
const [expanded, setExpanded] = useState(false);
// 3. Effects
useEffect(() => {
// Side effects
}, []);
// 4. Event handlers
const handleToggle = () => setExpanded(!expanded);
// 5. Render
return (
<Card className={cn("p-4", className)}>
<h2>Telemetry</h2>
<div>Mode: {ModeLabels[mode]}</div>
<div>Position: ({pose.x.toFixed(2)}, {pose.y.toFixed(2)})</div>
<Button onClick={handleToggle}>Toggle</Button>
</Card>
);
}
Key Points to Review:
any types allowedRun linting:
npm run lint
Key Points to Review:
npm run buildtsconfig.jsonKey Points to Review:
npm run buildFor more detailed information, see:
# Lint code
npm run lint
# Type check and build
npm run build
# Dev server with hot-reload
npm run dev
# Run tests (if configured)
npm run test
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience