Pre-conf work 2025

This commit is contained in:
2025-09-02 08:25:41 -04:00
parent 550021a18e
commit 4acbec6288
75 changed files with 8047 additions and 5228 deletions

View File

@@ -268,7 +268,7 @@ function TrialCard({ trial, userRole, onTrialAction }: TrialCardProps) {
}
export function TrialsGrid() {
const [refreshKey, setRefreshKey] = useState(0);
const [statusFilter, setStatusFilter] = useState<string>("all");
const { data: userSession } = api.auth.me.useQuery();
@@ -282,7 +282,15 @@ export function TrialsGrid() {
{
page: 1,
limit: 50,
status: statusFilter === "all" ? undefined : (statusFilter as any),
status:
statusFilter === "all"
? undefined
: (statusFilter as
| "scheduled"
| "in_progress"
| "completed"
| "aborted"
| "failed"),
},
{
refetchOnWindowFocus: false,
@@ -309,16 +317,13 @@ export function TrialsGrid() {
}
};
const handleTrialCreated = () => {
setRefreshKey((prev) => prev + 1);
void refetch();
};
// Group trials by status for better organization
const upcomingTrials = trials.filter((t) => t.status === "scheduled");
const activeTrials = trials.filter((t) => t.status === "in_progress");
const completedTrials = trials.filter((t) => t.status === "completed");
const cancelledTrials = trials.filter((t) => t.status === "aborted");
if (isLoading) {
return (

View File

@@ -2,20 +2,35 @@
import { format, formatDistanceToNow } from "date-fns";
import {
Activity, AlertTriangle, ArrowRight, Bot, Camera, CheckCircle, Eye, Hand, MessageSquare, Pause, Play, Settings, User, Volume2, XCircle
Activity,
AlertTriangle,
ArrowRight,
Bot,
Camera,
CheckCircle,
Eye,
Hand,
MessageSquare,
Pause,
Play,
Settings,
User,
Volume2,
XCircle,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { ScrollArea } from "~/components/ui/scroll-area";
import { api } from "~/trpc/react";
import type { WebSocketMessage } from "~/hooks/useWebSocket";
interface EventsLogProps {
trialId: string;
refreshKey: number;
isLive: boolean;
maxEvents?: number;
realtimeEvents?: any[];
realtimeEvents?: WebSocketMessage[];
isWebSocketConnected?: boolean;
}
@@ -24,7 +39,7 @@ interface TrialEvent {
trialId: string;
eventType: string;
timestamp: Date;
data: any;
data: Record<string, unknown> | null;
notes: string | null;
createdAt: Date;
}
@@ -177,7 +192,17 @@ export function EventsLog({
{
trialId,
limit: maxEvents,
type: filter === "all" ? undefined : filter as "error" | "custom" | "trial_start" | "trial_end" | "step_start" | "step_end" | "wizard_intervention",
type:
filter === "all"
? undefined
: (filter as
| "error"
| "custom"
| "trial_start"
| "trial_end"
| "step_start"
| "step_end"
| "wizard_intervention"),
},
{
refetchInterval: isLive && !isWebSocketConnected ? 2000 : 10000, // Less frequent polling when WebSocket is active
@@ -186,23 +211,48 @@ export function EventsLog({
},
);
// Convert WebSocket events to trial events format
const convertWebSocketEvent = (wsEvent: any): TrialEvent => ({
id: `ws-${Date.now()}-${Math.random()}`,
trialId,
eventType:
wsEvent.type === "trial_action_executed"
? "wizard_action"
: wsEvent.type === "intervention_logged"
? "wizard_intervention"
: wsEvent.type === "step_changed"
? "step_transition"
: wsEvent.type || "system_event",
timestamp: new Date(wsEvent.data?.timestamp || Date.now()),
data: wsEvent.data || {},
notes: wsEvent.data?.notes || null,
createdAt: new Date(wsEvent.data?.timestamp || Date.now()),
});
// Convert WebSocket events to trial events format (type-safe)
const convertWebSocketEvent = useCallback(
(wsEvent: WebSocketMessage): TrialEvent => {
const eventType =
wsEvent.type === "trial_action_executed"
? "wizard_action"
: wsEvent.type === "intervention_logged"
? "wizard_intervention"
: wsEvent.type === "step_changed"
? "step_transition"
: wsEvent.type || "system_event";
const rawData = wsEvent.data;
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null;
const data: Record<string, unknown> | null = isRecord(rawData)
? rawData
: null;
const ts =
isRecord(rawData) && typeof rawData.timestamp === "number"
? rawData.timestamp
: Date.now();
const notes =
isRecord(rawData) && typeof rawData.notes === "string"
? rawData.notes
: null;
return {
id: `ws-${Date.now()}-${Math.random()}`,
trialId,
eventType,
timestamp: new Date(ts),
data,
notes,
createdAt: new Date(ts),
};
},
[trialId],
);
// Update events when data changes (prioritize WebSocket events)
useEffect(() => {
@@ -210,11 +260,26 @@ export function EventsLog({
// Add database events
if (eventsData) {
newEvents = eventsData.map((event) => ({
...event,
type ApiTrialEvent = {
id: string;
trialId: string;
eventType: string;
timestamp: string | Date;
data: unknown;
};
const apiEvents = (eventsData as unknown as ApiTrialEvent[]) ?? [];
newEvents = apiEvents.map((event) => ({
id: event.id,
trialId: event.trialId,
eventType: event.eventType,
timestamp: new Date(event.timestamp),
data:
typeof event.data === "object" && event.data !== null
? (event.data as Record<string, unknown>)
: null,
notes: null,
createdAt: new Date(event.timestamp),
notes: null, // Add required field
}));
}
@@ -240,7 +305,14 @@ export function EventsLog({
.slice(-maxEvents); // Keep only the most recent events
setEvents(uniqueEvents);
}, [eventsData, refreshKey, realtimeEvents, trialId, maxEvents]);
}, [
eventsData,
refreshKey,
realtimeEvents,
trialId,
maxEvents,
convertWebSocketEvent,
]);
// Auto-scroll to bottom when new events arrive
useEffect(() => {
@@ -256,41 +328,87 @@ export function EventsLog({
);
};
const formatEventData = (eventType: string, data: any) => {
const formatEventData = (
eventType: string,
data: Record<string, unknown> | null,
): string | null => {
if (!data) return null;
const str = (k: string): string | undefined => {
const v = data[k];
return typeof v === "string" ? v : undefined;
};
const num = (k: string): number | undefined => {
const v = data[k];
return typeof v === "number" ? v : undefined;
};
switch (eventType) {
case "step_transition":
return `Step ${data.from_step + 1} → Step ${data.to_step + 1}${data.step_name ? `: ${data.step_name}` : ""}`;
case "step_transition": {
const fromIdx = num("from_step");
const toIdx = num("to_step");
const stepName = str("step_name");
if (typeof toIdx === "number") {
const fromLabel =
typeof fromIdx === "number" ? `${fromIdx + 1}` : "";
const nameLabel = stepName ? `: ${stepName}` : "";
return `Step ${fromLabel}${toIdx + 1}${nameLabel}`;
}
return "Step changed";
}
case "wizard_action":
return `${data.action_type ? data.action_type.replace(/_/g, " ") : "Action executed"}${data.step_name ? ` in ${data.step_name}` : ""}`;
case "wizard_action": {
const actionType = str("action_type");
const stepName = str("step_name");
const actionLabel = actionType
? actionType.replace(/_/g, " ")
: "Action executed";
const inStep = stepName ? ` in ${stepName}` : "";
return `${actionLabel}${inStep}`;
}
case "robot_action":
return `${data.action_name || "Robot action"}${data.parameters ? ` with parameters` : ""}`;
case "robot_action": {
const actionName = str("action_name") ?? "Robot action";
const hasParams =
typeof data.parameters !== "undefined" && data.parameters !== null;
return `${actionName}${hasParams ? " with parameters" : ""}`;
}
case "emergency_action":
return `Emergency: ${data.emergency_type ? data.emergency_type.replace(/_/g, " ") : "Unknown"}`;
case "emergency_action": {
const emergency = str("emergency_type");
return `Emergency: ${
emergency ? emergency.replace(/_/g, " ") : "Unknown"
}`;
}
case "recording_control":
return `Recording ${data.action === "start_recording" ? "started" : "stopped"}`;
case "recording_control": {
const action = str("action");
return `Recording ${action === "start_recording" ? "started" : "stopped"}`;
}
case "video_control":
return `Video ${data.action === "video_on" ? "enabled" : "disabled"}`;
case "video_control": {
const action = str("action");
return `Video ${action === "video_on" ? "enabled" : "disabled"}`;
}
case "audio_control":
return `Audio ${data.action === "audio_on" ? "enabled" : "disabled"}`;
case "audio_control": {
const action = str("action");
return `Audio ${action === "audio_on" ? "enabled" : "disabled"}`;
}
case "wizard_intervention":
case "wizard_intervention": {
return (
data.content || data.intervention_type || "Intervention recorded"
str("content") ?? str("intervention_type") ?? "Intervention recorded"
);
}
default:
if (typeof data === "string") return data;
if (data.message) return data.message;
if (data.description) return data.description;
default: {
const message = str("message");
if (message) return message;
const description = str("description");
if (description) return description;
return null;
}
}
};
@@ -305,7 +423,8 @@ export function EventsLog({
if (
index === 0 ||
Math.abs(
event.timestamp.getTime() - (events[index - 1]?.timestamp.getTime() ?? 0),
event.timestamp.getTime() -
(events[index - 1]?.timestamp.getTime() ?? 0),
) > 30000
) {
groups.push([event]);
@@ -317,7 +436,7 @@ export function EventsLog({
[],
);
const uniqueEventTypes = Array.from(new Set(events.map((e) => e.eventType)));
// uniqueEventTypes removed (unused)
if (isLoading) {
return (
@@ -433,9 +552,11 @@ export function EventsLog({
</div>
<div className="h-px flex-1 bg-slate-200"></div>
<div className="text-xs text-slate-400">
{group[0] ? formatDistanceToNow(group[0].timestamp, {
addSuffix: true,
}) : ""}
{group[0]
? formatDistanceToNow(group[0].timestamp, {
addSuffix: true,
})
: ""}
</div>
</div>
@@ -503,20 +624,22 @@ export function EventsLog({
{event.notes && (
<p className="mt-1 text-xs text-slate-500 italic">
"{event.notes}"
{event.notes}
</p>
)}
{event.data && Object.keys(event.data).length > 0 && (
<details className="mt-2">
<summary className="cursor-pointer text-xs text-blue-600 hover:text-blue-800">
View details
</summary>
<pre className="mt-1 overflow-x-auto rounded border bg-white p-2 text-xs text-slate-600">
{JSON.stringify(event.data, null, 2)}
</pre>
</details>
)}
{event.data &&
typeof event.data === "object" &&
Object.keys(event.data).length > 0 && (
<details className="mt-2">
<summary className="cursor-pointer text-xs text-blue-600 hover:text-blue-800">
View details
</summary>
<pre className="mt-1 overflow-x-auto rounded border bg-white p-2 text-xs text-slate-600">
{JSON.stringify(event.data, null, 2)}
</pre>
</details>
)}
</div>
<div className="flex-shrink-0 text-xs text-slate-400">

View File

@@ -17,6 +17,7 @@ import {
User,
} from "lucide-react";
import Link from "next/link";
import { api } from "~/trpc/react";
import { toast } from "sonner";
import { Badge } from "~/components/ui/badge";
@@ -106,13 +107,19 @@ const statusConfig = {
};
function TrialActionsCell({ trial }: { trial: Trial }) {
const startTrialMutation = api.trials.start.useMutation();
const completeTrialMutation = api.trials.complete.useMutation();
const abortTrialMutation = api.trials.abort.useMutation();
// const deleteTrialMutation = api.trials.delete.useMutation();
const handleDelete = async () => {
if (
window.confirm(`Are you sure you want to delete trial "${trial.name}"?`)
) {
try {
// Delete trial functionality not yet implemented
toast.success("Trial deleted successfully");
// await deleteTrialMutation.mutateAsync({ id: trial.id });
toast.success("Trial deletion not yet implemented");
// window.location.reload();
} catch {
toast.error("Failed to delete trial");
}
@@ -124,14 +131,22 @@ function TrialActionsCell({ trial }: { trial: Trial }) {
toast.success("Trial ID copied to clipboard");
};
const handleStartTrial = () => {
window.location.href = `/trials/${trial.id}/wizard`;
const handleStartTrial = async () => {
try {
await startTrialMutation.mutateAsync({ id: trial.id });
toast.success("Trial started successfully");
window.location.href = `/trials/${trial.id}/wizard`;
} catch {
toast.error("Failed to start trial");
}
};
const handlePauseTrial = async () => {
try {
// Pause trial functionality not yet implemented
toast.success("Trial paused");
// For now, pausing means completing the trial
await completeTrialMutation.mutateAsync({ id: trial.id });
toast.success("Trial paused/completed");
window.location.reload();
} catch {
toast.error("Failed to pause trial");
}
@@ -140,8 +155,9 @@ function TrialActionsCell({ trial }: { trial: Trial }) {
const handleStopTrial = async () => {
if (window.confirm("Are you sure you want to stop this trial?")) {
try {
// Stop trial functionality not yet implemented
await abortTrialMutation.mutateAsync({ id: trial.id });
toast.success("Trial stopped");
window.location.reload();
} catch {
toast.error("Failed to stop trial");
}

View File

@@ -180,12 +180,18 @@ export function TrialsDataTable() {
<div className="space-y-6">
<PageHeader
title="Trials"
description="Monitor and manage trial execution for your HRI experiments"
description="Schedule and manage trials for your HRI studies"
icon={TestTube}
actions={
<ActionButton href="/trials/new">
<ActionButton
href={
selectedStudyId
? `/studies/${selectedStudyId}/trials/new`
: "/trials/new"
}
>
<Plus className="mr-2 h-4 w-4" />
New Trial
Schedule Trial
</ActionButton>
}
/>
@@ -210,12 +216,18 @@ export function TrialsDataTable() {
<div className="space-y-6">
<PageHeader
title="Trials"
description="Monitor and manage trial execution for your HRI experiments"
description="Schedule and manage trials for your HRI studies"
icon={TestTube}
actions={
<ActionButton href="/trials/new">
<ActionButton
href={
selectedStudyId
? `/studies/${selectedStudyId}/trials/new`
: "/trials/new"
}
>
<Plus className="mr-2 h-4 w-4" />
New Trial
Schedule Trial
</ActionButton>
}
/>

View File

@@ -1,43 +1,62 @@
"use client";
import {
AlertTriangle, Camera, Clock, Hand, HelpCircle, Lightbulb, MessageSquare, Pause,
Play,
RotateCcw, Target, Video,
VideoOff, Volume2,
VolumeX, Zap
AlertTriangle,
Camera,
Clock,
Hand,
HelpCircle,
Lightbulb,
MessageSquare,
Pause,
RotateCcw,
Target,
Video,
VideoOff,
Volume2,
VolumeX,
Zap,
} from "lucide-react";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { Label } from "~/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Textarea } from "~/components/ui/textarea";
interface ActionControlsProps {
trialId: string;
currentStep: {
id: string;
name: string;
type: "wizard_action" | "robot_action" | "parallel_steps" | "conditional_branch";
parameters?: any;
actions?: any[];
type:
| "wizard_action"
| "robot_action"
| "parallel_steps"
| "conditional_branch";
description?: string;
parameters?: Record<string, unknown>;
duration?: number;
} | null;
onExecuteAction: (actionType: string, actionData: any) => Promise<void>;
trialId: string;
onActionComplete: (
actionId: string,
actionData: Record<string, unknown>,
) => void;
isConnected: boolean;
}
interface QuickAction {
@@ -50,7 +69,12 @@ interface QuickAction {
requiresConfirmation?: boolean;
}
export function ActionControls({ currentStep, onExecuteAction, trialId }: ActionControlsProps) {
export function ActionControls({
trialId: _trialId,
currentStep,
onActionComplete,
isConnected: _isConnected,
}: ActionControlsProps) {
const [isRecording, setIsRecording] = useState(false);
const [isVideoOn, setIsVideoOn] = useState(true);
const [isAudioOn, setIsAudioOn] = useState(true);
@@ -119,82 +143,71 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
{ value: "cut_power", label: "Emergency Power Cut" },
];
const handleQuickAction = async (action: QuickAction) => {
const handleQuickAction = (action: QuickAction) => {
if (action.requiresConfirmation) {
setShowEmergencyDialog(true);
return;
}
try {
await onExecuteAction(action.action, {
action_id: action.id,
step_id: currentStep?.id,
timestamp: new Date().toISOString(),
});
} catch (_error) {
console.error(`Failed to execute ${action.action}:`, _error);
}
onActionComplete(action.id, {
action_type: action.action,
notes: action.description,
timestamp: new Date().toISOString(),
});
};
const handleEmergencyAction = async () => {
const handleEmergencyAction = () => {
if (!selectedEmergencyAction) return;
try {
await onExecuteAction("emergency_action", {
emergency_type: selectedEmergencyAction,
step_id: currentStep?.id,
timestamp: new Date().toISOString(),
severity: "high",
});
setShowEmergencyDialog(false);
setSelectedEmergencyAction("");
} catch (_error) {
console.error("Failed to execute emergency action:", _error);
}
onActionComplete("emergency_action", {
emergency_type: selectedEmergencyAction,
notes: interventionNote || "Emergency action executed",
timestamp: new Date().toISOString(),
});
setShowEmergencyDialog(false);
setSelectedEmergencyAction("");
setInterventionNote("");
};
const handleInterventionSubmit = async () => {
const handleInterventionSubmit = () => {
if (!interventionNote.trim()) return;
try {
await onExecuteAction("wizard_intervention", {
intervention_type: "note",
content: interventionNote,
step_id: currentStep?.id,
timestamp: new Date().toISOString(),
});
setInterventionNote("");
setIsCommunicationOpen(false);
} catch (_error) {
console.error("Failed to submit intervention:", _error);
}
onActionComplete("wizard_intervention", {
intervention_type: "note",
content: interventionNote,
timestamp: new Date().toISOString(),
});
setInterventionNote("");
setIsCommunicationOpen(false);
};
const toggleRecording = async () => {
const toggleRecording = () => {
const newState = !isRecording;
setIsRecording(newState);
await onExecuteAction("recording_control", {
onActionComplete("recording_control", {
action: newState ? "start_recording" : "stop_recording",
timestamp: new Date().toISOString(),
});
};
const toggleVideo = async () => {
const toggleVideo = () => {
const newState = !isVideoOn;
setIsVideoOn(newState);
await onExecuteAction("video_control", {
onActionComplete("video_control", {
action: newState ? "video_on" : "video_off",
timestamp: new Date().toISOString(),
});
};
const toggleAudio = async () => {
const toggleAudio = () => {
const newState = !isAudioOn;
setIsAudioOn(newState);
await onExecuteAction("audio_control", {
onActionComplete("audio_control", {
action: newState ? "audio_on" : "audio_off",
timestamp: new Date().toISOString(),
});
@@ -217,7 +230,9 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
onClick={toggleRecording}
className="flex items-center space-x-2"
>
<div className={`w-2 h-2 rounded-full ${isRecording ? "bg-white animate-pulse" : "bg-red-500"}`}></div>
<div
className={`h-2 w-2 rounded-full ${isRecording ? "animate-pulse" : ""}`}
></div>
<span>{isRecording ? "Stop Recording" : "Start Recording"}</span>
</Button>
@@ -226,7 +241,11 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
onClick={toggleVideo}
className="flex items-center space-x-2"
>
{isVideoOn ? <Video className="h-4 w-4" /> : <VideoOff className="h-4 w-4" />}
{isVideoOn ? (
<Video className="h-4 w-4" />
) : (
<VideoOff className="h-4 w-4" />
)}
<span>Video</span>
</Button>
@@ -235,7 +254,11 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
onClick={toggleAudio}
className="flex items-center space-x-2"
>
{isAudioOn ? <Volume2 className="h-4 w-4" /> : <VolumeX className="h-4 w-4" />}
{isAudioOn ? (
<Volume2 className="h-4 w-4" />
) : (
<VolumeX className="h-4 w-4" />
)}
<span>Audio</span>
</Button>
@@ -265,15 +288,18 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
<Button
key={action.id}
variant={
action.type === "emergency" ? "destructive" :
action.type === "primary" ? "default" : "outline"
action.type === "emergency"
? "destructive"
: action.type === "primary"
? "default"
: "outline"
}
onClick={() => handleQuickAction(action)}
className="flex items-center justify-start space-x-3 h-12"
className="flex h-12 items-center justify-start space-x-3"
>
<action.icon className="h-4 w-4 flex-shrink-0" />
<div className="flex-1 text-left">
<div className="font-medium">{action.label}</div>
<h4 className="font-medium">{action.label}</h4>
<div className="text-xs opacity-75">{action.description}</div>
</div>
</Button>
@@ -293,29 +319,14 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="text-sm text-slate-600">
Current step: <span className="font-medium">{currentStep.name}</span>
<div className="text-muted-foreground text-sm">
Current step:{" "}
<span className="font-medium">{currentStep.name}</span>
</div>
{currentStep.actions && currentStep.actions.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Available Actions:</Label>
<div className="grid gap-2">
{currentStep.actions.map((action: any, index: number) => (
<Button
key={action.id || index}
variant="outline"
size="sm"
onClick={() => onExecuteAction(`step_action_${action.id}`, action)}
className="justify-start text-left"
>
<Play className="h-3 w-3 mr-2" />
{action.name}
</Button>
))}
</div>
</div>
)}
<div className="text-muted-foreground text-xs">
Use the controls below to execute wizard actions for this step.
</div>
</div>
</CardContent>
</Card>
@@ -343,8 +354,8 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
/>
</div>
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-slate-500" />
<span className="text-sm text-slate-500">
<Clock className="h-4 w-4" />
<span className="text-muted-foreground text-sm">
{new Date().toLocaleTimeString()}
</span>
</div>
@@ -370,18 +381,22 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
<Dialog open={showEmergencyDialog} onOpenChange={setShowEmergencyDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center space-x-2 text-red-600">
<DialogTitle className="flex items-center space-x-2">
<AlertTriangle className="h-5 w-5" />
<span>Emergency Action Required</span>
</DialogTitle>
<DialogDescription>
Select the type of emergency action to perform. This will immediately stop or override current robot operations.
Select the type of emergency action to perform. This will
immediately stop or override current robot operations.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="emergency-select">Emergency Action Type</Label>
<Select value={selectedEmergencyAction} onValueChange={setSelectedEmergencyAction}>
<Select
value={selectedEmergencyAction}
onValueChange={setSelectedEmergencyAction}
>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select emergency action..." />
</SelectTrigger>
@@ -394,11 +409,13 @@ export function ActionControls({ currentStep, onExecuteAction, trialId }: Action
</SelectContent>
</Select>
</div>
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<div className="rounded-lg border p-3">
<div className="flex items-start space-x-2">
<AlertTriangle className="h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-red-800">
<strong>Warning:</strong> Emergency actions will immediately halt all robot operations and may require manual intervention to resume.
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0" />
<div className="text-sm">
<strong>Warning:</strong> Emergency actions will immediately
halt all robot operations and may require manual intervention
to resume.
</div>
</div>
</div>

View File

@@ -0,0 +1,151 @@
"use client";
import { Clock, Activity, User, Bot, AlertCircle } from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import { ScrollArea } from "~/components/ui/scroll-area";
import { Badge } from "~/components/ui/badge";
import { useEffect, useState } from "react";
import type { WebSocketMessage } from "~/hooks/useWebSocket";
interface EventsLogSidebarProps {
events: WebSocketMessage[];
maxEvents?: number;
showTimestamps?: boolean;
}
const getEventIcon = (eventType: string) => {
switch (eventType) {
case "trial_status":
case "trial_action_executed":
return Activity;
case "step_changed":
return Clock;
case "wizard_intervention":
case "intervention_logged":
return User;
case "robot_action":
return Bot;
case "error":
return AlertCircle;
default:
return Activity;
}
};
const getEventVariant = (eventType: string) => {
switch (eventType) {
case "error":
return "destructive" as const;
case "wizard_intervention":
case "intervention_logged":
return "secondary" as const;
case "trial_status":
return "default" as const;
default:
return "outline" as const;
}
};
const formatEventData = (event: WebSocketMessage): string => {
switch (event.type) {
case "trial_status":
const trialData = event.data as { trial: { status: string } };
return `Trial status: ${trialData.trial.status}`;
case "step_changed":
const stepData = event.data as {
to_step: number;
step_name?: string;
};
return `Step ${stepData.to_step + 1}${stepData.step_name ? `: ${stepData.step_name}` : ""}`;
case "trial_action_executed":
const actionData = event.data as { action_type: string };
return `Action: ${actionData.action_type}`;
case "wizard_intervention":
case "intervention_logged":
const interventionData = event.data as { content?: string };
return interventionData.content ?? "Wizard intervention";
case "error":
const errorData = event.data as { message?: string };
return errorData.message ?? "System error";
default:
return `Event: ${event.type}`;
}
};
const getEventTimestamp = (event: WebSocketMessage): Date => {
const data = event.data as { timestamp?: number };
return data.timestamp ? new Date(data.timestamp) : new Date();
};
export function EventsLogSidebar({
events,
maxEvents = 10,
showTimestamps = true,
}: EventsLogSidebarProps) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const displayEvents = events.slice(-maxEvents).reverse();
if (displayEvents.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Clock className="text-muted-foreground mb-3 h-8 w-8" />
<p className="text-muted-foreground text-sm">No events yet</p>
<p className="text-muted-foreground mt-1 text-xs">
Events will appear here during trial execution
</p>
</div>
);
}
return (
<ScrollArea className="h-64">
<div className="space-y-3">
{displayEvents.map((event, index) => {
const Icon = getEventIcon(event.type);
const timestamp = getEventTimestamp(event);
const eventText = formatEventData(event);
return (
<div key={index} className="flex items-start gap-3">
<div className="mt-0.5 flex-shrink-0">
<div className="bg-muted rounded-full p-1.5">
<Icon className="h-3 w-3" />
</div>
</div>
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2">
<Badge
variant={getEventVariant(event.type)}
className="text-xs"
>
{event.type.replace(/_/g, " ")}
</Badge>
{showTimestamps && isClient && (
<span className="text-muted-foreground text-xs">
{formatDistanceToNow(timestamp, { addSuffix: true })}
</span>
)}
</div>
<p className="text-foreground text-sm break-words">
{eventText}
</p>
</div>
</div>
);
})}
</div>
</ScrollArea>
);
}

View File

@@ -0,0 +1,330 @@
"use client";
import { CheckCircle, Clock, PlayCircle, AlertCircle, Eye } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Progress } from "~/components/ui/progress";
import { Alert, AlertDescription } from "~/components/ui/alert";
interface ActionDefinition {
id: string;
stepId: string;
name: string;
description?: string;
type: string;
orderIndex: number;
parameters: Record<string, unknown>;
timeout?: number;
required: boolean;
condition?: string;
}
interface StepDefinition {
id: string;
name: string;
description?: string;
type: string;
orderIndex: number;
condition?: string;
actions: ActionDefinition[];
}
interface ExecutionContext {
trialId: string;
experimentId: string;
participantId: string;
wizardId?: string;
currentStepIndex: number;
startTime: Date;
variables: Record<string, unknown>;
}
interface ExecutionStepDisplayProps {
currentStep: StepDefinition | null;
executionContext: ExecutionContext | null;
totalSteps: number;
onExecuteStep: () => void;
onAdvanceStep: () => void;
onCompleteWizardAction: (
actionId: string,
data?: Record<string, unknown>,
) => void;
isExecuting: boolean;
}
export function ExecutionStepDisplay({
currentStep,
executionContext,
totalSteps,
onExecuteStep,
onAdvanceStep,
onCompleteWizardAction,
isExecuting,
}: ExecutionStepDisplayProps) {
if (!currentStep || !executionContext) {
return (
<Card className="shadow-sm">
<CardContent className="p-6 text-center">
<div className="text-muted-foreground">
<Clock className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="text-sm">No active step</p>
<p className="mt-1 text-xs">
Trial may not be started or all steps completed
</p>
</div>
</CardContent>
</Card>
);
}
const progress =
totalSteps > 0
? ((executionContext.currentStepIndex + 1) / totalSteps) * 100
: 0;
const getActionConfig = (
type: string,
): { icon: typeof PlayCircle; label: string } => {
const configs: Record<string, { icon: typeof PlayCircle; label: string }> =
{
wizard_say: {
icon: PlayCircle,
label: "Wizard Speech",
},
wizard_gesture: {
icon: PlayCircle,
label: "Wizard Gesture",
},
wizard_show_object: {
icon: Eye,
label: "Show Object",
},
observe_behavior: {
icon: Eye,
label: "Observe Behavior",
},
wait: { icon: Clock, label: "Wait" },
};
return (
configs[type] ?? {
icon: PlayCircle,
label: "Action",
}
);
};
const getWizardInstructions = (action: ActionDefinition): string => {
switch (action.type) {
case "wizard_say":
return `Say: "${String(action.parameters.text) ?? "Please speak to the participant"}";`;
case "wizard_gesture":
return `Perform gesture: ${String(action.parameters.gesture) ?? "as specified in the protocol"}`;
case "wizard_show_object":
return `Show object: ${String(action.parameters.object) ?? "as specified in the protocol"}`;
case "observe_behavior":
return `Observe and record: ${String(action.parameters.behavior) ?? "participant behavior"}`;
case "wait":
return `Wait for ${String(action.parameters.duration) ?? "1000"}ms`;
default:
return `Execute: ${action.name ?? "Unknown Action"}`;
}
};
const requiresWizardInput = (action: ActionDefinition): boolean => {
return [
"wizard_say",
"wizard_gesture",
"wizard_show_object",
"observe_behavior",
].includes(action.type);
};
return (
<div className="space-y-4">
{/* Step Progress */}
<Card className="shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-semibold">
Step {executionContext.currentStepIndex + 1} of {totalSteps}
</CardTitle>
<Badge variant="outline" className="text-xs">
{Math.round(progress)}% Complete
</Badge>
</div>
<Progress value={progress} className="h-2" />
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2">
<h3 className="font-medium">{currentStep.name}</h3>
{currentStep.description && (
<p className="text-muted-foreground text-sm">
{currentStep.description}
</p>
)}
<div className="flex items-center space-x-2">
<Badge variant="secondary" className="text-xs">
{currentStep.type
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge>
<span className="text-muted-foreground text-xs">
{currentStep.actions.length} action
{currentStep.actions.length !== 1 ? "s" : ""}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Step Actions */}
<Card className="shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium">
Step Actions
</CardTitle>
<Button
onClick={onExecuteStep}
disabled={isExecuting}
size="sm"
className="h-8"
>
<PlayCircle className="mr-1 h-3 w-3" />
{isExecuting ? "Executing..." : "Execute Step"}
</Button>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-3">
{currentStep.actions?.map((action, _index) => {
const config = getActionConfig(action.type);
const Icon = config.icon;
const needsWizardInput = requiresWizardInput(action);
return (
<div key={action.id} className="rounded-lg border p-3">
<div className="flex items-start justify-between">
<div className="flex-1 space-y-2">
<div className="flex items-center space-x-2">
<Icon className="h-4 w-4" />
<span className="text-sm font-medium">
{action.name}
</span>
<Badge variant="outline" className="text-xs">
{config.label}
</Badge>
{action.required && (
<Badge variant="destructive" className="text-xs">
Required
</Badge>
)}
</div>
{action.description && (
<p className="text-muted-foreground ml-6 text-xs">
{action.description}
</p>
)}
{needsWizardInput && (
<Alert className="mt-2 ml-6">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">
{getWizardInstructions(action)}
</AlertDescription>
</Alert>
)}
{/* Action Parameters */}
{Object.keys(action.parameters).length > 0 && (
<div className="mt-2 ml-6">
<details className="text-xs">
<summary className="text-muted-foreground cursor-pointer">
Parameters (
{Object.keys(action.parameters).length})
</summary>
<div className="mt-1 space-y-1">
{Object.entries(action.parameters).map(
([key, value]) => (
<div
key={key}
className="flex justify-between text-xs"
>
<span className="text-muted-foreground font-mono">
{key}:
</span>
<span className="font-mono">
{typeof value === "string"
? `"${value}"`
: String(value)}
</span>
</div>
),
)}
</div>
</details>
</div>
)}
</div>
{needsWizardInput && (
<Button
onClick={() => onCompleteWizardAction(action.id, {})}
size="sm"
variant="outline"
className="h-7 text-xs"
>
<CheckCircle className="mr-1 h-3 w-3" />
Complete
</Button>
)}
</div>
</div>
);
})}
</div>
{/* Step Controls */}
<div className="mt-4 flex justify-end space-x-2">
<Button
onClick={onAdvanceStep}
variant="outline"
size="sm"
disabled={isExecuting}
>
Next Step
</Button>
</div>
</CardContent>
</Card>
{/* Execution Variables (if any) */}
{Object.keys(executionContext.variables).length > 0 && (
<Card className="shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium">
Execution Variables
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-1">
{Object.entries(executionContext.variables).map(
([key, value]) => (
<div key={key} className="flex justify-between text-xs">
<span className="font-mono text-slate-600">{key}:</span>
<span className="font-mono text-slate-900">
{typeof value === "string" ? `"${value}"` : String(value)}
</span>
</div>
),
)}
</div>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -1,43 +1,41 @@
"use client";
import {
Briefcase, Clock, GraduationCap, Info, Mail, Shield, User
} from "lucide-react";
import { Briefcase, Clock, GraduationCap, Info, Shield } from "lucide-react";
import { Avatar, AvatarFallback } from "~/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
interface ParticipantInfoProps {
participant: {
id: string;
participantCode: string;
email: string | null;
name: string | null;
demographics: any;
demographics: Record<string, unknown> | null;
};
trialStatus: "scheduled" | "in_progress" | "completed" | "aborted" | "failed";
}
export function ParticipantInfo({ participant }: ParticipantInfoProps) {
const demographics = participant.demographics || {};
export function ParticipantInfo({
participant,
trialStatus: _trialStatus,
}: ParticipantInfoProps) {
const demographics = participant.demographics ?? {};
// Extract common demographic fields
const age = demographics.age;
const gender = demographics.gender;
const occupation = demographics.occupation;
const education = demographics.education;
const language = demographics.primaryLanguage || demographics.language;
const location = demographics.location || demographics.city;
const experience = demographics.robotExperience || demographics.experience;
const age = demographics.age as string | number | undefined;
const gender = demographics.gender as string | undefined;
const occupation = demographics.occupation as string | undefined;
const education = demographics.education as string | undefined;
const language =
(demographics.primaryLanguage as string | undefined) ??
(demographics.language as string | undefined);
const experience =
(demographics.robotExperience as string | undefined) ??
(demographics.experience as string | undefined);
// Get participant initials for avatar
const getInitials = () => {
if (participant.name) {
const nameParts = participant.name.split(" ");
return nameParts.map((part) => part.charAt(0).toUpperCase()).join("");
}
return participant.participantCode.substring(0, 2).toUpperCase();
};
const formatDemographicValue = (key: string, value: any) => {
const formatDemographicValue = (key: string, value: unknown) => {
if (value === null || value === undefined || value === "") return null;
// Handle different data types
@@ -53,81 +51,64 @@ export function ParticipantInfo({ participant }: ParticipantInfoProps) {
return JSON.stringify(value);
}
return String(value);
return typeof value === "string" ? value : JSON.stringify(value);
};
return (
<div className="space-y-4">
<div className="flex items-center space-x-2">
<User className="h-4 w-4 text-slate-600" />
<h3 className="font-medium text-slate-900">Participant</h3>
</div>
{/* Basic Info Card */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<Avatar className="h-10 w-10">
<AvatarFallback className="bg-blue-100 font-medium text-blue-600">
{getInitials()}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-slate-900">
{participant.name || "Anonymous"}
</div>
<div className="text-sm text-slate-600">
ID: {participant.participantCode}
</div>
{participant.email && (
<div className="mt-1 flex items-center space-x-1 text-xs text-slate-500">
<Mail className="h-3 w-3" />
<span className="truncate">{participant.email}</span>
</div>
)}
{/* Basic Info */}
<div className="rounded-lg border p-4">
<div className="flex items-start space-x-3">
<Avatar className="h-10 w-10">
<AvatarFallback className="font-medium">
{getInitials()}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-slate-900">
Participant {participant.participantCode}
</div>
<div className="text-sm text-slate-600">
ID: {participant.participantCode}
</div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Quick Demographics */}
{(age || gender || language) && (
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="grid grid-cols-1 gap-2 text-sm">
{age && (
<div className="flex items-center justify-between">
<span className="text-slate-600">Age:</span>
<span className="font-medium">{age}</span>
</div>
)}
{gender && (
<div className="flex items-center justify-between">
<span className="text-slate-600">Gender:</span>
<span className="font-medium capitalize">{gender}</span>
</div>
)}
{language && (
<div className="flex items-center justify-between">
<span className="text-slate-600">Language:</span>
<span className="font-medium">{language}</span>
</div>
)}
</div>
</CardContent>
</Card>
{(age ?? gender ?? language) && (
<div className="rounded-lg border p-4">
<div className="grid grid-cols-1 gap-2 text-sm">
{age && (
<div className="flex items-center justify-between">
<span className="text-slate-600">Age:</span>
<span className="font-medium">{age}</span>
</div>
)}
{gender && (
<div className="flex items-center justify-between">
<span className="text-slate-600">Gender:</span>
<span className="font-medium capitalize">{gender}</span>
</div>
)}
{language && (
<div className="flex items-center justify-between">
<span className="text-slate-600">Language:</span>
<span className="font-medium">{language}</span>
</div>
)}
</div>
</div>
)}
{/* Background Info */}
{(occupation || education || experience) && (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="flex items-center space-x-1 text-sm font-medium text-slate-700">
<Info className="h-3 w-3" />
<span>Background</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 pt-0">
{(occupation ?? education ?? experience) && (
<div className="rounded-lg border p-4">
<div className="mb-3 flex items-center space-x-1 text-sm font-medium text-slate-700">
<Info className="h-3 w-3" />
<span>Background</span>
</div>
<div className="space-y-2">
{occupation && (
<div className="flex items-start space-x-2 text-sm">
<Briefcase className="mt-0.5 h-3 w-3 flex-shrink-0 text-slate-400" />
@@ -155,19 +136,17 @@ export function ParticipantInfo({ participant }: ParticipantInfoProps) {
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{/* Additional Demographics */}
{Object.keys(demographics).length > 0 && (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-700">
Additional Info
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="rounded-lg border p-4">
<div className="mb-3 text-sm font-medium text-slate-700">
Additional Info
</div>
<div>
<div className="space-y-1">
{Object.entries(demographics)
.filter(
@@ -211,30 +190,26 @@ export function ParticipantInfo({ participant }: ParticipantInfoProps) {
);
})}
</div>
</CardContent>
</Card>
</div>
</div>
)}
{/* Consent Status */}
<Card className="border-green-200 bg-green-50 shadow-sm">
<CardContent className="p-3">
<div className="flex items-center space-x-2">
<div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="text-sm font-medium text-green-800">
Consent Verified
</span>
</div>
<div className="mt-1 text-xs text-green-600">
Participant has provided informed consent
</div>
</CardContent>
</Card>
<div className="rounded-lg border p-3">
<div className="flex items-center space-x-2">
<div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="text-sm font-medium">Consent Verified</span>
</div>
<div className="text-muted-foreground mt-1 text-xs">
Participant has provided informed consent
</div>
</div>
{/* Session Info */}
<div className="space-y-1 text-xs text-slate-500">
<div className="flex items-center space-x-1">
<Clock className="h-3 w-3" />
<span>Session started: {new Date().toLocaleTimeString()}</span>
<span>Session active</span>
</div>
</div>
</div>

View File

@@ -1,18 +1,25 @@
"use client";
import {
Activity, AlertTriangle, Battery,
BatteryLow, Bot, CheckCircle,
Clock, RefreshCw, Signal,
SignalHigh,
SignalLow,
SignalMedium, WifiOff
Activity,
AlertTriangle,
Battery,
BatteryLow,
Bot,
CheckCircle,
Clock,
RefreshCw,
Signal,
SignalHigh,
SignalLow,
SignalMedium,
WifiOff,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Alert, AlertDescription } from "~/components/ui/alert";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Progress } from "~/components/ui/progress";
interface RobotStatusProps {
@@ -37,10 +44,10 @@ interface RobotStatus {
z?: number;
orientation?: number;
};
sensors?: Record<string, any>;
sensors?: Record<string, string>;
}
export function RobotStatus({ trialId }: RobotStatusProps) {
export function RobotStatus({ trialId: _trialId }: RobotStatusProps) {
const [robotStatus, setRobotStatus] = useState<RobotStatus | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const [refreshing, setRefreshing] = useState(false);
@@ -62,32 +69,43 @@ export function RobotStatus({ trialId }: RobotStatusProps) {
position: {
x: 1.2,
y: 0.8,
orientation: 45
orientation: 45,
},
sensors: {
lidar: "operational",
camera: "operational",
imu: "operational",
odometry: "operational"
}
odometry: "operational",
},
};
setRobotStatus(mockStatus);
// Simulate periodic updates
const interval = setInterval(() => {
setRobotStatus(prev => {
setRobotStatus((prev) => {
if (!prev) return prev;
return {
...prev,
batteryLevel: Math.max(0, (prev.batteryLevel || 0) - Math.random() * 0.5),
signalStrength: Math.max(0, Math.min(100, (prev.signalStrength || 0) + (Math.random() - 0.5) * 10)),
batteryLevel: Math.max(
0,
(prev.batteryLevel ?? 0) - Math.random() * 0.5,
),
signalStrength: Math.max(
0,
Math.min(
100,
(prev.signalStrength ?? 0) + (Math.random() - 0.5) * 10,
),
),
lastHeartbeat: new Date(),
position: prev.position ? {
...prev.position,
x: prev.position.x + (Math.random() - 0.5) * 0.1,
y: prev.position.y + (Math.random() - 0.5) * 0.1,
} : undefined
position: prev.position
? {
...prev.position,
x: prev.position.x + (Math.random() - 0.5) * 0.1,
y: prev.position.y + (Math.random() - 0.5) * 0.1,
}
: undefined,
};
});
setLastUpdate(new Date());
@@ -103,35 +121,35 @@ export function RobotStatus({ trialId }: RobotStatusProps) {
icon: CheckCircle,
color: "text-green-600",
bgColor: "bg-green-100",
label: "Connected"
label: "Connected",
};
case "connecting":
return {
icon: RefreshCw,
color: "text-blue-600",
bgColor: "bg-blue-100",
label: "Connecting"
label: "Connecting",
};
case "disconnected":
return {
icon: WifiOff,
color: "text-gray-600",
bgColor: "bg-gray-100",
label: "Disconnected"
label: "Disconnected",
};
case "error":
return {
icon: AlertTriangle,
color: "text-red-600",
bgColor: "bg-red-100",
label: "Error"
label: "Error",
};
default:
return {
icon: WifiOff,
color: "text-gray-600",
bgColor: "bg-gray-100",
label: "Unknown"
label: "Unknown",
};
}
};
@@ -159,182 +177,173 @@ export function RobotStatus({ trialId }: RobotStatusProps) {
if (!robotStatus) {
return (
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Bot className="h-4 w-4 text-slate-600" />
<h3 className="font-medium text-slate-900">Robot Status</h3>
<div className="rounded-lg border p-4 text-center">
<div className="text-slate-500">
<Bot className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="text-sm">No robot connected</p>
</div>
</div>
<Card className="shadow-sm">
<CardContent className="p-4 text-center">
<div className="text-slate-500">
<Bot className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No robot connected</p>
</div>
</CardContent>
</Card>
</div>
);
}
const statusConfig = getConnectionStatusConfig(robotStatus.connectionStatus);
const StatusIcon = statusConfig.icon;
const SignalIcon = getSignalIcon(robotStatus.signalStrength || 0);
const BatteryIcon = getBatteryIcon(robotStatus.batteryLevel || 0);
const SignalIcon = getSignalIcon(robotStatus.signalStrength ?? 0);
const BatteryIcon = getBatteryIcon(robotStatus.batteryLevel ?? 0);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Bot className="h-4 w-4 text-slate-600" />
<h3 className="font-medium text-slate-900">Robot Status</h3>
</div>
<div className="flex items-center justify-end">
<Button
variant="ghost"
size="sm"
onClick={handleRefreshStatus}
disabled={refreshing}
>
<RefreshCw className={`h-3 w-3 ${refreshing ? 'animate-spin' : ''}`} />
<RefreshCw
className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`}
/>
</Button>
</div>
{/* Main Status Card */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="space-y-3">
{/* Robot Info */}
<div className="flex items-center justify-between">
<div className="font-medium text-slate-900">{robotStatus.name}</div>
<Badge className={`${statusConfig.bgColor} ${statusConfig.color}`} variant="secondary">
<StatusIcon className="mr-1 h-3 w-3" />
{statusConfig.label}
</Badge>
</div>
{/* Connection Details */}
<div className="text-sm text-slate-600">
Protocol: {robotStatus.communicationProtocol}
</div>
{/* Status Indicators */}
<div className="grid grid-cols-2 gap-3">
{/* Battery */}
{robotStatus.batteryLevel !== undefined && (
<div className="space-y-1">
<div className="flex items-center space-x-1 text-xs text-slate-600">
<BatteryIcon className={`h-3 w-3 ${
robotStatus.batteryLevel <= 20 ? 'text-red-500' : 'text-green-500'
}`} />
<span>Battery</span>
</div>
<div className="flex items-center space-x-2">
<Progress
value={robotStatus.batteryLevel}
className="flex-1 h-1.5"
/>
<span className="text-xs font-medium w-8">
{Math.round(robotStatus.batteryLevel)}%
</span>
</div>
</div>
)}
{/* Signal Strength */}
{robotStatus.signalStrength !== undefined && (
<div className="space-y-1">
<div className="flex items-center space-x-1 text-xs text-slate-600">
<SignalIcon className="h-3 w-3" />
<span>Signal</span>
</div>
<div className="flex items-center space-x-2">
<Progress
value={robotStatus.signalStrength}
className="flex-1 h-1.5"
/>
<span className="text-xs font-medium w-8">
{Math.round(robotStatus.signalStrength)}%
</span>
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
{/* Current Mode */}
<Card className="shadow-sm">
<CardContent className="p-3">
<div className="rounded-lg border p-4">
<div className="space-y-3">
{/* Robot Info */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Activity className="h-3 w-3 text-slate-600" />
<span className="text-sm text-slate-600">Mode:</span>
</div>
<Badge variant="outline" className="text-xs">
{robotStatus.currentMode.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
<div className="font-medium text-slate-900">{robotStatus.name}</div>
<Badge
className={`${statusConfig.bgColor} ${statusConfig.color}`}
variant="secondary"
>
<StatusIcon className="mr-1 h-3 w-3" />
{statusConfig.label}
</Badge>
</div>
{robotStatus.isMoving && (
<div className="flex items-center space-x-1 mt-2 text-xs text-blue-600">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
<span>Robot is moving</span>
</div>
)}
</CardContent>
</Card>
{/* Connection Details */}
<div className="text-sm text-slate-600">
Protocol: {robotStatus.communicationProtocol}
</div>
{/* Status Indicators */}
<div className="grid grid-cols-2 gap-3">
{/* Battery */}
{robotStatus.batteryLevel !== undefined && (
<div className="space-y-1">
<div className="flex items-center space-x-1 text-xs text-slate-600">
<BatteryIcon className="h-3 w-3" />
<span>Battery</span>
</div>
<div className="flex items-center space-x-2">
<Progress
value={robotStatus.batteryLevel}
className="h-1.5 flex-1"
/>
<span className="w-8 text-xs font-medium">
{Math.round(robotStatus.batteryLevel)}%
</span>
</div>
</div>
)}
{/* Signal Strength */}
{robotStatus.signalStrength !== undefined && (
<div className="space-y-1">
<div className="flex items-center space-x-1 text-xs text-slate-600">
<SignalIcon className="h-3 w-3" />
<span>Signal</span>
</div>
<div className="flex items-center space-x-2">
<Progress
value={robotStatus.signalStrength}
className="h-1.5 flex-1"
/>
<span className="w-8 text-xs font-medium">
{Math.round(robotStatus.signalStrength)}%
</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Current Mode */}
<div className="rounded-lg border p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Activity className="h-3 w-3 text-slate-600" />
<span className="text-sm text-slate-600">Mode:</span>
</div>
<Badge variant="outline" className="text-xs">
{robotStatus.currentMode
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())}
</Badge>
</div>
{robotStatus.isMoving && (
<div className="mt-2 flex items-center space-x-1 text-xs">
<div className="h-1.5 w-1.5 animate-pulse rounded-full"></div>
<span>Robot is moving</span>
</div>
)}
</div>
{/* Position Info */}
{robotStatus.position && (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-700">Position</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="rounded-lg border p-4">
<div className="mb-3 text-sm font-medium text-slate-700">
Position
</div>
<div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex justify-between">
<span className="text-slate-600">X:</span>
<span className="font-mono">{robotStatus.position.x.toFixed(2)}m</span>
<span className="font-mono">
{robotStatus.position.x.toFixed(2)}m
</span>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Y:</span>
<span className="font-mono">{robotStatus.position.y.toFixed(2)}m</span>
<span className="font-mono">
{robotStatus.position.y.toFixed(2)}m
</span>
</div>
{robotStatus.position.orientation !== undefined && (
<div className="flex justify-between col-span-2">
<div className="col-span-2 flex justify-between">
<span className="text-slate-600">Orientation:</span>
<span className="font-mono">{Math.round(robotStatus.position.orientation)}°</span>
<span className="font-mono">
{Math.round(robotStatus.position.orientation)}°
</span>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</div>
)}
{/* Sensors Status */}
{robotStatus.sensors && (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-slate-700">Sensors</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="rounded-lg border p-4">
<div className="mb-3 text-sm font-medium text-slate-700">Sensors</div>
<div>
<div className="space-y-1">
{Object.entries(robotStatus.sensors).map(([sensor, status]) => (
<div key={sensor} className="flex items-center justify-between text-xs">
<div
key={sensor}
className="flex items-center justify-between text-xs"
>
<span className="text-slate-600 capitalize">{sensor}:</span>
<Badge
variant="outline"
className={`text-xs ${
status === 'operational'
? 'text-green-600 border-green-200'
: 'text-red-600 border-red-200'
}`}
>
<Badge variant="outline" className="text-xs">
{status}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
)}
{/* Error Alert */}
@@ -348,7 +357,7 @@ export function RobotStatus({ trialId }: RobotStatusProps) {
)}
{/* Last Update */}
<div className="text-xs text-slate-500 flex items-center space-x-1">
<div className="flex items-center space-x-1 text-xs text-slate-500">
<Clock className="h-3 w-3" />
<span>Last update: {lastUpdate.toLocaleTimeString()}</span>
</div>

View File

@@ -1,9 +1,23 @@
"use client";
import {
Activity, ArrowRight, Bot, CheckCircle, GitBranch, MessageSquare, Play, Settings, Timer,
User, Users
Activity,
ArrowRight,
Bot,
CheckCircle,
GitBranch,
MessageSquare,
Play,
Settings,
Timer,
User,
Users,
} from "lucide-react";
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
import { useState } from "react";
import { Alert, AlertDescription } from "~/components/ui/alert";
import { Badge } from "~/components/ui/badge";
@@ -16,7 +30,11 @@ interface StepDisplayProps {
step: {
id: string;
name: string;
type: "wizard_action" | "robot_action" | "parallel_steps" | "conditional_branch";
type:
| "wizard_action"
| "robot_action"
| "parallel_steps"
| "conditional_branch";
description?: string;
parameters?: any;
duration?: number;
@@ -63,10 +81,12 @@ export function StepDisplay({
stepIndex,
totalSteps,
isActive,
onExecuteAction
onExecuteAction,
}: StepDisplayProps) {
const [isExecuting, setIsExecuting] = useState(false);
const [completedActions, setCompletedActions] = useState<Set<string>>(new Set());
const [completedActions, setCompletedActions] = useState<Set<string>>(
new Set(),
);
const stepConfig = stepTypeConfig[step.type];
const StepIcon = stepConfig.icon;
@@ -75,7 +95,7 @@ export function StepDisplay({
setIsExecuting(true);
try {
await onExecuteAction(actionId, actionData);
setCompletedActions(prev => new Set([...prev, actionId]));
setCompletedActions((prev) => new Set([...prev, actionId]));
} catch (_error) {
console.error("Failed to execute action:", _error);
} finally {
@@ -97,17 +117,19 @@ export function StepDisplay({
{step.actions && step.actions.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium text-slate-900">Available Actions:</h4>
<h4 className="font-medium text-slate-900">
Available Actions:
</h4>
<div className="grid gap-2">
{step.actions.map((action: any, index: number) => {
const isCompleted = completedActions.has(action.id);
return (
<div
key={action.id || index}
className={`flex items-center justify-between p-3 rounded-lg border ${
className={`flex items-center justify-between rounded-lg border p-3 ${
isCompleted
? "bg-green-50 border-green-200"
: "bg-slate-50 border-slate-200"
? "border-green-200 bg-green-50"
: "border-slate-200 bg-slate-50"
}`}
>
<div className="flex items-center space-x-3">
@@ -117,16 +139,20 @@ export function StepDisplay({
<Play className="h-4 w-4 text-slate-400" />
)}
<div>
<p className="font-medium text-sm">{action.name}</p>
<p className="text-sm font-medium">{action.name}</p>
{action.description && (
<p className="text-xs text-slate-600">{action.description}</p>
<p className="text-xs text-slate-600">
{action.description}
</p>
)}
</div>
</div>
{isActive && !isCompleted && (
<Button
size="sm"
onClick={() => handleActionExecution(action.id, action)}
onClick={() =>
handleActionExecution(action.id, action)
}
disabled={isExecuting}
>
Execute
@@ -153,8 +179,10 @@ export function StepDisplay({
{step.parameters && (
<div className="space-y-2">
<h4 className="font-medium text-slate-900">Robot Parameters:</h4>
<div className="bg-slate-50 rounded-lg p-3 text-sm font-mono">
<h4 className="font-medium text-slate-900">
Robot Parameters:
</h4>
<div className="rounded-lg bg-slate-50 p-3 font-mono text-sm">
<pre>{JSON.stringify(step.parameters, null, 2)}</pre>
</div>
</div>
@@ -181,22 +209,26 @@ export function StepDisplay({
{step.substeps && step.substeps.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium text-slate-900">Parallel Actions:</h4>
<h4 className="font-medium text-slate-900">
Parallel Actions:
</h4>
<div className="grid gap-3">
{step.substeps.map((substep: any, index: number) => (
<div
key={substep.id || index}
className="flex items-center space-x-3 p-3 bg-slate-50 rounded-lg border"
className="flex items-center space-x-3 rounded-lg border bg-slate-50 p-3"
>
<div className="flex-shrink-0">
<div className="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center text-xs font-medium text-purple-600">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-purple-100 text-xs font-medium text-purple-600">
{index + 1}
</div>
</div>
<div className="flex-1">
<p className="font-medium text-sm">{substep.name}</p>
<p className="text-sm font-medium">{substep.name}</p>
{substep.description && (
<p className="text-xs text-slate-600">{substep.description}</p>
<p className="text-xs text-slate-600">
{substep.description}
</p>
)}
</div>
<div className="flex-shrink-0">
@@ -225,7 +257,7 @@ export function StepDisplay({
{step.conditions && (
<div className="space-y-2">
<h4 className="font-medium text-slate-900">Conditions:</h4>
<div className="bg-slate-50 rounded-lg p-3 text-sm">
<div className="rounded-lg bg-slate-50 p-3 text-sm">
<pre>{JSON.stringify(step.conditions, null, 2)}</pre>
</div>
</div>
@@ -233,19 +265,23 @@ export function StepDisplay({
{step.branches && step.branches.length > 0 && (
<div className="space-y-3">
<h4 className="font-medium text-slate-900">Possible Branches:</h4>
<h4 className="font-medium text-slate-900">
Possible Branches:
</h4>
<div className="grid gap-2">
{step.branches.map((branch: any, index: number) => (
<div
key={branch.id || index}
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border"
className="flex items-center justify-between rounded-lg border bg-slate-50 p-3"
>
<div className="flex items-center space-x-3">
<ArrowRight className="h-4 w-4 text-orange-500" />
<div>
<p className="font-medium text-sm">{branch.name}</p>
<p className="text-sm font-medium">{branch.name}</p>
{branch.condition && (
<p className="text-xs text-slate-600">If: {branch.condition}</p>
<p className="text-xs text-slate-600">
If: {branch.condition}
</p>
)}
</div>
</div>
@@ -253,7 +289,9 @@ export function StepDisplay({
<Button
size="sm"
variant="outline"
onClick={() => handleActionExecution(`branch_${branch.id}`, branch)}
onClick={() =>
handleActionExecution(`branch_${branch.id}`, branch)
}
disabled={isExecuting}
>
Select
@@ -269,8 +307,8 @@ export function StepDisplay({
default:
return (
<div className="text-center py-8 text-slate-500">
<Settings className="h-8 w-8 mx-auto mb-2" />
<div className="py-8 text-center text-slate-500">
<Settings className="mx-auto mb-2 h-8 w-8" />
<p>Unknown step type: {step.type}</p>
</div>
);
@@ -278,32 +316,46 @@ export function StepDisplay({
};
return (
<Card className={`transition-all duration-200 ${
isActive ? "ring-2 ring-blue-500 shadow-lg" : "border-slate-200"
}`}>
<Card
className={`transition-all duration-200 ${
isActive ? "shadow-lg ring-2 ring-blue-500" : "border-slate-200"
}`}
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
<div className={`flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center ${
stepConfig.color === "blue" ? "bg-blue-100" :
stepConfig.color === "green" ? "bg-green-100" :
stepConfig.color === "purple" ? "bg-purple-100" :
stepConfig.color === "orange" ? "bg-orange-100" :
"bg-slate-100"
}`}>
<StepIcon className={`h-5 w-5 ${
stepConfig.color === "blue" ? "text-blue-600" :
stepConfig.color === "green" ? "text-green-600" :
stepConfig.color === "purple" ? "text-purple-600" :
stepConfig.color === "orange" ? "text-orange-600" :
"text-slate-600"
}`} />
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${
stepConfig.color === "blue"
? "bg-blue-100"
: stepConfig.color === "green"
? "bg-green-100"
: stepConfig.color === "purple"
? "bg-purple-100"
: stepConfig.color === "orange"
? "bg-orange-100"
: "bg-slate-100"
}`}
>
<StepIcon
className={`h-5 w-5 ${
stepConfig.color === "blue"
? "text-blue-600"
: stepConfig.color === "green"
? "text-green-600"
: stepConfig.color === "purple"
? "text-purple-600"
: stepConfig.color === "orange"
? "text-orange-600"
: "text-slate-600"
}`}
/>
</div>
<div className="flex-1 min-w-0">
<div className="min-w-0 flex-1">
<CardTitle className="text-lg font-semibold text-slate-900">
{step.name}
</CardTitle>
<div className="flex items-center space-x-2 mt-1">
<div className="mt-1 flex items-center space-x-2">
<Badge variant="outline" className="text-xs">
{stepConfig.label}
</Badge>
@@ -311,7 +363,7 @@ export function StepDisplay({
Step {stepIndex + 1} of {totalSteps}
</span>
</div>
<p className="text-sm text-slate-600 mt-1">
<p className="mt-1 text-sm text-slate-600">
{stepConfig.description}
</p>
</div>
@@ -341,9 +393,14 @@ export function StepDisplay({
<Separator className="my-4" />
<div className="flex items-center justify-between text-xs text-slate-500">
<span>Step Progress</span>
<span>{stepIndex + 1}/{totalSteps}</span>
<span>
{stepIndex + 1}/{totalSteps}
</span>
</div>
<Progress value={((stepIndex + 1) / totalSteps) * 100} className="h-1 mt-2" />
<Progress
value={((stepIndex + 1) / totalSteps) * 100}
className="mt-2 h-1"
/>
</CardContent>
</Card>
);

View File

@@ -1,8 +1,15 @@
"use client";
import {
Activity, Bot, CheckCircle,
Circle, Clock, GitBranch, Play, Target, Users
Activity,
Bot,
CheckCircle,
Circle,
Clock,
GitBranch,
Play,
Target,
Users,
} from "lucide-react";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
@@ -13,10 +20,14 @@ interface TrialProgressProps {
steps: Array<{
id: string;
name: string;
type: "wizard_action" | "robot_action" | "parallel_steps" | "conditional_branch";
type:
| "wizard_action"
| "robot_action"
| "parallel_steps"
| "conditional_branch";
description?: string;
duration?: number;
parameters?: any;
parameters?: Record<string, unknown>;
}>;
currentStepIndex: number;
trialStatus: "scheduled" | "in_progress" | "completed" | "aborted" | "failed";
@@ -29,7 +40,7 @@ const stepTypeConfig = {
color: "blue",
bgColor: "bg-blue-100",
textColor: "text-blue-600",
borderColor: "border-blue-300"
borderColor: "border-blue-300",
},
robot_action: {
label: "Robot",
@@ -37,7 +48,7 @@ const stepTypeConfig = {
color: "green",
bgColor: "bg-green-100",
textColor: "text-green-600",
borderColor: "border-green-300"
borderColor: "border-green-300",
},
parallel_steps: {
label: "Parallel",
@@ -45,7 +56,7 @@ const stepTypeConfig = {
color: "purple",
bgColor: "bg-purple-100",
textColor: "text-purple-600",
borderColor: "border-purple-300"
borderColor: "border-purple-300",
},
conditional_branch: {
label: "Branch",
@@ -53,17 +64,21 @@ const stepTypeConfig = {
color: "orange",
bgColor: "bg-orange-100",
textColor: "text-orange-600",
borderColor: "border-orange-300"
}
borderColor: "border-orange-300",
},
};
export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialProgressProps) {
export function TrialProgress({
steps,
currentStepIndex,
trialStatus,
}: TrialProgressProps) {
if (!steps || steps.length === 0) {
return (
<Card>
<CardContent className="p-6 text-center">
<div className="text-slate-500">
<Target className="h-8 w-8 mx-auto mb-2 opacity-50" />
<Target className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="text-sm">No experiment steps defined</p>
</div>
</CardContent>
@@ -71,19 +86,28 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
);
}
const progress = trialStatus === "completed" ? 100 :
trialStatus === "aborted" ? 0 :
((currentStepIndex + 1) / steps.length) * 100;
const progress =
trialStatus === "completed"
? 100
: trialStatus === "aborted"
? 0
: ((currentStepIndex + 1) / steps.length) * 100;
const completedSteps = trialStatus === "completed" ? steps.length :
trialStatus === "aborted" || trialStatus === "failed" ? 0 :
currentStepIndex;
const completedSteps =
trialStatus === "completed"
? steps.length
: trialStatus === "aborted" || trialStatus === "failed"
? 0
: currentStepIndex;
const getStepStatus = (index: number) => {
if (trialStatus === "aborted" || trialStatus === "failed") return "aborted";
if (trialStatus === "completed" || index < currentStepIndex) return "completed";
if (index === currentStepIndex && trialStatus === "in_progress") return "active";
if (index === currentStepIndex && trialStatus === "scheduled") return "pending";
if (trialStatus === "completed" || index < currentStepIndex)
return "completed";
if (index === currentStepIndex && trialStatus === "in_progress")
return "active";
if (index === currentStepIndex && trialStatus === "scheduled")
return "pending";
return "upcoming";
};
@@ -95,7 +119,7 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
iconColor: "text-green-600",
bgColor: "bg-green-100",
borderColor: "border-green-300",
textColor: "text-green-800"
textColor: "text-green-800",
};
case "active":
return {
@@ -103,7 +127,7 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
iconColor: "text-blue-600",
bgColor: "bg-blue-100",
borderColor: "border-blue-300",
textColor: "text-blue-800"
textColor: "text-blue-800",
};
case "pending":
return {
@@ -111,7 +135,7 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
iconColor: "text-amber-600",
bgColor: "bg-amber-100",
borderColor: "border-amber-300",
textColor: "text-amber-800"
textColor: "text-amber-800",
};
case "aborted":
return {
@@ -119,7 +143,7 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
iconColor: "text-red-600",
bgColor: "bg-red-100",
borderColor: "border-red-300",
textColor: "text-red-800"
textColor: "text-red-800",
};
default: // upcoming
return {
@@ -127,12 +151,15 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
iconColor: "text-slate-400",
bgColor: "bg-slate-100",
borderColor: "border-slate-300",
textColor: "text-slate-600"
textColor: "text-slate-600",
};
}
};
const totalDuration = steps.reduce((sum, step) => sum + (step.duration || 0), 0);
const totalDuration = steps.reduce(
(sum, step) => sum + (step.duration ?? 0),
0,
);
return (
<Card>
@@ -165,19 +192,25 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
<Progress
value={progress}
className={`h-2 ${
trialStatus === "completed" ? "bg-green-100" :
trialStatus === "aborted" || trialStatus === "failed" ? "bg-red-100" :
"bg-blue-100"
trialStatus === "completed"
? "bg-green-100"
: trialStatus === "aborted" || trialStatus === "failed"
? "bg-red-100"
: "bg-blue-100"
}`}
/>
<div className="flex justify-between text-xs text-slate-500">
<span>Start</span>
<span>
{trialStatus === "completed" ? "Completed" :
trialStatus === "aborted" ? "Aborted" :
trialStatus === "failed" ? "Failed" :
trialStatus === "in_progress" ? "In Progress" :
"Not Started"}
{trialStatus === "completed"
? "Completed"
: trialStatus === "aborted"
? "Aborted"
: trialStatus === "failed"
? "Failed"
: trialStatus === "in_progress"
? "In Progress"
: "Not Started"}
</span>
</div>
</div>
@@ -186,7 +219,9 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
{/* Steps Timeline */}
<div className="space-y-4">
<h4 className="font-medium text-slate-900 text-sm">Experiment Steps</h4>
<h4 className="text-sm font-medium text-slate-900">
Experiment Steps
</h4>
<div className="space-y-3">
{steps.map((step, index) => {
@@ -201,9 +236,10 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
{/* Connection Line */}
{index < steps.length - 1 && (
<div
className={`absolute left-6 top-12 w-0.5 h-6 ${
className={`absolute top-12 left-6 h-6 w-0.5 ${
getStepStatus(index + 1) === "completed" ||
(getStepStatus(index + 1) === "active" && status === "completed")
(getStepStatus(index + 1) === "active" &&
status === "completed")
? "bg-green-300"
: "bg-slate-300"
}`}
@@ -211,57 +247,76 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
)}
{/* Step Card */}
<div className={`flex items-start space-x-3 p-3 rounded-lg border transition-all ${
status === "active"
? `${statusConfig.bgColor} ${statusConfig.borderColor} shadow-md ring-2 ring-blue-200`
: status === "completed"
? `${statusConfig.bgColor} ${statusConfig.borderColor}`
: status === "aborted"
? `${statusConfig.bgColor} ${statusConfig.borderColor}`
: "bg-slate-50 border-slate-200"
}`}>
<div
className={`flex items-start space-x-3 rounded-lg border p-3 transition-all ${
status === "active"
? `${statusConfig.bgColor} ${statusConfig.borderColor} shadow-md ring-2 ring-blue-200`
: status === "completed"
? `${statusConfig.bgColor} ${statusConfig.borderColor}`
: status === "aborted"
? `${statusConfig.bgColor} ${statusConfig.borderColor}`
: "border-slate-200 bg-slate-50"
}`}
>
{/* Step Number & Status */}
<div className="flex-shrink-0 space-y-1">
<div className={`w-12 h-8 rounded-lg flex items-center justify-center ${
status === "active" ? statusConfig.bgColor :
status === "completed" ? "bg-green-100" :
status === "aborted" ? "bg-red-100" :
"bg-slate-100"
}`}>
<span className={`text-sm font-medium ${
status === "active" ? statusConfig.textColor :
status === "completed" ? "text-green-700" :
status === "aborted" ? "text-red-700" :
"text-slate-600"
}`}>
<div
className={`flex h-8 w-12 items-center justify-center rounded-lg ${
status === "active"
? statusConfig.bgColor
: status === "completed"
? "bg-green-100"
: status === "aborted"
? "bg-red-100"
: "bg-slate-100"
}`}
>
<span
className={`text-sm font-medium ${
status === "active"
? statusConfig.textColor
: status === "completed"
? "text-green-700"
: status === "aborted"
? "text-red-700"
: "text-slate-600"
}`}
>
{index + 1}
</span>
</div>
<div className="flex justify-center">
<StatusIcon className={`h-4 w-4 ${statusConfig.iconColor}`} />
<StatusIcon
className={`h-4 w-4 ${statusConfig.iconColor}`}
/>
</div>
</div>
{/* Step Content */}
<div className="flex-1 min-w-0">
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<h5 className={`font-medium truncate ${
status === "active" ? "text-slate-900" :
status === "completed" ? "text-green-900" :
status === "aborted" ? "text-red-900" :
"text-slate-700"
}`}>
<h5
className={`truncate font-medium ${
status === "active"
? "text-slate-900"
: status === "completed"
? "text-green-900"
: status === "aborted"
? "text-red-900"
: "text-slate-700"
}`}
>
{step.name}
</h5>
{step.description && (
<p className="text-sm text-slate-600 mt-1 line-clamp-2">
<p className="mt-1 line-clamp-2 text-sm text-slate-600">
{step.description}
</p>
)}
</div>
<div className="flex-shrink-0 ml-3 space-y-1">
<div className="ml-3 flex-shrink-0 space-y-1">
<Badge
variant="outline"
className={`text-xs ${stepConfig.textColor} ${stepConfig.borderColor}`}
@@ -280,19 +335,19 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
{/* Step Status Message */}
{status === "active" && trialStatus === "in_progress" && (
<div className="flex items-center space-x-1 mt-2 text-sm text-blue-600">
<div className="mt-2 flex items-center space-x-1 text-sm text-blue-600">
<Activity className="h-3 w-3 animate-pulse" />
<span>Currently executing...</span>
</div>
)}
{status === "active" && trialStatus === "scheduled" && (
<div className="flex items-center space-x-1 mt-2 text-sm text-amber-600">
<div className="mt-2 flex items-center space-x-1 text-sm text-amber-600">
<Clock className="h-3 w-3" />
<span>Ready to start</span>
</div>
)}
{status === "completed" && (
<div className="flex items-center space-x-1 mt-2 text-sm text-green-600">
<div className="mt-2 flex items-center space-x-1 text-sm text-green-600">
<CheckCircle className="h-3 w-3" />
<span>Completed</span>
</div>
@@ -309,7 +364,9 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
<Separator />
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-green-600">{completedSteps}</div>
<div className="text-2xl font-bold text-green-600">
{completedSteps}
</div>
<div className="text-xs text-slate-600">Completed</div>
</div>
<div>
@@ -320,7 +377,9 @@ export function TrialProgress({ steps, currentStepIndex, trialStatus }: TrialPro
</div>
<div>
<div className="text-2xl font-bold text-slate-600">
{steps.length - completedSteps - (trialStatus === "in_progress" ? 1 : 0)}
{steps.length -
completedSteps -
(trialStatus === "in_progress" ? 1 : 0)}
</div>
<div className="text-xs text-slate-600">Remaining</div>
</div>

File diff suppressed because it is too large Load Diff