Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogHeader, | |
| DialogTitle, | |
| DialogDescription, | |
| } from "@/components/ui/dialog"; | |
| import PortDetectionModal from "@/components/ui/PortDetectionModal"; | |
| import PortDetectionButton from "@/components/ui/PortDetectionButton"; | |
| import CameraConfiguration, { | |
| CameraConfig, | |
| } from "@/components/recording/CameraConfiguration"; | |
| import { useApi } from "@/contexts/ApiContext"; | |
| import { useAutoSave } from "@/hooks/useAutoSave"; | |
| interface RecordingModalProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| leaderPort: string; | |
| setLeaderPort: (value: string) => void; | |
| followerPort: string; | |
| setFollowerPort: (value: string) => void; | |
| leaderConfig: string; | |
| setLeaderConfig: (value: string) => void; | |
| followerConfig: string; | |
| setFollowerConfig: (value: string) => void; | |
| leaderConfigs: string[]; | |
| followerConfigs: string[]; | |
| datasetRepoId: string; | |
| setDatasetRepoId: (value: string) => void; | |
| singleTask: string; | |
| setSingleTask: (value: string) => void; | |
| numEpisodes: number; | |
| setNumEpisodes: (value: number) => void; | |
| cameras: CameraConfig[]; | |
| setCameras: (cameras: CameraConfig[]) => void; | |
| isLoadingConfigs: boolean; | |
| onStart: () => void; | |
| releaseStreamsRef?: React.MutableRefObject<(() => void) | null>; | |
| } | |
| const RecordingModal: React.FC<RecordingModalProps> = ({ | |
| open, | |
| onOpenChange, | |
| leaderPort, | |
| setLeaderPort, | |
| followerPort, | |
| setFollowerPort, | |
| leaderConfig, | |
| setLeaderConfig, | |
| followerConfig, | |
| setFollowerConfig, | |
| leaderConfigs, | |
| followerConfigs, | |
| datasetRepoId, | |
| setDatasetRepoId, | |
| singleTask, | |
| setSingleTask, | |
| numEpisodes, | |
| setNumEpisodes, | |
| cameras, | |
| setCameras, | |
| isLoadingConfigs, | |
| onStart, | |
| releaseStreamsRef, | |
| }) => { | |
| const { baseUrl, fetchWithHeaders } = useApi(); | |
| const { debouncedSavePort, debouncedSaveConfig } = useAutoSave(); | |
| const [showPortDetection, setShowPortDetection] = useState(false); | |
| const [detectionRobotType, setDetectionRobotType] = useState< | |
| "leader" | "follower" | |
| >("leader"); | |
| const handlePortDetection = (robotType: "leader" | "follower") => { | |
| setDetectionRobotType(robotType); | |
| setShowPortDetection(true); | |
| }; | |
| const handlePortDetected = (port: string) => { | |
| if (detectionRobotType === "leader") { | |
| setLeaderPort(port); | |
| } else { | |
| setFollowerPort(port); | |
| } | |
| }; | |
| // Enhanced port change handlers that save automatically | |
| const handleLeaderPortChange = (value: string) => { | |
| setLeaderPort(value); | |
| // Auto-save with debouncing to avoid excessive API calls | |
| debouncedSavePort("leader", value); | |
| }; | |
| const handleFollowerPortChange = (value: string) => { | |
| setFollowerPort(value); | |
| // Auto-save with debouncing to avoid excessive API calls | |
| debouncedSavePort("follower", value); | |
| }; | |
| // Enhanced config change handlers that save automatically | |
| const handleLeaderConfigChange = (value: string) => { | |
| setLeaderConfig(value); | |
| // Auto-save with debouncing to avoid excessive API calls | |
| debouncedSaveConfig("leader", value); | |
| }; | |
| const handleFollowerConfigChange = (value: string) => { | |
| setFollowerConfig(value); | |
| // Auto-save with debouncing to avoid excessive API calls | |
| debouncedSaveConfig("follower", value); | |
| }; | |
| // Load saved ports and configurations on component mount | |
| useEffect(() => { | |
| const loadSavedData = async () => { | |
| try { | |
| // Load leader port | |
| const leaderResponse = await fetchWithHeaders( | |
| `${baseUrl}/robot-port/leader` | |
| ); | |
| const leaderData = await leaderResponse.json(); | |
| if (leaderData.status === "success" && leaderData.default_port) { | |
| setLeaderPort(leaderData.default_port); | |
| } | |
| // Load follower port | |
| const followerResponse = await fetchWithHeaders( | |
| `${baseUrl}/robot-port/follower` | |
| ); | |
| const followerData = await followerResponse.json(); | |
| if (followerData.status === "success" && followerData.default_port) { | |
| setFollowerPort(followerData.default_port); | |
| } | |
| // Load leader configuration | |
| const leaderConfigResponse = await fetchWithHeaders( | |
| `${baseUrl}/robot-config/leader?available_configs=${leaderConfigs.join( | |
| "," | |
| )}` | |
| ); | |
| const leaderConfigData = await leaderConfigResponse.json(); | |
| if ( | |
| leaderConfigData.status === "success" && | |
| leaderConfigData.default_config | |
| ) { | |
| setLeaderConfig(leaderConfigData.default_config); | |
| } | |
| // Load follower configuration | |
| const followerConfigResponse = await fetchWithHeaders( | |
| `${baseUrl}/robot-config/follower?available_configs=${followerConfigs.join( | |
| "," | |
| )}` | |
| ); | |
| const followerConfigData = await followerConfigResponse.json(); | |
| if ( | |
| followerConfigData.status === "success" && | |
| followerConfigData.default_config | |
| ) { | |
| setFollowerConfig(followerConfigData.default_config); | |
| } | |
| } catch (error) { | |
| console.error("Error loading saved data:", error); | |
| } | |
| }; | |
| if (open && leaderConfigs.length > 0 && followerConfigs.length > 0) { | |
| loadSavedData(); | |
| } | |
| }, [ | |
| open, | |
| setLeaderPort, | |
| setFollowerPort, | |
| setLeaderConfig, | |
| setFollowerConfig, | |
| leaderConfigs, | |
| followerConfigs, | |
| baseUrl, | |
| fetchWithHeaders, | |
| ]); | |
| return ( | |
| <> | |
| <Dialog open={open} onOpenChange={onOpenChange}> | |
| <DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-[600px] p-8 max-h-[90vh] overflow-y-auto"> | |
| <DialogHeader> | |
| <div className="flex justify-center items-center mb-4"> | |
| <div className="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center"> | |
| <span className="text-white font-bold text-sm">REC</span> | |
| </div> | |
| </div> | |
| <DialogTitle className="text-white text-center text-2xl font-bold"> | |
| Configure Recording | |
| </DialogTitle> | |
| </DialogHeader> | |
| <div className="space-y-6 py-4"> | |
| <DialogDescription className="text-gray-400 text-base leading-relaxed text-center"> | |
| Configure the robot arm settings and dataset parameters for | |
| recording. | |
| </DialogDescription> | |
| <div className="grid grid-cols-1 gap-6"> | |
| <div className="space-y-4"> | |
| <h3 className="text-lg font-semibold text-white border-b border-gray-700 pb-2"> | |
| Robot Configuration | |
| </h3> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="recordLeaderPort" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Leader Port | |
| </Label> | |
| <div className="flex gap-2"> | |
| <Input | |
| id="recordLeaderPort" | |
| value={leaderPort} | |
| onChange={(e) => handleLeaderPortChange(e.target.value)} | |
| placeholder="/dev/tty.usbmodem5A460816421" | |
| className="bg-gray-800 border-gray-700 text-white flex-1" | |
| /> | |
| <PortDetectionButton | |
| onClick={() => handlePortDetection("leader")} | |
| robotType="leader" | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="recordLeaderConfig" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Leader Calibration Config | |
| </Label> | |
| <Select | |
| value={leaderConfig} | |
| onValueChange={handleLeaderConfigChange} | |
| > | |
| <SelectTrigger className="bg-gray-800 border-gray-700 text-white"> | |
| <SelectValue | |
| placeholder={ | |
| isLoadingConfigs | |
| ? "Loading configs..." | |
| : "Select leader config" | |
| } | |
| /> | |
| </SelectTrigger> | |
| <SelectContent className="bg-gray-800 border-gray-700"> | |
| {leaderConfigs.map((config) => ( | |
| <SelectItem | |
| key={config} | |
| value={config} | |
| className="text-white hover:bg-gray-700" | |
| > | |
| {config} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="recordFollowerPort" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Follower Port | |
| </Label> | |
| <div className="flex gap-2"> | |
| <Input | |
| id="recordFollowerPort" | |
| value={followerPort} | |
| onChange={(e) => | |
| handleFollowerPortChange(e.target.value) | |
| } | |
| placeholder="/dev/tty.usbmodem5A460816621" | |
| className="bg-gray-800 border-gray-700 text-white flex-1" | |
| /> | |
| <PortDetectionButton | |
| onClick={() => handlePortDetection("follower")} | |
| robotType="follower" | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="recordFollowerConfig" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Follower Calibration Config | |
| </Label> | |
| <Select | |
| value={followerConfig} | |
| onValueChange={handleFollowerConfigChange} | |
| > | |
| <SelectTrigger className="bg-gray-800 border-gray-700 text-white"> | |
| <SelectValue | |
| placeholder={ | |
| isLoadingConfigs | |
| ? "Loading configs..." | |
| : "Select follower config" | |
| } | |
| /> | |
| </SelectTrigger> | |
| <SelectContent className="bg-gray-800 border-gray-700"> | |
| {followerConfigs.map((config) => ( | |
| <SelectItem | |
| key={config} | |
| value={config} | |
| className="text-white hover:bg-gray-700" | |
| > | |
| {config} | |
| </SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <h3 className="text-lg font-semibold text-white border-b border-gray-700 pb-2"> | |
| Dataset Configuration | |
| </h3> | |
| <div className="grid grid-cols-1 gap-4"> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="datasetRepoId" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Dataset Repository ID * | |
| </Label> | |
| <Input | |
| id="datasetRepoId" | |
| value={datasetRepoId} | |
| onChange={(e) => setDatasetRepoId(e.target.value)} | |
| placeholder="username/dataset_name" | |
| className="bg-gray-800 border-gray-700 text-white" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="singleTask" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Task Name * | |
| </Label> | |
| <Input | |
| id="singleTask" | |
| value={singleTask} | |
| onChange={(e) => setSingleTask(e.target.value)} | |
| placeholder="e.g., pick_and_place" | |
| className="bg-gray-800 border-gray-700 text-white" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label | |
| htmlFor="numEpisodes" | |
| className="text-sm font-medium text-gray-300" | |
| > | |
| Number of Episodes | |
| </Label> | |
| <Input | |
| id="numEpisodes" | |
| type="number" | |
| min="1" | |
| max="100" | |
| value={numEpisodes} | |
| onChange={(e) => setNumEpisodes(parseInt(e.target.value))} | |
| className="bg-gray-800 border-gray-700 text-white" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <CameraConfiguration | |
| cameras={cameras} | |
| onCamerasChange={setCameras} | |
| releaseStreamsRef={releaseStreamsRef} | |
| /> | |
| </div> | |
| </div> | |
| <div className="flex flex-col sm:flex-row gap-4 justify-center pt-4"> | |
| <Button | |
| onClick={onStart} | |
| className="w-full sm:w-auto bg-red-500 hover:bg-red-600 text-white px-10 py-6 text-lg transition-all shadow-md shadow-red-500/30 hover:shadow-lg hover:shadow-red-500/40" | |
| disabled={isLoadingConfigs} | |
| > | |
| Start Recording | |
| </Button> | |
| <Button | |
| onClick={() => onOpenChange(false)} | |
| variant="outline" | |
| className="w-full sm:w-auto border-gray-500 hover:border-gray-200 px-10 py-6 text-lg text-zinc-500 bg-zinc-900 hover:bg-zinc-800" | |
| > | |
| Cancel | |
| </Button> | |
| </div> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| <PortDetectionModal | |
| open={showPortDetection} | |
| onOpenChange={setShowPortDetection} | |
| robotType={detectionRobotType} | |
| onPortDetected={handlePortDetected} | |
| /> | |
| </> | |
| ); | |
| }; | |
| export default RecordingModal; | |