Spaces:
Running
Running
Merge pull request #1 from DavidLMS/save-port-on-every-change
Browse files
src/components/landing/DirectFollowerModal.tsx
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
| 20 |
import { Settings } from "lucide-react";
|
| 21 |
import PortDetectionModal from "@/components/ui/PortDetectionModal";
|
| 22 |
import PortDetectionButton from "@/components/ui/PortDetectionButton";
|
|
|
|
|
|
|
| 23 |
|
| 24 |
interface DirectFollowerModalProps {
|
| 25 |
open: boolean;
|
|
@@ -44,28 +46,40 @@ const DirectFollowerModal: React.FC<DirectFollowerModalProps> = ({
|
|
| 44 |
isLoadingConfigs,
|
| 45 |
onStart,
|
| 46 |
}) => {
|
|
|
|
|
|
|
| 47 |
const [showPortDetection, setShowPortDetection] = useState(false);
|
| 48 |
|
| 49 |
-
// Load saved follower port on component mount
|
| 50 |
useEffect(() => {
|
| 51 |
-
const
|
| 52 |
try {
|
| 53 |
-
|
| 54 |
-
|
|
|
|
| 55 |
);
|
| 56 |
const followerData = await followerResponse.json();
|
| 57 |
if (followerData.status === "success" && followerData.default_port) {
|
| 58 |
setFollowerPort(followerData.default_port);
|
| 59 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
} catch (error) {
|
| 61 |
-
console.error("Error loading saved
|
| 62 |
}
|
| 63 |
};
|
| 64 |
|
| 65 |
-
if (open) {
|
| 66 |
-
|
| 67 |
}
|
| 68 |
-
}, [open, setFollowerPort]);
|
| 69 |
|
| 70 |
const handlePortDetection = () => {
|
| 71 |
setShowPortDetection(true);
|
|
@@ -75,6 +89,20 @@ const DirectFollowerModal: React.FC<DirectFollowerModalProps> = ({
|
|
| 75 |
setFollowerPort(port);
|
| 76 |
};
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
return (
|
| 79 |
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 80 |
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-[500px] p-8">
|
|
@@ -103,7 +131,7 @@ const DirectFollowerModal: React.FC<DirectFollowerModalProps> = ({
|
|
| 103 |
<Input
|
| 104 |
id="followerPort"
|
| 105 |
value={followerPort}
|
| 106 |
-
onChange={(e) =>
|
| 107 |
placeholder="/dev/tty.usbmodem5A460816621"
|
| 108 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 109 |
/>
|
|
@@ -121,7 +149,7 @@ const DirectFollowerModal: React.FC<DirectFollowerModalProps> = ({
|
|
| 121 |
>
|
| 122 |
Follower Calibration Config
|
| 123 |
</Label>
|
| 124 |
-
<Select value={followerConfig} onValueChange={
|
| 125 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 126 |
<SelectValue
|
| 127 |
placeholder={
|
|
|
|
| 20 |
import { Settings } from "lucide-react";
|
| 21 |
import PortDetectionModal from "@/components/ui/PortDetectionModal";
|
| 22 |
import PortDetectionButton from "@/components/ui/PortDetectionButton";
|
| 23 |
+
import { useApi } from "@/contexts/ApiContext";
|
| 24 |
+
import { useAutoSave } from "@/hooks/useAutoSave";
|
| 25 |
|
| 26 |
interface DirectFollowerModalProps {
|
| 27 |
open: boolean;
|
|
|
|
| 46 |
isLoadingConfigs,
|
| 47 |
onStart,
|
| 48 |
}) => {
|
| 49 |
+
const { baseUrl, fetchWithHeaders } = useApi();
|
| 50 |
+
const { debouncedSavePort, debouncedSaveConfig } = useAutoSave();
|
| 51 |
const [showPortDetection, setShowPortDetection] = useState(false);
|
| 52 |
|
| 53 |
+
// Load saved follower port and configuration on component mount
|
| 54 |
useEffect(() => {
|
| 55 |
+
const loadSavedData = async () => {
|
| 56 |
try {
|
| 57 |
+
// Load follower port
|
| 58 |
+
const followerResponse = await fetchWithHeaders(
|
| 59 |
+
`${baseUrl}/robot-port/follower`
|
| 60 |
);
|
| 61 |
const followerData = await followerResponse.json();
|
| 62 |
if (followerData.status === "success" && followerData.default_port) {
|
| 63 |
setFollowerPort(followerData.default_port);
|
| 64 |
}
|
| 65 |
+
|
| 66 |
+
// Load follower configuration
|
| 67 |
+
const followerConfigResponse = await fetchWithHeaders(
|
| 68 |
+
`${baseUrl}/robot-config/follower?available_configs=${followerConfigs.join(',')}`
|
| 69 |
+
);
|
| 70 |
+
const followerConfigData = await followerConfigResponse.json();
|
| 71 |
+
if (followerConfigData.status === "success" && followerConfigData.default_config) {
|
| 72 |
+
setFollowerConfig(followerConfigData.default_config);
|
| 73 |
+
}
|
| 74 |
} catch (error) {
|
| 75 |
+
console.error("Error loading saved data:", error);
|
| 76 |
}
|
| 77 |
};
|
| 78 |
|
| 79 |
+
if (open && followerConfigs.length > 0) {
|
| 80 |
+
loadSavedData();
|
| 81 |
}
|
| 82 |
+
}, [open, setFollowerPort, setFollowerConfig, followerConfigs, baseUrl, fetchWithHeaders]);
|
| 83 |
|
| 84 |
const handlePortDetection = () => {
|
| 85 |
setShowPortDetection(true);
|
|
|
|
| 89 |
setFollowerPort(port);
|
| 90 |
};
|
| 91 |
|
| 92 |
+
// Enhanced port change handler that saves automatically
|
| 93 |
+
const handleFollowerPortChange = (value: string) => {
|
| 94 |
+
setFollowerPort(value);
|
| 95 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 96 |
+
debouncedSavePort("follower", value);
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
// Enhanced config change handler that saves automatically
|
| 100 |
+
const handleFollowerConfigChange = (value: string) => {
|
| 101 |
+
setFollowerConfig(value);
|
| 102 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 103 |
+
debouncedSaveConfig("follower", value);
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
return (
|
| 107 |
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 108 |
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-[500px] p-8">
|
|
|
|
| 131 |
<Input
|
| 132 |
id="followerPort"
|
| 133 |
value={followerPort}
|
| 134 |
+
onChange={(e) => handleFollowerPortChange(e.target.value)}
|
| 135 |
placeholder="/dev/tty.usbmodem5A460816621"
|
| 136 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 137 |
/>
|
|
|
|
| 149 |
>
|
| 150 |
Follower Calibration Config
|
| 151 |
</Label>
|
| 152 |
+
<Select value={followerConfig} onValueChange={handleFollowerConfigChange}>
|
| 153 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 154 |
<SelectValue
|
| 155 |
placeholder={
|
src/components/landing/RecordingModal.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import PortDetectionModal from "@/components/ui/PortDetectionModal";
|
|
| 21 |
import PortDetectionButton from "@/components/ui/PortDetectionButton";
|
| 22 |
import QrCodeModal from "@/components/recording/QrCodeModal";
|
| 23 |
import { useApi } from "@/contexts/ApiContext";
|
|
|
|
| 24 |
interface RecordingModalProps {
|
| 25 |
open: boolean;
|
| 26 |
onOpenChange: (open: boolean) => void;
|
|
@@ -66,6 +67,7 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 66 |
onStart,
|
| 67 |
}) => {
|
| 68 |
const { baseUrl, fetchWithHeaders } = useApi();
|
|
|
|
| 69 |
const [showPortDetection, setShowPortDetection] = useState(false);
|
| 70 |
const [detectionRobotType, setDetectionRobotType] = useState<
|
| 71 |
"leader" | "follower"
|
|
@@ -73,9 +75,46 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 73 |
const [showQrCodeModal, setShowQrCodeModal] = useState(false);
|
| 74 |
const [sessionId, setSessionId] = useState("");
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
useEffect(() => {
|
| 78 |
-
const
|
| 79 |
try {
|
| 80 |
// Load leader port
|
| 81 |
const leaderResponse = await fetchWithHeaders(
|
|
@@ -94,25 +133,34 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 94 |
if (followerData.status === "success" && followerData.default_port) {
|
| 95 |
setFollowerPort(followerData.default_port);
|
| 96 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
} catch (error) {
|
| 98 |
-
console.error("Error loading saved
|
| 99 |
}
|
| 100 |
};
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
}, [open, setLeaderPort, setFollowerPort]);
|
| 105 |
-
const handlePortDetection = (robotType: "leader" | "follower") => {
|
| 106 |
-
setDetectionRobotType(robotType);
|
| 107 |
-
setShowPortDetection(true);
|
| 108 |
-
};
|
| 109 |
-
const handlePortDetected = (port: string) => {
|
| 110 |
-
if (detectionRobotType === "leader") {
|
| 111 |
-
setLeaderPort(port);
|
| 112 |
-
} else {
|
| 113 |
-
setFollowerPort(port);
|
| 114 |
}
|
| 115 |
-
};
|
|
|
|
| 116 |
const handleQrCodeClick = () => {
|
| 117 |
// Generate a session ID for this recording session
|
| 118 |
const newSessionId = `recording_${Date.now()}_${Math.random()
|
|
@@ -175,7 +223,7 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 175 |
<Input
|
| 176 |
id="recordLeaderPort"
|
| 177 |
value={leaderPort}
|
| 178 |
-
onChange={(e) =>
|
| 179 |
placeholder="/dev/tty.usbmodem5A460816421"
|
| 180 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 181 |
/>
|
|
@@ -194,7 +242,7 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 194 |
</Label>
|
| 195 |
<Select
|
| 196 |
value={leaderConfig}
|
| 197 |
-
onValueChange={
|
| 198 |
>
|
| 199 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 200 |
<SelectValue
|
|
@@ -229,7 +277,7 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 229 |
<Input
|
| 230 |
id="recordFollowerPort"
|
| 231 |
value={followerPort}
|
| 232 |
-
onChange={(e) =>
|
| 233 |
placeholder="/dev/tty.usbmodem5A460816621"
|
| 234 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 235 |
/>
|
|
@@ -248,7 +296,7 @@ const RecordingModal: React.FC<RecordingModalProps> = ({
|
|
| 248 |
</Label>
|
| 249 |
<Select
|
| 250 |
value={followerConfig}
|
| 251 |
-
onValueChange={
|
| 252 |
>
|
| 253 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 254 |
<SelectValue
|
|
|
|
| 21 |
import PortDetectionButton from "@/components/ui/PortDetectionButton";
|
| 22 |
import QrCodeModal from "@/components/recording/QrCodeModal";
|
| 23 |
import { useApi } from "@/contexts/ApiContext";
|
| 24 |
+
import { useAutoSave } from "@/hooks/useAutoSave";
|
| 25 |
interface RecordingModalProps {
|
| 26 |
open: boolean;
|
| 27 |
onOpenChange: (open: boolean) => void;
|
|
|
|
| 67 |
onStart,
|
| 68 |
}) => {
|
| 69 |
const { baseUrl, fetchWithHeaders } = useApi();
|
| 70 |
+
const { debouncedSavePort, debouncedSaveConfig } = useAutoSave();
|
| 71 |
const [showPortDetection, setShowPortDetection] = useState(false);
|
| 72 |
const [detectionRobotType, setDetectionRobotType] = useState<
|
| 73 |
"leader" | "follower"
|
|
|
|
| 75 |
const [showQrCodeModal, setShowQrCodeModal] = useState(false);
|
| 76 |
const [sessionId, setSessionId] = useState("");
|
| 77 |
|
| 78 |
+
const handlePortDetection = (robotType: "leader" | "follower") => {
|
| 79 |
+
setDetectionRobotType(robotType);
|
| 80 |
+
setShowPortDetection(true);
|
| 81 |
+
};
|
| 82 |
+
const handlePortDetected = (port: string) => {
|
| 83 |
+
if (detectionRobotType === "leader") {
|
| 84 |
+
setLeaderPort(port);
|
| 85 |
+
} else {
|
| 86 |
+
setFollowerPort(port);
|
| 87 |
+
}
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// Enhanced port change handlers that save automatically
|
| 91 |
+
const handleLeaderPortChange = (value: string) => {
|
| 92 |
+
setLeaderPort(value);
|
| 93 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 94 |
+
debouncedSavePort("leader", value);
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const handleFollowerPortChange = (value: string) => {
|
| 98 |
+
setFollowerPort(value);
|
| 99 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 100 |
+
debouncedSavePort("follower", value);
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
// Enhanced config change handlers that save automatically
|
| 104 |
+
const handleLeaderConfigChange = (value: string) => {
|
| 105 |
+
setLeaderConfig(value);
|
| 106 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 107 |
+
debouncedSaveConfig("leader", value);
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const handleFollowerConfigChange = (value: string) => {
|
| 111 |
+
setFollowerConfig(value);
|
| 112 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 113 |
+
debouncedSaveConfig("follower", value);
|
| 114 |
+
};
|
| 115 |
+
// Load saved ports and configurations on component mount
|
| 116 |
useEffect(() => {
|
| 117 |
+
const loadSavedData = async () => {
|
| 118 |
try {
|
| 119 |
// Load leader port
|
| 120 |
const leaderResponse = await fetchWithHeaders(
|
|
|
|
| 133 |
if (followerData.status === "success" && followerData.default_port) {
|
| 134 |
setFollowerPort(followerData.default_port);
|
| 135 |
}
|
| 136 |
+
|
| 137 |
+
// Load leader configuration
|
| 138 |
+
const leaderConfigResponse = await fetchWithHeaders(
|
| 139 |
+
`${baseUrl}/robot-config/leader?available_configs=${leaderConfigs.join(',')}`
|
| 140 |
+
);
|
| 141 |
+
const leaderConfigData = await leaderConfigResponse.json();
|
| 142 |
+
if (leaderConfigData.status === "success" && leaderConfigData.default_config) {
|
| 143 |
+
setLeaderConfig(leaderConfigData.default_config);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Load follower configuration
|
| 147 |
+
const followerConfigResponse = await fetchWithHeaders(
|
| 148 |
+
`${baseUrl}/robot-config/follower?available_configs=${followerConfigs.join(',')}`
|
| 149 |
+
);
|
| 150 |
+
const followerConfigData = await followerConfigResponse.json();
|
| 151 |
+
if (followerConfigData.status === "success" && followerConfigData.default_config) {
|
| 152 |
+
setFollowerConfig(followerConfigData.default_config);
|
| 153 |
+
}
|
| 154 |
} catch (error) {
|
| 155 |
+
console.error("Error loading saved data:", error);
|
| 156 |
}
|
| 157 |
};
|
| 158 |
+
|
| 159 |
+
if (open && leaderConfigs.length > 0 && followerConfigs.length > 0) {
|
| 160 |
+
loadSavedData();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
+
}, [open, setLeaderPort, setFollowerPort, setLeaderConfig, setFollowerConfig, leaderConfigs, followerConfigs, baseUrl, fetchWithHeaders]);
|
| 163 |
+
|
| 164 |
const handleQrCodeClick = () => {
|
| 165 |
// Generate a session ID for this recording session
|
| 166 |
const newSessionId = `recording_${Date.now()}_${Math.random()
|
|
|
|
| 223 |
<Input
|
| 224 |
id="recordLeaderPort"
|
| 225 |
value={leaderPort}
|
| 226 |
+
onChange={(e) => handleLeaderPortChange(e.target.value)}
|
| 227 |
placeholder="/dev/tty.usbmodem5A460816421"
|
| 228 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 229 |
/>
|
|
|
|
| 242 |
</Label>
|
| 243 |
<Select
|
| 244 |
value={leaderConfig}
|
| 245 |
+
onValueChange={handleLeaderConfigChange}
|
| 246 |
>
|
| 247 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 248 |
<SelectValue
|
|
|
|
| 277 |
<Input
|
| 278 |
id="recordFollowerPort"
|
| 279 |
value={followerPort}
|
| 280 |
+
onChange={(e) => handleFollowerPortChange(e.target.value)}
|
| 281 |
placeholder="/dev/tty.usbmodem5A460816621"
|
| 282 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 283 |
/>
|
|
|
|
| 296 |
</Label>
|
| 297 |
<Select
|
| 298 |
value={followerConfig}
|
| 299 |
+
onValueChange={handleFollowerConfigChange}
|
| 300 |
>
|
| 301 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 302 |
<SelectValue
|
src/components/landing/TeleoperationModal.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { Settings } from "lucide-react";
|
|
| 20 |
import PortDetectionModal from "@/components/ui/PortDetectionModal";
|
| 21 |
import PortDetectionButton from "@/components/ui/PortDetectionButton";
|
| 22 |
import { useApi } from "@/contexts/ApiContext";
|
|
|
|
| 23 |
|
| 24 |
interface TeleoperationModalProps {
|
| 25 |
open: boolean;
|
|
@@ -55,14 +56,15 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 55 |
onStart,
|
| 56 |
}) => {
|
| 57 |
const { baseUrl, fetchWithHeaders } = useApi();
|
|
|
|
| 58 |
const [showPortDetection, setShowPortDetection] = useState(false);
|
| 59 |
const [detectionRobotType, setDetectionRobotType] = useState<
|
| 60 |
"leader" | "follower"
|
| 61 |
>("leader");
|
| 62 |
|
| 63 |
-
// Load saved ports on component mount
|
| 64 |
useEffect(() => {
|
| 65 |
-
const
|
| 66 |
try {
|
| 67 |
// Load leader port
|
| 68 |
const leaderResponse = await fetchWithHeaders(
|
|
@@ -81,15 +83,33 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 81 |
if (followerData.status === "success" && followerData.default_port) {
|
| 82 |
setFollowerPort(followerData.default_port);
|
| 83 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
} catch (error) {
|
| 85 |
-
console.error("Error loading saved
|
| 86 |
}
|
| 87 |
};
|
| 88 |
|
| 89 |
-
if (open) {
|
| 90 |
-
|
| 91 |
}
|
| 92 |
-
}, [open, setLeaderPort, setFollowerPort]);
|
| 93 |
|
| 94 |
const handlePortDetection = (robotType: "leader" | "follower") => {
|
| 95 |
setDetectionRobotType(robotType);
|
|
@@ -103,6 +123,32 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 103 |
setFollowerPort(port);
|
| 104 |
}
|
| 105 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
return (
|
| 107 |
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 108 |
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-[600px] p-8">
|
|
@@ -132,7 +178,7 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 132 |
<Input
|
| 133 |
id="leaderPort"
|
| 134 |
value={leaderPort}
|
| 135 |
-
onChange={(e) =>
|
| 136 |
placeholder="/dev/tty.usbmodem5A460816421"
|
| 137 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 138 |
/>
|
|
@@ -150,7 +196,7 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 150 |
>
|
| 151 |
Leader Calibration Config
|
| 152 |
</Label>
|
| 153 |
-
<Select value={leaderConfig} onValueChange={
|
| 154 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 155 |
<SelectValue
|
| 156 |
placeholder={
|
|
@@ -185,7 +231,7 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 185 |
<Input
|
| 186 |
id="followerPort"
|
| 187 |
value={followerPort}
|
| 188 |
-
onChange={(e) =>
|
| 189 |
placeholder="/dev/tty.usbmodem5A460816621"
|
| 190 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 191 |
/>
|
|
@@ -203,7 +249,7 @@ const TeleoperationModal: React.FC<TeleoperationModalProps> = ({
|
|
| 203 |
>
|
| 204 |
Follower Calibration Config
|
| 205 |
</Label>
|
| 206 |
-
<Select value={followerConfig} onValueChange={
|
| 207 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 208 |
<SelectValue
|
| 209 |
placeholder={
|
|
|
|
| 20 |
import PortDetectionModal from "@/components/ui/PortDetectionModal";
|
| 21 |
import PortDetectionButton from "@/components/ui/PortDetectionButton";
|
| 22 |
import { useApi } from "@/contexts/ApiContext";
|
| 23 |
+
import { useAutoSave } from "@/hooks/useAutoSave";
|
| 24 |
|
| 25 |
interface TeleoperationModalProps {
|
| 26 |
open: boolean;
|
|
|
|
| 56 |
onStart,
|
| 57 |
}) => {
|
| 58 |
const { baseUrl, fetchWithHeaders } = useApi();
|
| 59 |
+
const { debouncedSavePort, debouncedSaveConfig } = useAutoSave();
|
| 60 |
const [showPortDetection, setShowPortDetection] = useState(false);
|
| 61 |
const [detectionRobotType, setDetectionRobotType] = useState<
|
| 62 |
"leader" | "follower"
|
| 63 |
>("leader");
|
| 64 |
|
| 65 |
+
// Load saved ports and configurations on component mount
|
| 66 |
useEffect(() => {
|
| 67 |
+
const loadSavedData = async () => {
|
| 68 |
try {
|
| 69 |
// Load leader port
|
| 70 |
const leaderResponse = await fetchWithHeaders(
|
|
|
|
| 83 |
if (followerData.status === "success" && followerData.default_port) {
|
| 84 |
setFollowerPort(followerData.default_port);
|
| 85 |
}
|
| 86 |
+
|
| 87 |
+
// Load leader configuration
|
| 88 |
+
const leaderConfigResponse = await fetchWithHeaders(
|
| 89 |
+
`${baseUrl}/robot-config/leader?available_configs=${leaderConfigs.join(',')}`
|
| 90 |
+
);
|
| 91 |
+
const leaderConfigData = await leaderConfigResponse.json();
|
| 92 |
+
if (leaderConfigData.status === "success" && leaderConfigData.default_config) {
|
| 93 |
+
setLeaderConfig(leaderConfigData.default_config);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Load follower configuration
|
| 97 |
+
const followerConfigResponse = await fetchWithHeaders(
|
| 98 |
+
`${baseUrl}/robot-config/follower?available_configs=${followerConfigs.join(',')}`
|
| 99 |
+
);
|
| 100 |
+
const followerConfigData = await followerConfigResponse.json();
|
| 101 |
+
if (followerConfigData.status === "success" && followerConfigData.default_config) {
|
| 102 |
+
setFollowerConfig(followerConfigData.default_config);
|
| 103 |
+
}
|
| 104 |
} catch (error) {
|
| 105 |
+
console.error("Error loading saved data:", error);
|
| 106 |
}
|
| 107 |
};
|
| 108 |
|
| 109 |
+
if (open && leaderConfigs.length > 0 && followerConfigs.length > 0) {
|
| 110 |
+
loadSavedData();
|
| 111 |
}
|
| 112 |
+
}, [open, setLeaderPort, setFollowerPort, setLeaderConfig, setFollowerConfig, leaderConfigs, followerConfigs, baseUrl, fetchWithHeaders]);
|
| 113 |
|
| 114 |
const handlePortDetection = (robotType: "leader" | "follower") => {
|
| 115 |
setDetectionRobotType(robotType);
|
|
|
|
| 123 |
setFollowerPort(port);
|
| 124 |
}
|
| 125 |
};
|
| 126 |
+
|
| 127 |
+
// Enhanced port change handlers that save automatically
|
| 128 |
+
const handleLeaderPortChange = (value: string) => {
|
| 129 |
+
setLeaderPort(value);
|
| 130 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 131 |
+
debouncedSavePort("leader", value);
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
const handleFollowerPortChange = (value: string) => {
|
| 135 |
+
setFollowerPort(value);
|
| 136 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 137 |
+
debouncedSavePort("follower", value);
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
// Enhanced config change handlers that save automatically
|
| 141 |
+
const handleLeaderConfigChange = (value: string) => {
|
| 142 |
+
setLeaderConfig(value);
|
| 143 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 144 |
+
debouncedSaveConfig("leader", value);
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const handleFollowerConfigChange = (value: string) => {
|
| 148 |
+
setFollowerConfig(value);
|
| 149 |
+
// Auto-save with debouncing to avoid excessive API calls
|
| 150 |
+
debouncedSaveConfig("follower", value);
|
| 151 |
+
};
|
| 152 |
return (
|
| 153 |
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 154 |
<DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-[600px] p-8">
|
|
|
|
| 178 |
<Input
|
| 179 |
id="leaderPort"
|
| 180 |
value={leaderPort}
|
| 181 |
+
onChange={(e) => handleLeaderPortChange(e.target.value)}
|
| 182 |
placeholder="/dev/tty.usbmodem5A460816421"
|
| 183 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 184 |
/>
|
|
|
|
| 196 |
>
|
| 197 |
Leader Calibration Config
|
| 198 |
</Label>
|
| 199 |
+
<Select value={leaderConfig} onValueChange={handleLeaderConfigChange}>
|
| 200 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 201 |
<SelectValue
|
| 202 |
placeholder={
|
|
|
|
| 231 |
<Input
|
| 232 |
id="followerPort"
|
| 233 |
value={followerPort}
|
| 234 |
+
onChange={(e) => handleFollowerPortChange(e.target.value)}
|
| 235 |
placeholder="/dev/tty.usbmodem5A460816621"
|
| 236 |
className="bg-gray-800 border-gray-700 text-white flex-1"
|
| 237 |
/>
|
|
|
|
| 249 |
>
|
| 250 |
Follower Calibration Config
|
| 251 |
</Label>
|
| 252 |
+
<Select value={followerConfig} onValueChange={handleFollowerConfigChange}>
|
| 253 |
<SelectTrigger className="bg-gray-800 border-gray-700 text-white">
|
| 254 |
<SelectValue
|
| 255 |
placeholder={
|
src/hooks/useAutoSave.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useRef } from 'react';
|
| 2 |
+
import { useApi } from '@/contexts/ApiContext';
|
| 3 |
+
|
| 4 |
+
export const useAutoSave = () => {
|
| 5 |
+
const { baseUrl, fetchWithHeaders } = useApi();
|
| 6 |
+
const timeoutRefs = useRef<{ [key: string]: NodeJS.Timeout }>({});
|
| 7 |
+
const configTimeoutRefs = useRef<{ [key: string]: NodeJS.Timeout }>({});
|
| 8 |
+
|
| 9 |
+
const savePortAutomatically = useCallback(async (robotType: 'leader' | 'follower', port: string) => {
|
| 10 |
+
if (!port.trim()) return;
|
| 11 |
+
|
| 12 |
+
try {
|
| 13 |
+
await fetchWithHeaders(`${baseUrl}/save-robot-port`, {
|
| 14 |
+
method: 'POST',
|
| 15 |
+
headers: {
|
| 16 |
+
'Content-Type': 'application/json',
|
| 17 |
+
},
|
| 18 |
+
body: JSON.stringify({
|
| 19 |
+
robot_type: robotType,
|
| 20 |
+
port: port.trim(),
|
| 21 |
+
}),
|
| 22 |
+
});
|
| 23 |
+
console.log(`Auto-saved ${robotType} port: ${port}`);
|
| 24 |
+
} catch (error) {
|
| 25 |
+
console.error(`Error saving ${robotType} port:`, error);
|
| 26 |
+
}
|
| 27 |
+
}, [baseUrl, fetchWithHeaders]);
|
| 28 |
+
|
| 29 |
+
const saveConfigAutomatically = useCallback(async (robotType: 'leader' | 'follower', configName: string) => {
|
| 30 |
+
if (!configName.trim()) return;
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
await fetchWithHeaders(`${baseUrl}/save-robot-config`, {
|
| 34 |
+
method: 'POST',
|
| 35 |
+
headers: {
|
| 36 |
+
'Content-Type': 'application/json',
|
| 37 |
+
},
|
| 38 |
+
body: JSON.stringify({
|
| 39 |
+
robot_type: robotType,
|
| 40 |
+
config_name: configName.trim(),
|
| 41 |
+
}),
|
| 42 |
+
});
|
| 43 |
+
console.log(`Auto-saved ${robotType} config: ${configName}`);
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.error(`Error saving ${robotType} config:`, error);
|
| 46 |
+
}
|
| 47 |
+
}, [baseUrl, fetchWithHeaders]);
|
| 48 |
+
|
| 49 |
+
const debouncedSavePort = useCallback((robotType: 'leader' | 'follower', port: string, delay: number = 1500) => {
|
| 50 |
+
// Clear existing timeout for this robotType
|
| 51 |
+
if (timeoutRefs.current[robotType]) {
|
| 52 |
+
clearTimeout(timeoutRefs.current[robotType]);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Set new timeout
|
| 56 |
+
timeoutRefs.current[robotType] = setTimeout(() => {
|
| 57 |
+
savePortAutomatically(robotType, port);
|
| 58 |
+
delete timeoutRefs.current[robotType];
|
| 59 |
+
}, delay);
|
| 60 |
+
}, [savePortAutomatically]);
|
| 61 |
+
|
| 62 |
+
const debouncedSaveConfig = useCallback((robotType: 'leader' | 'follower', configName: string, delay: number = 1000) => {
|
| 63 |
+
const key = `${robotType}_config`;
|
| 64 |
+
|
| 65 |
+
// Clear existing timeout for this robotType config
|
| 66 |
+
if (configTimeoutRefs.current[key]) {
|
| 67 |
+
clearTimeout(configTimeoutRefs.current[key]);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Set new timeout
|
| 71 |
+
configTimeoutRefs.current[key] = setTimeout(() => {
|
| 72 |
+
saveConfigAutomatically(robotType, configName);
|
| 73 |
+
delete configTimeoutRefs.current[key];
|
| 74 |
+
}, delay);
|
| 75 |
+
}, [saveConfigAutomatically]);
|
| 76 |
+
|
| 77 |
+
return { debouncedSavePort, debouncedSaveConfig };
|
| 78 |
+
};
|