| 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<PortDetectionModalProps> = ({ |
| open, |
| onOpenChange, |
| robotType, |
| onPortDetected, |
| }) => { |
| const [step, setStep] = useState<"detecting" | "success" | "error">( |
| "detecting" |
| ); |
| const [detectedPort, setDetectedPort] = useState<string>(""); |
| const [error, setError] = useState<string>(""); |
| const cancelledRef = useRef(false); |
| const abortRef = useRef<AbortController | null>(null); |
| const successTimerRef = useRef<number | null>(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; |
|
|
| |
| |
| |
| 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; |
| } |
| }; |
| |
| }, [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 ( |
| <div className="space-y-6 text-center"> |
| <Loader2 className="w-16 h-16 text-blue-500 mx-auto animate-spin" /> |
| <div className="space-y-2"> |
| <h3 className="text-lg font-semibold text-white"> |
| Unplug the {robotType} arm |
| </h3> |
| <p className="text-gray-400"> |
| Disconnect the {robotType} robot arm from USB. The port will be |
| detected automatically. |
| </p> |
| </div> |
| <div className="flex justify-center"> |
| <Button |
| onClick={handleCancel} |
| variant="outline" |
| className="border-gray-500 hover:border-gray-200 text-gray-300 hover:text-white px-8 py-2" |
| > |
| Cancel |
| </Button> |
| </div> |
| </div> |
| ); |
|
|
| case "success": |
| return ( |
| <div className="space-y-6 text-center"> |
| <CheckCircle className="w-16 h-16 text-green-500 mx-auto" /> |
| <div className="space-y-2"> |
| <h3 className="text-lg font-semibold text-white"> |
| Port Detected |
| </h3> |
| <p className="text-xl font-mono text-green-400 bg-gray-800 px-4 py-2 rounded inline-block"> |
| {detectedPort} |
| </p> |
| </div> |
| </div> |
| ); |
|
|
| case "error": |
| return ( |
| <div className="space-y-6 text-center"> |
| <AlertCircle className="w-16 h-16 text-red-500 mx-auto" /> |
| <div className="space-y-2"> |
| <h3 className="text-lg font-semibold text-white"> |
| Detection Failed |
| </h3> |
| <div className="bg-red-900/20 border border-red-800 rounded-lg p-3"> |
| <p className="text-red-400 text-sm">{error}</p> |
| </div> |
| </div> |
| <div className="flex gap-4 justify-center"> |
| <Button |
| onClick={handleRetry} |
| className="bg-blue-500 hover:bg-blue-600 text-white px-8 py-2" |
| > |
| Try Again |
| </Button> |
| <Button |
| onClick={handleCancel} |
| variant="outline" |
| className="border-gray-500 hover:border-gray-200 text-gray-300 hover:text-white px-8 py-2" |
| > |
| Cancel |
| </Button> |
| </div> |
| </div> |
| ); |
|
|
| default: |
| return null; |
| } |
| }; |
|
|
| return ( |
| <Dialog open={open} onOpenChange={onOpenChange}> |
| <DialogContent className="bg-gray-900 border-gray-800 text-white sm:max-w-[500px] p-8"> |
| <DialogHeader> |
| <DialogTitle className="text-white text-center text-xl font-bold"> |
| Port Detection |
| </DialogTitle> |
| <DialogDescription className="text-gray-400 text-center"> |
| Detect the USB port for your {robotType} arm |
| </DialogDescription> |
| </DialogHeader> |
| |
| <div className="py-4">{renderStepContent()}</div> |
| </DialogContent> |
| </Dialog> |
| ); |
| }; |
|
|
| export default PortDetectionModal; |
|
|