import React, { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Loader2, CheckCircle, AlertCircle } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { useApi } from "@/contexts/ApiContext"; interface PortDetectionModalProps { open: boolean; onOpenChange: (open: boolean) => void; robotType: "leader" | "follower"; onPortDetected: (port: string) => void; } const SUCCESS_HOLD_MS = 2000; const PortDetectionModal: React.FC = ({ open, onOpenChange, robotType, onPortDetected, }) => { const [step, setStep] = useState<"detecting" | "success" | "error">( "detecting" ); const [detectedPort, setDetectedPort] = useState(""); const [error, setError] = useState(""); const cancelledRef = useRef(false); const abortRef = useRef(null); const successTimerRef = useRef(null); const { toast } = useToast(); const { baseUrl, fetchWithHeaders } = useApi(); const runDetection = async () => { try { abortRef.current = new AbortController(); const startResponse = await fetchWithHeaders( `${baseUrl}/start-port-detection`, { method: "POST", body: JSON.stringify({ robot_type: robotType }), signal: abortRef.current.signal, } ); const startData = await startResponse.json(); if (cancelledRef.current) return; if (startData.status !== "success") { throw new Error(startData.message || "Failed to start port detection"); } const portsBefore: string[] = startData.data.ports_before; // Poll the backend in a loop. Each call waits up to 15s for an unplug; // we silently retry on timeout so the user has unlimited time to read // and act. The loop ends on success, abort, or a non-timeout failure. while (!cancelledRef.current) { abortRef.current = new AbortController(); const response = await fetchWithHeaders( `${baseUrl}/detect-port-after-disconnect`, { method: "POST", body: JSON.stringify({ ports_before: portsBefore }), signal: abortRef.current.signal, } ); const data = await response.json(); if (cancelledRef.current) return; if (data.status === "success") { setDetectedPort(data.port); await savePort(data.port); if (cancelledRef.current) return; setStep("success"); toast({ title: "Port Detected Successfully", description: `${robotType} port detected: ${data.port}`, }); successTimerRef.current = window.setTimeout(() => { if (cancelledRef.current) return; onPortDetected(data.port); onOpenChange(false); }, SUCCESS_HOLD_MS); return; } const message = typeof data.message === "string" ? data.message : ""; if (message.includes("Timed out")) continue; throw new Error(message || "Failed to detect port"); } } catch (e) { if (cancelledRef.current) return; if (e instanceof DOMException && e.name === "AbortError") return; console.error("Port detection failed:", e); setError(e instanceof Error ? e.message : "Unknown error"); setStep("error"); } }; const savePort = async (port: string) => { try { await fetchWithHeaders(`${baseUrl}/save-robot-port`, { method: "POST", body: JSON.stringify({ robot_type: robotType, port }), }); } catch (e) { console.error("Error saving port:", e); } }; useEffect(() => { if (!open) return; cancelledRef.current = false; setStep("detecting"); setError(""); setDetectedPort(""); runDetection(); return () => { cancelledRef.current = true; abortRef.current?.abort(); if (successTimerRef.current !== null) { window.clearTimeout(successTimerRef.current); successTimerRef.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const handleCancel = () => { onOpenChange(false); }; const handleRetry = () => { cancelledRef.current = false; abortRef.current?.abort(); setStep("detecting"); setError(""); setDetectedPort(""); runDetection(); }; const renderStepContent = () => { switch (step) { case "detecting": return (

Unplug the {robotType} arm

Disconnect the {robotType} robot arm from USB. The port will be detected automatically.

); case "success": return (

Port Detected

{detectedPort}

); case "error": return (

Detection Failed

{error}

); default: return null; } }; return ( Port Detection Detect the USB port for your {robotType} arm
{renderStepContent()}
); }; export default PortDetectionModal;