mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
feat: Enhance trial event display with improved formatting and icons, refine trial wizard panels, and update dashboard page layouts.
This commit is contained in:
@@ -25,10 +25,10 @@ import Link from "next/link";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import { Alert, AlertDescription } from "~/components/ui/alert";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||
import { WizardControlPanel } from "./panels/WizardControlPanel";
|
||||
import { WizardExecutionPanel } from "./panels/WizardExecutionPanel";
|
||||
import { WizardMonitoringPanel } from "./panels/WizardMonitoringPanel";
|
||||
import { WizardObservationPane } from "./panels/WizardObservationPane";
|
||||
import { WebcamPanel } from "./panels/WebcamPanel";
|
||||
import { TrialStatusBar } from "./panels/TrialStatusBar";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useWizardRos } from "~/hooks/useWizardRos";
|
||||
@@ -121,20 +121,22 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
const [completedActionsCount, setCompletedActionsCount] = useState(0);
|
||||
|
||||
// Collapse state for panels
|
||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||
const [obsCollapsed, setObsCollapsed] = useState(false);
|
||||
|
||||
// Center tabs (Timeline | Actions)
|
||||
const [centerTab, setCenterTab] = useState<"timeline" | "actions">("timeline");
|
||||
|
||||
// Reset completed actions when step changes
|
||||
useEffect(() => {
|
||||
setCompletedActionsCount(0);
|
||||
}, [currentStepIndex]);
|
||||
|
||||
// Track completed steps
|
||||
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
|
||||
const [skippedSteps, setSkippedSteps] = useState<Set<number>>(new Set());
|
||||
|
||||
// Track the last response value from wizard_wait_for_response for branching
|
||||
const [lastResponse, setLastResponse] = useState<string | null>(null);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
// Get experiment steps from API
|
||||
const { data: experimentSteps } = api.experiments.getSteps.useQuery(
|
||||
@@ -492,16 +494,27 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
const handlePauseTrial = async () => {
|
||||
try {
|
||||
await pauseTrialMutation.mutateAsync({ id: trial.id });
|
||||
logEventMutation.mutate({
|
||||
trialId: trial.id,
|
||||
type: "trial_paused",
|
||||
data: { timestamp: new Date() }
|
||||
});
|
||||
setIsPaused(true);
|
||||
toast.info("Trial paused");
|
||||
} catch (error) {
|
||||
console.error("Failed to pause trial:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResumeTrial = async () => {
|
||||
try {
|
||||
logEventMutation.mutate({
|
||||
trialId: trial.id,
|
||||
type: "trial_resumed",
|
||||
data: { timestamp: new Date() }
|
||||
});
|
||||
setIsPaused(false);
|
||||
toast.success("Trial resumed");
|
||||
} catch (error) {
|
||||
console.error("Failed to resume trial:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextStep = (targetIndex?: number) => {
|
||||
// If explicit target provided (from branching choice), use it
|
||||
if (typeof targetIndex === 'number') {
|
||||
@@ -577,6 +590,24 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
}
|
||||
});
|
||||
|
||||
// Mark steps as skipped
|
||||
setSkippedSteps(prev => {
|
||||
const next = new Set(prev);
|
||||
for (let i = currentStepIndex + 1; i < targetIndex; i++) {
|
||||
if (!completedSteps.has(i)) {
|
||||
next.add(i);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
// Mark current as complete
|
||||
setCompletedSteps(prev => {
|
||||
const next = new Set(prev);
|
||||
next.add(currentStepIndex);
|
||||
return next;
|
||||
});
|
||||
|
||||
setCurrentStepIndex(targetIndex);
|
||||
setCompletedActionsCount(0);
|
||||
return;
|
||||
@@ -590,6 +621,13 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
// Default: Linear progression
|
||||
const nextIndex = currentStepIndex + 1;
|
||||
if (nextIndex < steps.length) {
|
||||
// Mark current step as complete
|
||||
setCompletedSteps(prev => {
|
||||
const next = new Set(prev);
|
||||
next.add(currentStepIndex);
|
||||
return next;
|
||||
});
|
||||
|
||||
// Log step change
|
||||
logEventMutation.mutate({
|
||||
trialId: trial.id,
|
||||
@@ -600,6 +638,7 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
fromStepId: currentStep?.id,
|
||||
toStepId: steps[nextIndex]?.id,
|
||||
stepName: steps[nextIndex]?.name,
|
||||
method: "auto"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -609,14 +648,51 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
}
|
||||
};
|
||||
|
||||
const handleStepSelect = (index: number) => {
|
||||
if (index === currentStepIndex) return;
|
||||
|
||||
// Log manual jump
|
||||
logEventMutation.mutate({
|
||||
trialId: trial.id,
|
||||
type: "intervention_step_jump",
|
||||
data: {
|
||||
fromIndex: currentStepIndex,
|
||||
toIndex: index,
|
||||
fromStepId: currentStep?.id,
|
||||
toStepId: steps[index]?.id,
|
||||
stepName: steps[index]?.name,
|
||||
method: "manual"
|
||||
}
|
||||
});
|
||||
|
||||
// Mark current as complete if leaving it?
|
||||
// Maybe better to only mark on "Next" or explicit complete.
|
||||
// If I jump away, I might not be done.
|
||||
// I'll leave 'completedSteps' update to explicit actions or completion.
|
||||
|
||||
setCurrentStepIndex(index);
|
||||
};
|
||||
|
||||
const handleCompleteTrial = async () => {
|
||||
try {
|
||||
// Mark final step as complete
|
||||
setCompletedSteps(prev => {
|
||||
const next = new Set(prev);
|
||||
next.add(currentStepIndex);
|
||||
return next;
|
||||
});
|
||||
|
||||
await completeTrialMutation.mutateAsync({ id: trial.id });
|
||||
|
||||
|
||||
// Invalidate queries so the analysis page sees the completed state immediately
|
||||
await utils.trials.get.invalidate({ id: trial.id });
|
||||
await utils.trials.getEvents.invalidate({ trialId: trial.id });
|
||||
|
||||
// Trigger archive in background
|
||||
archiveTrialMutation.mutate({ id: trial.id });
|
||||
|
||||
// Immediately navigate to analysis
|
||||
router.push(`/studies/${trial.experiment.studyId}/trials/${trial.id}/analysis`);
|
||||
} catch (error) {
|
||||
console.error("Failed to complete trial:", error);
|
||||
}
|
||||
@@ -707,12 +783,60 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
category: String(parameters?.category || "quick_note")
|
||||
});
|
||||
} else {
|
||||
// Generic action logging
|
||||
// Generic action logging - now with more details
|
||||
// Find the action definition to get its name
|
||||
let actionName = actionId;
|
||||
let actionType = "unknown";
|
||||
|
||||
// Helper to search recursively
|
||||
const findAction = (actions: ActionData[], id: string): ActionData | undefined => {
|
||||
for (const action of actions) {
|
||||
if (action.id === id) return action;
|
||||
if (action.parameters?.children) {
|
||||
const found = findAction(action.parameters.children as ActionData[], id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Search in current step first
|
||||
let foundAction: ActionData | undefined;
|
||||
if (steps[currentStepIndex]?.actions) {
|
||||
foundAction = findAction(steps[currentStepIndex]!.actions!, actionId);
|
||||
}
|
||||
|
||||
// If not found, search all steps (less efficient but safer)
|
||||
if (!foundAction) {
|
||||
for (const step of steps) {
|
||||
if (step.actions) {
|
||||
foundAction = findAction(step.actions, actionId);
|
||||
if (foundAction) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAction) {
|
||||
actionName = foundAction.name;
|
||||
actionType = foundAction.type;
|
||||
} else {
|
||||
// Fallback for Wizard Actions (often have label/value in parameters)
|
||||
if (parameters?.label && typeof parameters.label === 'string') {
|
||||
actionName = parameters.label;
|
||||
actionType = "wizard_button";
|
||||
} else if (parameters?.value && typeof parameters.value === 'string') {
|
||||
actionName = parameters.value;
|
||||
actionType = "wizard_input";
|
||||
}
|
||||
}
|
||||
|
||||
await logEventMutation.mutateAsync({
|
||||
trialId: trial.id,
|
||||
type: "action_executed",
|
||||
data: {
|
||||
actionId,
|
||||
actionName,
|
||||
actionType,
|
||||
parameters
|
||||
}
|
||||
});
|
||||
@@ -734,6 +858,22 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
) => {
|
||||
try {
|
||||
setIsExecutingAction(true);
|
||||
|
||||
// Core actions execute directly via tRPC (no ROS needed)
|
||||
if (pluginName === "hristudio-core" || pluginName === "hristudio-woz") {
|
||||
await executeRobotActionMutation.mutateAsync({
|
||||
trialId: trial.id,
|
||||
pluginName,
|
||||
actionId,
|
||||
parameters,
|
||||
});
|
||||
if (options?.autoAdvance) {
|
||||
handleNextStep();
|
||||
}
|
||||
setIsExecutingAction(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try direct WebSocket execution first for better performance
|
||||
if (rosConnected) {
|
||||
try {
|
||||
@@ -778,18 +918,12 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use tRPC execution if WebSocket not connected
|
||||
await executeRobotActionMutation.mutateAsync({
|
||||
trialId: trial.id,
|
||||
pluginName,
|
||||
actionId,
|
||||
parameters,
|
||||
});
|
||||
|
||||
toast.success(`Robot action executed: ${actionId}`);
|
||||
if (options?.autoAdvance) {
|
||||
handleNextStep();
|
||||
}
|
||||
// Not connected - show error and don't try to execute
|
||||
const errorMsg = "Robot not connected. Cannot execute action.";
|
||||
toast.error(errorMsg);
|
||||
console.warn(errorMsg);
|
||||
// Throw to stop execution flow
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to execute robot action:", error);
|
||||
@@ -825,7 +959,7 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
// Generic skip logging
|
||||
await logEventMutation.mutateAsync({
|
||||
trialId: trial.id,
|
||||
type: "action_skipped",
|
||||
type: "intervention_action_skipped",
|
||||
data: {
|
||||
actionId,
|
||||
parameters
|
||||
@@ -842,9 +976,17 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
toast.error("Failed to skip action");
|
||||
}
|
||||
},
|
||||
[logRobotActionMutation, trial.id],
|
||||
[logRobotActionMutation, trial.id, logEventMutation, handleNextStep],
|
||||
);
|
||||
|
||||
const handleLogEvent = useCallback((type: string, data?: any) => {
|
||||
logEventMutation.mutate({
|
||||
trialId: trial.id,
|
||||
type,
|
||||
data
|
||||
});
|
||||
}, [logEventMutation, trial.id]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
@@ -869,13 +1011,13 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
{trial.status === "in_progress" && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={isPaused ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={handlePauseTrial}
|
||||
onClick={isPaused ? handleResumeTrial : handlePauseTrial}
|
||||
className="gap-2"
|
||||
>
|
||||
<Pause className="h-4 w-4" />
|
||||
Pause
|
||||
{isPaused ? <Play className="h-4 w-4" /> : <Pause className="h-4 w-4" />}
|
||||
{isPaused ? "Resume" : "Pause"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -922,190 +1064,128 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
className="flex-none px-2 pb-2"
|
||||
/>
|
||||
|
||||
{/* Main Grid - 2 rows */}
|
||||
<div className="flex-1 min-h-0 flex flex-col gap-2 px-2 pb-2">
|
||||
{/* Top Row - 3 Column Layout */}
|
||||
<div className="flex-1 min-h-0 flex gap-2">
|
||||
{/* Left Sidebar - Control Panel (Collapsible) */}
|
||||
{!leftCollapsed && (
|
||||
<div className="flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm w-80">
|
||||
<div className="flex items-center justify-between border-b px-3 py-2 bg-muted/30">
|
||||
<span className="text-sm font-medium">Control</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setLeftCollapsed(true)}
|
||||
>
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden min-h-0 bg-muted/10">
|
||||
<div id="tour-wizard-controls-wrapper" className="h-full">
|
||||
<WizardControlPanel
|
||||
trial={trial}
|
||||
currentStep={currentStep}
|
||||
steps={steps}
|
||||
currentStepIndex={currentStepIndex}
|
||||
onStartTrial={handleStartTrial}
|
||||
onPauseTrial={handlePauseTrial}
|
||||
onNextStep={handleNextStep}
|
||||
onCompleteTrial={handleCompleteTrial}
|
||||
onAbortTrial={handleAbortTrial}
|
||||
onExecuteAction={handleExecuteAction}
|
||||
isStarting={startTrialMutation.isPending}
|
||||
readOnly={trial.status === 'completed' || _userRole === 'observer'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Main Grid - Single Row */}
|
||||
<div className="flex-1 min-h-0 flex gap-2 px-2 pb-2">
|
||||
|
||||
{/* Center - Tabbed Workspace */}
|
||||
{/* Center - Execution Workspace */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm">
|
||||
<div className="flex items-center border-b px-3 py-2 bg-muted/30 min-h-[45px]">
|
||||
{leftCollapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 mr-2"
|
||||
onClick={() => setLeftCollapsed(false)}
|
||||
title="Open Tools Panel"
|
||||
>
|
||||
<PanelLeftOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Trial Execution</span>
|
||||
{currentStep && (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{currentStep.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="mr-2 text-xs text-muted-foreground font-medium">
|
||||
Step {currentStepIndex + 1} / {steps.length}
|
||||
</div>
|
||||
|
||||
{rightCollapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setRightCollapsed(false)}
|
||||
title="Open Robot Status"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* Center - Execution Workspace */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm">
|
||||
<div className="flex items-center border-b px-3 py-2 bg-muted/30 min-h-[45px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Trial Execution</span>
|
||||
{currentStep && (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{currentStep.name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto min-h-0 bg-muted/10">
|
||||
<div id="tour-wizard-timeline" className="h-full">
|
||||
<WizardExecutionPanel
|
||||
trial={trial}
|
||||
currentStep={currentStep}
|
||||
steps={steps}
|
||||
currentStepIndex={currentStepIndex}
|
||||
trialEvents={trialEvents}
|
||||
onStepSelect={(index: number) => setCurrentStepIndex(index)}
|
||||
onExecuteAction={handleExecuteAction}
|
||||
onExecuteRobotAction={handleExecuteRobotAction}
|
||||
activeTab={executionPanelTab}
|
||||
onTabChange={setExecutionPanelTab}
|
||||
onSkipAction={handleSkipAction}
|
||||
isExecuting={isExecutingAction}
|
||||
onNextStep={handleNextStep}
|
||||
completedActionsCount={completedActionsCount}
|
||||
onActionCompleted={() => setCompletedActionsCount(c => c + 1)}
|
||||
onCompleteTrial={handleCompleteTrial}
|
||||
readOnly={trial.status === 'completed' || _userRole === 'observer'}
|
||||
rosConnected={rosConnected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Robot Status (Collapsible) */}
|
||||
{!rightCollapsed && (
|
||||
<div className="flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm w-80">
|
||||
<div className="flex items-center justify-between border-b px-3 py-2 bg-muted/30">
|
||||
<span className="text-sm font-medium">Robot Control & Status</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setRightCollapsed(true)}
|
||||
>
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto min-h-0 bg-muted/10">
|
||||
<div id="tour-wizard-robot-status" className="h-full">
|
||||
<WizardMonitoringPanel
|
||||
rosConnected={rosConnected}
|
||||
rosConnecting={rosConnecting}
|
||||
rosError={rosError ?? undefined}
|
||||
robotStatus={robotStatus}
|
||||
connectRos={connectRos}
|
||||
disconnectRos={disconnectRos}
|
||||
executeRosAction={executeRosAction}
|
||||
onSetAutonomousLife={setAutonomousLife}
|
||||
onExecuteRobotAction={handleExecuteRobotAction}
|
||||
studyId={trial.experiment.studyId}
|
||||
trialId={trial.id}
|
||||
readOnly={trial.status === 'completed' || _userRole === 'observer'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Bottom Row - Observations (Full Width, Collapsible) */}
|
||||
{!obsCollapsed && (
|
||||
<div className="flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm h-48 flex-none">
|
||||
<div className="flex items-center border-b px-3 py-2 bg-muted/30 gap-3">
|
||||
<span className="text-sm font-medium">Observations</span>
|
||||
<div className="flex-1" />
|
||||
<div className="mr-2 text-xs text-muted-foreground font-medium">
|
||||
Step {currentStepIndex + 1} / {steps.length}
|
||||
</div>
|
||||
|
||||
{rightCollapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setObsCollapsed(true)}
|
||||
onClick={() => setRightCollapsed(false)}
|
||||
title="Open Status & Tools"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto min-h-0 bg-muted/10">
|
||||
<WizardObservationPane
|
||||
onAddAnnotation={handleAddAnnotation}
|
||||
isSubmitting={addAnnotationMutation.isPending}
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto bg-muted/10 pb-0">
|
||||
<div id="tour-wizard-timeline" className="h-full">
|
||||
<WizardExecutionPanel
|
||||
trial={trial}
|
||||
currentStep={currentStep}
|
||||
steps={steps}
|
||||
currentStepIndex={currentStepIndex}
|
||||
completedStepIndices={completedSteps}
|
||||
trialEvents={trialEvents}
|
||||
readOnly={trial.status === 'completed'}
|
||||
isPaused={isPaused}
|
||||
onStepSelect={handleStepSelect}
|
||||
onExecuteAction={handleExecuteAction}
|
||||
onExecuteRobotAction={handleExecuteRobotAction}
|
||||
activeTab={executionPanelTab}
|
||||
onTabChange={setExecutionPanelTab}
|
||||
onSkipAction={handleSkipAction}
|
||||
isExecuting={isExecutingAction}
|
||||
onNextStep={handleNextStep}
|
||||
completedActionsCount={completedActionsCount}
|
||||
onActionCompleted={() => setCompletedActionsCount(c => c + 1)}
|
||||
onCompleteTrial={handleCompleteTrial}
|
||||
readOnly={trial.status === 'completed' || _userRole === 'observer'}
|
||||
rosConnected={rosConnected}
|
||||
onLogEvent={handleLogEvent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
obsCollapsed && (
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar - Tools Tabs (Collapsible) */}
|
||||
<div className={cn(
|
||||
"flex flex-col overflow-hidden rounded-lg border bg-background shadow-sm w-[350px] lg:w-[400px]",
|
||||
rightCollapsed && "hidden"
|
||||
)}>
|
||||
<div className="flex items-center justify-between border-b px-3 py-2 bg-muted/30 shrink-0">
|
||||
<span className="text-sm font-medium">Tools</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setObsCollapsed(false)}
|
||||
className="w-full flex-none"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setRightCollapsed(true)}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4 mr-2" />
|
||||
Show Observations
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
</div >
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden bg-background">
|
||||
<Tabs defaultValue="camera_obs" className="flex flex-col h-full w-full">
|
||||
<TabsList className="w-full justify-start rounded-none border-b bg-muted/30 px-3 py-1 shrink-0 h-10">
|
||||
<TabsTrigger value="camera_obs" className="text-xs flex-1">Camera & Obs</TabsTrigger>
|
||||
<TabsTrigger value="robot" className="text-xs flex-1">Robot Control</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="camera_obs" className="flex-1 flex flex-col m-0 p-0 h-full overflow-hidden min-h-0">
|
||||
<div className="flex-none bg-muted/30 border-b h-48 sm:h-56 relative group shrink-0">
|
||||
<WebcamPanel readOnly={trial.status === 'completed'} trialId={trial.id} trialStatus={trial.status} />
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto min-h-0 bg-muted/10">
|
||||
<WizardObservationPane
|
||||
onAddAnnotation={handleAddAnnotation}
|
||||
onFlagIntervention={() => handleExecuteAction("intervene")}
|
||||
isSubmitting={addAnnotationMutation.isPending}
|
||||
trialEvents={trialEvents}
|
||||
readOnly={trial.status === 'completed'}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="robot" className="flex-1 m-0 h-full overflow-hidden">
|
||||
<WizardMonitoringPanel
|
||||
rosConnected={rosConnected}
|
||||
rosConnecting={rosConnecting}
|
||||
rosError={rosError ?? undefined}
|
||||
robotStatus={robotStatus}
|
||||
connectRos={connectRos}
|
||||
disconnectRos={disconnectRos}
|
||||
executeRosAction={executeRosAction}
|
||||
onSetAutonomousLife={setAutonomousLife}
|
||||
onExecuteRobotAction={handleExecuteRobotAction}
|
||||
studyId={trial.experiment.studyId}
|
||||
trialId={trial.id}
|
||||
trialStatus={trial.status}
|
||||
readOnly={trial.status === 'completed' || _userRole === 'observer'}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user