mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-15 08:34:44 -05:00
refactor: simplify wizard UI by removing trial monitoring and robot control tabs, and streamlining monitoring panel props.
This commit is contained in:
@@ -491,12 +491,6 @@ export const WizardInterface = React.memo(function WizardInterface({
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<WizardMonitoringPanel
|
<WizardMonitoringPanel
|
||||||
trial={trial}
|
|
||||||
trialEvents={trialEvents}
|
|
||||||
isConnected={rosConnected}
|
|
||||||
wsError={undefined}
|
|
||||||
activeTab={monitoringPanelTab}
|
|
||||||
onTabChange={setMonitoringPanelTab}
|
|
||||||
rosConnected={rosConnected}
|
rosConnected={rosConnected}
|
||||||
rosConnecting={rosConnecting}
|
rosConnecting={rosConnecting}
|
||||||
rosError={rosError ?? undefined}
|
rosError={rosError ?? undefined}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
User,
|
User,
|
||||||
Bot,
|
Bot,
|
||||||
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
@@ -173,29 +174,25 @@ export function WizardControlPanel({
|
|||||||
value === "actions" ||
|
value === "actions" ||
|
||||||
value === "robot"
|
value === "robot"
|
||||||
) {
|
) {
|
||||||
onTabChange(value as "control" | "step" | "actions" | "robot");
|
onTabChange(value as "control" | "step" | "actions");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex min-h-0 flex-1 flex-col"
|
className="flex min-h-0 flex-1 flex-col"
|
||||||
>
|
>
|
||||||
<div className="border-b px-2 py-1">
|
<div className="border-b px-2 py-1">
|
||||||
<TabsList className="grid w-full grid-cols-4">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="control" className="text-xs">
|
<TabsTrigger value="control" className="text-xs">
|
||||||
<Settings className="mr-1 h-3 w-3" />
|
<Play className="mr-1 h-3 w-3" />
|
||||||
Control
|
Control
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="step" className="text-xs">
|
<TabsTrigger value="step" className="text-xs">
|
||||||
<Play className="mr-1 h-3 w-3" />
|
<Eye className="mr-1 h-3 w-3" />
|
||||||
Step
|
Step
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="actions" className="text-xs">
|
<TabsTrigger value="actions" className="text-xs">
|
||||||
<Zap className="mr-1 h-3 w-3" />
|
<Zap className="mr-1 h-3 w-3" />
|
||||||
Actions
|
Actions
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="robot" className="text-xs">
|
|
||||||
<Bot className="mr-1 h-3 w-3" />
|
|
||||||
Robot
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useRef } from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
User,
|
|
||||||
Activity,
|
|
||||||
Wifi,
|
|
||||||
WifiOff,
|
|
||||||
AlertCircle,
|
|
||||||
CheckCircle,
|
|
||||||
Clock,
|
|
||||||
Power,
|
Power,
|
||||||
PowerOff,
|
PowerOff,
|
||||||
Eye,
|
AlertCircle,
|
||||||
Volume2,
|
|
||||||
Move,
|
|
||||||
Hand,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
import { Alert, AlertDescription } from "~/components/ui/alert";
|
import { Alert, AlertDescription } from "~/components/ui/alert";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
|
||||||
import { Progress } from "~/components/ui/progress";
|
import { Progress } from "~/components/ui/progress";
|
||||||
import { Button } from "~/components/ui/button";
|
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<string, unknown> | null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrialEvent {
|
|
||||||
type: string;
|
|
||||||
timestamp: Date;
|
|
||||||
data?: unknown;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WizardMonitoringPanelProps {
|
interface WizardMonitoringPanelProps {
|
||||||
trial: TrialData;
|
|
||||||
trialEvents: TrialEvent[];
|
|
||||||
isConnected: boolean;
|
|
||||||
wsError?: string;
|
|
||||||
activeTab: "status" | "robot" | "events";
|
|
||||||
onTabChange: (tab: "status" | "robot" | "events") => void;
|
|
||||||
// ROS connection props
|
|
||||||
rosConnected: boolean;
|
rosConnected: boolean;
|
||||||
rosConnecting: boolean;
|
rosConnecting: boolean;
|
||||||
rosError?: string;
|
rosError?: string;
|
||||||
@@ -85,13 +35,7 @@ interface WizardMonitoringPanelProps {
|
|||||||
) => Promise<unknown>;
|
) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
|
const WizardMonitoringPanel = function WizardMonitoringPanel({
|
||||||
trial,
|
|
||||||
trialEvents,
|
|
||||||
isConnected,
|
|
||||||
wsError,
|
|
||||||
activeTab,
|
|
||||||
onTabChange,
|
|
||||||
rosConnected,
|
rosConnected,
|
||||||
rosConnecting,
|
rosConnecting,
|
||||||
rosError,
|
rosError,
|
||||||
@@ -100,271 +44,15 @@ const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
|
|||||||
disconnectRos,
|
disconnectRos,
|
||||||
executeRosAction,
|
executeRosAction,
|
||||||
}: WizardMonitoringPanelProps) {
|
}: WizardMonitoringPanelProps) {
|
||||||
// ROS connection is now passed as props, no need for separate hook
|
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b p-3">
|
<div className="flex items-center justify-between border-b p-3">
|
||||||
<div className="flex items-center justify-between">
|
<h2 className="text-sm font-semibold">Robot Control</h2>
|
||||||
<h3 className="text-sm font-medium">Monitoring</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isConnected ? (
|
|
||||||
<Wifi className="h-4 w-4 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<WifiOff className="h-4 w-4 text-orange-600" />
|
|
||||||
)}
|
|
||||||
<Badge
|
|
||||||
variant={isConnected ? "default" : "secondary"}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{isConnected ? "Live" : "Offline"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{wsError && (
|
|
||||||
<Alert variant="destructive" className="mt-2">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription className="text-xs">{wsError}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabbed Content */}
|
{/* Robot Status and Controls */}
|
||||||
<Tabs
|
<ScrollArea className="flex-1">
|
||||||
value={activeTab}
|
|
||||||
onValueChange={(value: string) => {
|
|
||||||
if (value === "status" || value === "robot" || value === "events") {
|
|
||||||
onTabChange(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex min-h-0 flex-1 flex-col"
|
|
||||||
>
|
|
||||||
<div className="border-b px-2 py-1">
|
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
|
||||||
<TabsTrigger value="status" className="text-xs">
|
|
||||||
<Eye className="mr-1 h-3 w-3" />
|
|
||||||
Status
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="robot" className="text-xs">
|
|
||||||
<Bot className="mr-1 h-3 w-3" />
|
|
||||||
Robot
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="events" className="text-xs">
|
|
||||||
<Activity className="mr-1 h-3 w-3" />
|
|
||||||
Events
|
|
||||||
{trialEvents.length > 0 && (
|
|
||||||
<Badge variant="secondary" className="ml-1 text-xs">
|
|
||||||
{trialEvents.length}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-h-0 flex-1">
|
|
||||||
{/* Status Tab */}
|
|
||||||
<TabsContent value="status" className="m-0 h-full">
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
<div className="space-y-4 p-3">
|
|
||||||
{/* Connection Status */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Connection</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
ROS Bridge
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
variant={isConnected ? "default" : "secondary"}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{isConnected ? "Connected" : "Offline"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Data Mode
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{isConnected ? "Real-time" : "Polling"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Trial Information */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Trial Info</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">ID</span>
|
|
||||||
<span className="font-mono text-xs">
|
|
||||||
{trial.id.slice(-8)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Session
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">#{trial.sessionNumber}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Status
|
|
||||||
</span>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{trial.status.replace("_", " ")}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{trial.startedAt && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Started
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{formatTimestamp(new Date(trial.startedAt))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Participant Information */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Participant</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Code
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-xs">
|
|
||||||
{trial.participant.participantCode}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Session
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">#{trial.sessionNumber}</span>
|
|
||||||
</div>
|
|
||||||
{trial.participant.demographics && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Demographics
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{Object.keys(trial.participant.demographics).length}{" "}
|
|
||||||
fields
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* System Information */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">System</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Experiment
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="max-w-24 truncate text-xs"
|
|
||||||
title={trial.experiment.name}
|
|
||||||
>
|
|
||||||
{trial.experiment.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Study
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-xs">
|
|
||||||
{trial.experiment.studyId.slice(-8)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Platform
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">HRIStudio</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Robot Tab */}
|
|
||||||
<TabsContent value="robot" className="m-0 h-full">
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
<div className="space-y-4 p-3">
|
<div className="space-y-4 p-3">
|
||||||
{/* Robot Status */}
|
{/* Robot Status */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -414,48 +102,6 @@ const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Battery
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="text-xs">
|
|
||||||
{robotStatus && robotStatus.battery > 0
|
|
||||||
? `${Math.round(robotStatus.battery)}%`
|
|
||||||
: rosConnected
|
|
||||||
? "Reading..."
|
|
||||||
: "No data"}
|
|
||||||
</span>
|
|
||||||
<Progress
|
|
||||||
value={
|
|
||||||
robotStatus && robotStatus.battery > 0
|
|
||||||
? robotStatus.battery
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
className="h-1 w-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Position
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{robotStatus
|
|
||||||
? `(${robotStatus.position.x.toFixed(1)}, ${robotStatus.position.y.toFixed(1)})`
|
|
||||||
: "--"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Last Update
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{robotStatus
|
|
||||||
? robotStatus.lastUpdate.toLocaleTimeString()
|
|
||||||
: "--"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ROS Connection Controls */}
|
{/* ROS Connection Controls */}
|
||||||
@@ -497,256 +143,163 @@ const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Connection Help */}
|
|
||||||
{!rosConnected && !rosConnecting && (
|
{!rosConnected && !rosConnecting && (
|
||||||
<Alert className="mt-2">
|
<div className="mt-4">
|
||||||
|
<Alert>
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertDescription className="text-xs">
|
<AlertDescription className="text-xs">
|
||||||
<div className="space-y-1">
|
Connect to ROS bridge for live robot monitoring and
|
||||||
<div className="font-medium">Troubleshooting:</div>
|
control.
|
||||||
<div>
|
|
||||||
1. Check ROS Bridge:{" "}
|
|
||||||
<code className="bg-muted rounded px-1 text-xs">
|
|
||||||
telnet localhost 9090
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<div>2. NAO6 must be awake and connected</div>
|
|
||||||
<div>
|
|
||||||
3. Try: Click Connect → Wait 2s → Test Speech
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Robot Actions */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Active Actions</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground text-center text-xs">
|
|
||||||
No active actions
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Recent Trial Events */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Recent Events</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{trialEvents
|
|
||||||
.filter((e) => e.type.includes("robot"))
|
|
||||||
.slice(-2)
|
|
||||||
.map((event, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="border-border/50 flex items-center justify-between rounded border p-2"
|
|
||||||
>
|
|
||||||
<span className="text-xs font-medium">
|
|
||||||
{event.type.replace(/_/g, " ")}
|
|
||||||
</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{formatTimestamp(event.timestamp)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{trialEvents.filter((e) => e.type.includes("robot"))
|
|
||||||
.length === 0 && (
|
|
||||||
<div className="text-muted-foreground py-2 text-center text-xs">
|
|
||||||
No robot events yet
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{/* Robot Configuration */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Configuration</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Type
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">NAO6</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
ROS Bridge
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-xs">localhost:9090</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Platform
|
|
||||||
</span>
|
|
||||||
<span className="font-mono text-xs">NAOqi</span>
|
|
||||||
</div>
|
|
||||||
{robotStatus &&
|
|
||||||
Object.keys(robotStatus.joints).length > 0 && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
Joints
|
|
||||||
</span>
|
|
||||||
<span className="text-xs">
|
|
||||||
{Object.keys(robotStatus.joints).length} active
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Manual Subscription Controls */}
|
|
||||||
{rosConnected && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Manual Controls</div>
|
|
||||||
|
|
||||||
{/* Connection Test */}
|
|
||||||
<div className="grid grid-cols-1 gap-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "say_text", {
|
|
||||||
text: "Connection test - can you hear me?",
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!rosConnected}
|
|
||||||
>
|
|
||||||
<Volume2 className="mr-1 h-3 w-3" />
|
|
||||||
Test Speech
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Topic Subscriptions */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-muted-foreground text-xs font-medium">
|
|
||||||
Subscribe to Topics:
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 gap-1">
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
Subscriptions managed automatically
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Quick Robot Actions */}
|
|
||||||
{rosConnected && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium">Robot Actions</div>
|
|
||||||
|
|
||||||
{/* Movement Controls */}
|
{/* Movement Controls */}
|
||||||
<div className="grid grid-cols-3 gap-1">
|
{rosConnected && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Movement</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{/* Row 1: Turn Left, Forward, Turn Right */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "walk_forward", {
|
|
||||||
speed: 0.05,
|
|
||||||
duration: 2,
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Forward
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "turn_left", {
|
executeRosAction("nao6-ros2", "turn_left", {
|
||||||
speed: 0.3,
|
speed: 0.3,
|
||||||
duration: 2,
|
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Turn Left
|
↺ Turn L
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
executeRosAction("nao6-ros2", "walk_forward", {
|
||||||
|
speed: 0.5,
|
||||||
|
}).catch(console.error);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
↑ Forward
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "turn_right", {
|
executeRosAction("nao6-ros2", "turn_right", {
|
||||||
speed: 0.3,
|
speed: 0.3,
|
||||||
duration: 2,
|
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Turn R ↻
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Row 2: Left, Stop, Right */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
executeRosAction("nao6-ros2", "strafe_left", {
|
||||||
|
speed: 0.3,
|
||||||
|
}).catch(console.error);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Left
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
executeRosAction("nao6-ros2", "emergency_stop", {}).catch(
|
||||||
|
console.error,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
■ Stop
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
executeRosAction("nao6-ros2", "strafe_right", {
|
||||||
|
speed: 0.3,
|
||||||
|
}).catch(console.error);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Right →
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Row 3: Empty, Back, Empty */}
|
||||||
|
<div></div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
executeRosAction("nao6-ros2", "walk_backward", {
|
||||||
|
speed: 0.3,
|
||||||
|
}).catch(console.error);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
↓ Back
|
||||||
|
</Button>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
{rosConnected && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium">Quick Actions</div>
|
||||||
|
|
||||||
|
{/* TTS Input */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type text to speak..."
|
||||||
|
className="flex-1 rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && e.currentTarget.value.trim()) {
|
||||||
|
executeRosAction("nao6-ros2", "say_text", {
|
||||||
|
text: e.currentTarget.value.trim(),
|
||||||
|
}).catch(console.error);
|
||||||
|
e.currentTarget.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
onClick={(e) => {
|
||||||
|
const input = e.currentTarget.previousElementSibling as HTMLInputElement;
|
||||||
|
if (input?.value.trim()) {
|
||||||
|
executeRosAction("nao6-ros2", "say_text", {
|
||||||
|
text: input.value.trim(),
|
||||||
|
}).catch(console.error);
|
||||||
|
input.value = "";
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Turn Right
|
Say
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Head Controls */}
|
{/* Preset Actions */}
|
||||||
<div className="grid grid-cols-3 gap-1">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "turn_head", {
|
|
||||||
yaw: 0,
|
|
||||||
pitch: 0,
|
|
||||||
speed: 0.3,
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Center Head
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "turn_head", {
|
|
||||||
yaw: 0.5,
|
|
||||||
pitch: 0,
|
|
||||||
speed: 0.3,
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Look Left
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction("nao6-ros2", "turn_head", {
|
|
||||||
yaw: -0.5,
|
|
||||||
pitch: 0,
|
|
||||||
speed: 0.3,
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Look Right
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Animation & LED Controls */}
|
|
||||||
<div className="grid grid-cols-2 gap-1">
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -768,7 +321,7 @@ const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (rosConnected) {
|
if (rosConnected) {
|
||||||
executeRosAction("nao6-ros2", "say_text", {
|
executeRosAction("nao6-ros2", "say_text", {
|
||||||
text: "Experiment ready!",
|
text: "I am ready!",
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -776,102 +329,12 @@ const WizardMonitoringPanel = React.memo(function WizardMonitoringPanel({
|
|||||||
Say Ready
|
Say Ready
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Emergency Controls */}
|
|
||||||
<div className="grid grid-cols-1 gap-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
className="text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
if (rosConnected) {
|
|
||||||
executeRosAction(
|
|
||||||
"nao6-ros2",
|
|
||||||
"emergency_stop",
|
|
||||||
{},
|
|
||||||
).catch(console.error);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🛑 Emergency Stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!rosConnected && !rosConnecting && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<Alert>
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription className="text-xs">
|
|
||||||
Connect to ROS bridge for live robot monitoring and
|
|
||||||
control
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Events Tab */}
|
|
||||||
<TabsContent value="events" className="m-0 h-full">
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
<div className="p-3">
|
|
||||||
{trialEvents.length === 0 ? (
|
|
||||||
<div className="text-muted-foreground flex h-32 items-center justify-center text-center text-sm">
|
|
||||||
No events recorded yet
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium">Live Events</span>
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{trialEvents.length}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{trialEvents
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((event, index) => {
|
|
||||||
const EventIcon = getEventIcon(event.type);
|
|
||||||
const eventColor = getEventColor(event.type);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${event.timestamp.getTime()}-${index}`}
|
|
||||||
className="border-border/50 flex items-start gap-2 rounded-lg border p-2"
|
|
||||||
>
|
|
||||||
<div className={`mt-0.5 ${eventColor}`}>
|
|
||||||
<EventIcon className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="text-sm font-medium capitalize">
|
|
||||||
{event.type.replace(/_/g, " ")}
|
|
||||||
</div>
|
|
||||||
{event.message && (
|
|
||||||
<div className="text-muted-foreground mt-1 text-xs">
|
|
||||||
{event.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
{formatTimestamp(event.timestamp)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export { WizardMonitoringPanel };
|
export { WizardMonitoringPanel };
|
||||||
|
|||||||
@@ -107,12 +107,15 @@ export function useWizardRos(
|
|||||||
if (!service) return;
|
if (!service) return;
|
||||||
|
|
||||||
const handleConnected = () => {
|
const handleConnected = () => {
|
||||||
if (!mountedRef.current) return;
|
console.log("[useWizardRos] handleConnected called, mountedRef:", mountedRef.current);
|
||||||
console.log("[useWizardRos] Connected to ROS bridge");
|
// Set state immediately, before checking mounted status
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
setConnectionError(null);
|
setConnectionError(null);
|
||||||
|
|
||||||
|
if (mountedRef.current) {
|
||||||
onConnectedRef.current?.();
|
onConnectedRef.current?.();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisconnected = () => {
|
const handleDisconnected = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user