"use client"; import React, { useState, useRef } from "react"; import { Bot, User, Activity, Wifi, WifiOff, AlertCircle, CheckCircle, Clock, Power, PowerOff, Eye, } from "lucide-react"; import { Badge } from "~/components/ui/badge"; import { Separator } from "~/components/ui/separator"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs"; import { Progress } from "~/components/ui/progress"; import { Button } from "~/components/ui/button"; interface TrialData { id: string; status: "scheduled" | "in_progress" | "completed" | "aborted" | "failed"; scheduledAt: Date | null; startedAt: Date | null; completedAt: Date | null; duration: number | null; sessionNumber: number | null; notes: string | null; experimentId: string; participantId: string | null; wizardId: string | null; experiment: { id: string; name: string; description: string | null; studyId: string; }; participant: { id: string; participantCode: string; demographics: Record | null; }; } interface TrialEvent { type: string; timestamp: Date; data?: unknown; message?: string; } interface WizardMonitoringPanelProps { trial: TrialData; trialEvents: TrialEvent[]; isConnected: boolean; wsError?: string; activeTab: "status" | "robot" | "events"; onTabChange: (tab: "status" | "robot" | "events") => void; } const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({ trial, trialEvents, isConnected, wsError, activeTab, onTabChange, }: WizardMonitoringPanelProps) { // ROS Bridge connection state const [rosConnected, setRosConnected] = useState(false); const [rosConnecting, setRosConnecting] = useState(false); const [rosError, setRosError] = useState(null); const [rosSocket, setRosSocket] = useState(null); const [robotStatus, setRobotStatus] = useState({ connected: false, battery: 0, position: { x: 0, y: 0, theta: 0 }, joints: {}, sensors: {}, lastUpdate: new Date(), }); const ROS_BRIDGE_URL = "ws://134.82.159.25:9090"; // Use refs to persist connection state across re-renders const connectionAttemptRef = useRef(false); const socketRef = useRef(null); const connectRos = () => { // Prevent multiple connection attempts if (connectionAttemptRef.current) { console.log("Connection already in progress, skipping"); return; } if ( rosSocket?.readyState === WebSocket.OPEN || socketRef.current?.readyState === WebSocket.OPEN ) { console.log("Already connected, skipping"); return; } // Prevent rapid reconnection attempts if (rosConnecting) { console.log("Connection in progress, please wait"); return; } connectionAttemptRef.current = true; setRosConnecting(true); setRosError(null); console.log("🔌 Connecting to ROS Bridge:", ROS_BRIDGE_URL); const socket = new WebSocket(ROS_BRIDGE_URL); socketRef.current = socket; // Add connection timeout const connectionTimeout = setTimeout(() => { if (socket.readyState === WebSocket.CONNECTING) { socket.close(); connectionAttemptRef.current = false; setRosConnecting(false); setRosError("Connection timeout (10s) - ROS Bridge not responding"); } }, 10000); socket.onopen = () => { clearTimeout(connectionTimeout); connectionAttemptRef.current = false; console.log("Connected to ROS Bridge successfully"); setRosConnected(true); setRosConnecting(false); setRosSocket(socket); setRosError(null); // Just log connection success - no auto actions console.log("WebSocket connected successfully to ROS Bridge"); setRobotStatus((prev) => ({ ...prev, connected: true, lastUpdate: new Date(), })); }; socket.onmessage = (event) => { try { const data = JSON.parse(event.data as string) as { topic?: string; msg?: Record; op?: string; level?: string; }; // Handle status messages if (data.op === "status") { console.log("ROS Bridge status:", data.msg, "Level:", data.level); return; } // Handle topic messages if (data.topic === "/joint_states" && data.msg) { setRobotStatus((prev) => ({ ...prev, joints: data.msg ?? {}, lastUpdate: new Date(), })); } else if (data.topic === "/naoqi_driver/battery" && data.msg) { const batteryPercent = (data.msg.percentage as number) || 0; setRobotStatus((prev) => ({ ...prev, battery: Math.round(batteryPercent), lastUpdate: new Date(), })); } else if (data.topic === "/diagnostics" && data.msg) { // Handle diagnostic messages for battery console.log("Diagnostics received:", data.msg); } } catch (error) { console.error("Error parsing ROS message:", error); } }; socket.onclose = (event) => { clearTimeout(connectionTimeout); connectionAttemptRef.current = false; setRosConnected(false); setRosConnecting(false); setRosSocket(null); socketRef.current = null; setRobotStatus((prev) => ({ ...prev, connected: false, battery: 0, joints: {}, sensors: {}, })); // Only show error if it wasn't a normal closure (code 1000) if (event.code !== 1000) { let errorMsg = "Connection lost"; if (event.code === 1006) { errorMsg = "ROS Bridge not responding - check if rosbridge_server is running"; } else if (event.code === 1011) { errorMsg = "Server error in ROS Bridge"; } else if (event.code === 1002) { errorMsg = "Protocol error - check ROS Bridge version"; } else if (event.code === 1001) { errorMsg = "Server going away - ROS Bridge may have restarted"; } console.log( `🔌 Connection closed - Code: ${event.code}, Reason: ${event.reason}`, ); setRosError(`${errorMsg} (${event.code})`); } }; socket.onerror = (error) => { clearTimeout(connectionTimeout); connectionAttemptRef.current = false; console.error("ROS Bridge WebSocket error:", error); setRosConnected(false); setRosConnecting(false); setRosError( "Failed to connect to ROS bridge - check if rosbridge_server is running", ); setRobotStatus((prev) => ({ ...prev, connected: false })); }; }; const disconnectRos = () => { console.log("Manually disconnecting from ROS Bridge"); connectionAttemptRef.current = false; if (rosSocket) { // Close with normal closure code to avoid error messages rosSocket.close(1000, "User disconnected"); } if (socketRef.current) { socketRef.current.close(1000, "User disconnected"); } // Clear all state setRosSocket(null); socketRef.current = null; setRosConnected(false); setRosConnecting(false); setRosError(null); setRobotStatus({ connected: false, battery: 0, position: { x: 0, y: 0, theta: 0 }, joints: {}, sensors: {}, lastUpdate: new Date(), }); }; const executeRobotAction = ( action: string, parameters?: Record, ) => { if (!rosSocket || !rosConnected) { setRosError("Robot not connected"); return; } let message: { op: string; topic: string; type: string; msg: Record; }; switch (action) { case "say_text": const speechText = parameters?.text ?? "Hello from wizard interface!"; console.log("🔊 Preparing speech command:", speechText); message = { op: "publish", topic: "/speech", type: "std_msgs/String", msg: { data: speechText }, }; console.log( "📤 Speech message constructed:", JSON.stringify(message, null, 2), ); break; case "move_forward": case "move_backward": case "turn_left": case "turn_right": const speed = (parameters?.speed as number) || 0.1; const linear = action.includes("forward") ? speed : action.includes("backward") ? -speed : 0; const angular = action.includes("left") ? speed : action.includes("right") ? -speed : 0; message = { op: "publish", topic: "/cmd_vel", type: "geometry_msgs/Twist", msg: { linear: { x: linear, y: 0, z: 0 }, angular: { x: 0, y: 0, z: angular }, }, }; break; case "stop_movement": message = { op: "publish", topic: "/cmd_vel", type: "geometry_msgs/Twist", msg: { linear: { x: 0, y: 0, z: 0 }, angular: { x: 0, y: 0, z: 0 }, }, }; break; case "head_movement": case "turn_head": const yaw = (parameters?.yaw as number) || 0; const pitch = (parameters?.pitch as number) || 0; const headSpeed = (parameters?.speed as number) || 0.3; message = { op: "publish", topic: "/joint_angles", type: "naoqi_bridge_msgs/JointAnglesWithSpeed", msg: { joint_names: ["HeadYaw", "HeadPitch"], joint_angles: [yaw, pitch], speed: headSpeed, }, }; break; case "play_animation": const animation = (parameters?.animation as string) ?? "Hello"; message = { op: "publish", topic: "/naoqi_driver/animation", type: "std_msgs/String", msg: { data: animation }, }; break; default: setRosError(`Unknown action: ${String(action)}`); return; } try { const messageStr = JSON.stringify(message); console.log("📡 Sending to ROS Bridge:", messageStr); rosSocket.send(messageStr); console.log(`✅ Sent robot action: ${action}`, parameters); } catch (error) { console.error("❌ Failed to send command:", error); setRosError(`Failed to send command: ${String(error)}`); } }; const subscribeToTopic = (topic: string, messageType: string) => { if (!rosSocket || !rosConnected) { setRosError("Cannot subscribe - not connected"); return; } try { const subscribeMsg = { op: "subscribe", topic: topic, type: messageType, }; rosSocket.send(JSON.stringify(subscribeMsg)); console.log(`Manually subscribed to ${topic}`); } catch (error) { setRosError(`Failed to subscribe to ${topic}: ${String(error)}`); } }; // Don't close connection on unmount to prevent disconnection issues // Connection will persist across component re-renders // Removed auto-reconnect to prevent interference with manual connections const formatTimestamp = React.useCallback((timestamp: Date) => { return new Date(timestamp).toLocaleTimeString(); }, []); const getEventIcon = (eventType: string) => { switch (eventType.toLowerCase()) { case "trial_started": case "trial_resumed": return CheckCircle; case "trial_paused": case "trial_stopped": return AlertCircle; case "step_completed": case "action_completed": return CheckCircle; case "robot_action": case "robot_status": return Bot; case "wizard_action": case "wizard_intervention": return User; case "system_error": case "connection_error": return AlertCircle; default: return Activity; } }; const getEventColor = (eventType: string) => { switch (eventType.toLowerCase()) { case "trial_started": case "trial_resumed": case "step_completed": case "action_completed": return "text-green-600"; case "trial_paused": case "trial_stopped": return "text-yellow-600"; case "system_error": case "connection_error": case "trial_failed": return "text-red-600"; case "robot_action": case "robot_status": return "text-blue-600"; case "wizard_action": case "wizard_intervention": return "text-purple-600"; default: return "text-muted-foreground"; } }; return (
{/* Header */}

Monitoring

{isConnected ? ( ) : ( )} {isConnected ? "Live" : "Offline"}
{wsError && ( {wsError} )}
{/* Tabbed Content */} { if (value === "status" || value === "robot" || value === "events") { onTabChange(value); } }} className="flex min-h-0 flex-1 flex-col" >
Status Robot Events {trialEvents.length > 0 && ( {trialEvents.length} )}
{/* Status Tab */}
{/* Connection Status */}
Connection
WebSocket {isConnected ? "Connected" : "Offline"}
Data Mode {isConnected ? "Real-time" : "Polling"}
{/* Trial Information */}
Trial Info
ID {trial.id.slice(-8)}
Session #{trial.sessionNumber}
Status {trial.status.replace("_", " ")}
{trial.startedAt && (
Started {formatTimestamp(new Date(trial.startedAt))}
)}
{/* Participant Information */}
Participant
Code {trial.participant.participantCode}
Session #{trial.sessionNumber}
{trial.participant.demographics && (
Demographics {Object.keys(trial.participant.demographics).length}{" "} fields
)}
{/* System Information */}
System
Experiment {trial.experiment.name}
Study {trial.experiment.studyId.slice(-8)}
Platform HRIStudio
{/* Robot Tab */}
{/* Robot Status */}
Robot Status
{rosConnected ? ( ) : ( )}
ROS Bridge
{rosConnecting ? "Connecting..." : rosConnected ? "Ready" : rosError ? "Failed" : "Offline"} {rosConnected && ( )} {rosConnecting && ( )}
Battery
{robotStatus && robotStatus.battery > 0 ? `${Math.round(robotStatus.battery)}%` : rosConnected ? "Reading..." : "No data"} 0 ? robotStatus.battery : 0 } className="h-1 w-8" />
Position {robotStatus ? `(${robotStatus.position.x.toFixed(1)}, ${robotStatus.position.y.toFixed(1)})` : "--"}
Last Update {robotStatus ? robotStatus.lastUpdate.toLocaleTimeString() : "--"}
{/* ROS Connection Controls */}
{!rosConnected ? ( ) : ( )}
{rosError && ( {rosError} )} {/* Connection Help */} {!rosConnected && !rosConnecting && (
Troubleshooting:
1. Check ROS Bridge:{" "} telnet 134.82.159.25 9090
2. NAO6 must be awake and connected
3. Try: Click Connect → Wait 2s → Test Speech
)}
{/* Robot Actions */}
Active Actions
No active actions
{/* Recent Trial Events */}
Recent Events
{trialEvents .filter((e) => e.type.includes("robot")) .slice(-2) .map((event, index) => (
{event.type.replace(/_/g, " ")} {formatTimestamp(event.timestamp)}
))} {trialEvents.filter((e) => e.type.includes("robot")) .length === 0 && (
No robot events yet
)}
{/* Robot Configuration */}
Configuration
Type NAO6
ROS Bridge localhost:9090
Platform NAOqi
{robotStatus && Object.keys(robotStatus.joints).length > 0 && (
Joints {Object.keys(robotStatus.joints).length} active
)}
{/* Manual Subscription Controls */} {rosConnected && (
Manual Controls
{/* Connection Test */}
{/* Topic Subscriptions */}
Subscribe to Topics:
)} {/* Quick Robot Actions */} {rosConnected && (
Robot Actions
{/* Movement Controls */}
{/* Head Controls */}
{/* Animation & LED Controls */}
{/* Emergency Controls */}
)} {!rosConnected && !rosConnecting && (
Connect to ROS bridge for live robot monitoring and control
)}
{/* Events Tab */}
{trialEvents.length === 0 ? (
No events recorded yet
) : (
Live Events {trialEvents.length}
{trialEvents .slice() .reverse() .map((event, index) => { const EventIcon = getEventIcon(event.type); const eventColor = getEventColor(event.type); return (
{event.type.replace(/_/g, " ")}
{event.message && (
{event.message}
)}
{formatTimestamp(event.timestamp)}
); })}
)}
); }); export { WizardMonitoringPanel };