Spaces:
Running
Running
| <script lang="ts"> | |
| import * as Dialog from "@/components/ui/dialog"; | |
| import { Button } from "@/components/ui/button"; | |
| import * as Card from "@/components/ui/card"; | |
| import * as Alert from "@/components/ui/alert"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { toast } from "svelte-sonner"; | |
| import { robotManager } from "$lib/robot/RobotManager.svelte"; | |
| import { getApiBaseUrl, getWebSocketBaseUrl } from "$lib/utils/config"; | |
| import type { Robot } from "$lib/robot/Robot.svelte"; | |
| interface Props { | |
| open: boolean; | |
| robot: Robot | null; | |
| } | |
| let { open = $bindable(), robot }: Props = $props(); | |
| // Robot selection modal state for server slaves | |
| let showRobotSelectionModal = $state(false); | |
| let availableServerRobots = $state<{ id: string; name: string; robot_type: string }[]>([]); | |
| let selectedServerRobotId = $state<string>(""); | |
| // Get URLs from configuration | |
| const apiBaseUrl = getApiBaseUrl(); | |
| const wsBaseUrl = getWebSocketBaseUrl(); | |
| // Slave connection functions | |
| async function connectMockSlave() { | |
| if (!robot) return; | |
| try { | |
| await robotManager.connectMockSlave(robot.id, 50); | |
| } catch (err) { | |
| toast.error("Failed to Connect Mock Slave", { | |
| description: `Could not connect mock slave: ${err}` | |
| }); | |
| console.error(err); | |
| } | |
| } | |
| async function connectUSBSlave() { | |
| if (!robot) return; | |
| try { | |
| await robotManager.connectUSBSlave(robot.id); | |
| } catch (err) { | |
| toast.error("Failed to Connect USB Slave", { | |
| description: `Could not connect USB slave: ${err}` | |
| }); | |
| console.error(err); | |
| } | |
| } | |
| async function connectRemoteServerSlave() { | |
| if (!robot) return; | |
| try { | |
| // First, fetch available robots from the server | |
| const response = await fetch(`${apiBaseUrl}/api/robots`); | |
| if (!response.ok) { | |
| throw new Error(`Server responded with ${response.status}: ${response.statusText}`); | |
| } | |
| const robots = await response.json(); | |
| if (robots.length === 0) { | |
| toast.error("No Server Robots Available", { | |
| description: "No robots available on the server. Create a robot on the server first." | |
| }); | |
| return; | |
| } | |
| // Show modal for robot selection | |
| availableServerRobots = robots; | |
| selectedServerRobotId = robots[0]?.id || ""; | |
| showRobotSelectionModal = true; | |
| } catch (err) { | |
| toast.error("Failed to Fetch Server Robots", { | |
| description: `Could not fetch server robots: ${err}` | |
| }); | |
| console.error(err); | |
| } | |
| } | |
| async function confirmRobotSelection() { | |
| if (!robot || !selectedServerRobotId) return; | |
| try { | |
| await robotManager.connectRemoteServerSlave( | |
| robot.id, | |
| wsBaseUrl, | |
| undefined, | |
| selectedServerRobotId | |
| ); | |
| // Close modal | |
| showRobotSelectionModal = false; | |
| } catch (err) { | |
| toast.error("Failed to Connect Remote Server Slave", { | |
| description: `Could not connect remote server slave: ${err}` | |
| }); | |
| console.error(err); | |
| } | |
| } | |
| function cancelRobotSelection() { | |
| showRobotSelectionModal = false; | |
| selectedServerRobotId = ""; | |
| } | |
| async function disconnectSlave(slaveId: string) { | |
| if (!robot) return; | |
| try { | |
| await robotManager.disconnectSlave(robot.id, slaveId); | |
| } catch (err) { | |
| toast.error("Failed to Disconnect Slave", { | |
| description: `Could not disconnect slave: ${err}` | |
| }); | |
| console.error(err); | |
| } | |
| } | |
| </script> | |
| <Dialog.Root bind:open> | |
| <Dialog.Content | |
| class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100" | |
| > | |
| <Dialog.Header class="pb-3"> | |
| <Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100"> | |
| <span class="icon-[mdi--devices] size-5 text-blue-400"></span> | |
| Slave Connection | |
| </Dialog.Title> | |
| <Dialog.Description class="text-sm text-slate-400"> | |
| Configure slave targets for robot {robot?.id} | |
| </Dialog.Description> | |
| </Dialog.Header> | |
| {#if robot} | |
| <div class="space-y-4"> | |
| <!-- Current Status - Compact --> | |
| <div | |
| class="flex items-center justify-between rounded-lg border border-blue-500/30 bg-blue-900/20 p-3" | |
| > | |
| <div class="flex items-center gap-2"> | |
| <span class="icon-[fa6-solid--ear-listen] size-4 text-blue-400"></span> | |
| <span class="text-sm font-medium text-blue-300">Slave Status</span> | |
| </div> | |
| <Badge variant="default" class="bg-blue-600 text-xs"> | |
| {robot.connectedSlaves.length} / {robot.slaves.length} Connected | |
| </Badge> | |
| </div> | |
| <!-- Slave Controls --> | |
| <Card.Root class="border-blue-500/30 bg-blue-500/5"> | |
| <Card.Header class="pb-2"> | |
| <Card.Title class="flex items-center gap-2 text-base text-blue-200"> | |
| <span class="icon-[mdi--devices] size-4"></span> | |
| Connection Options | |
| </Card.Title> | |
| </Card.Header> | |
| <Card.Content class="space-y-3"> | |
| <div class="space-y-2"> | |
| <Button | |
| variant="secondary" | |
| onclick={connectMockSlave} | |
| class="h-8 w-full bg-yellow-600 text-sm text-white hover:bg-yellow-700" | |
| > | |
| <span class="icon-[mdi--robot-confused] mr-2 size-4"></span> | |
| Add Mock Slave | |
| </Button> | |
| <Button | |
| variant="secondary" | |
| onclick={connectUSBSlave} | |
| class="h-8 w-full bg-green-600 text-sm text-white hover:bg-green-700" | |
| > | |
| <span class="icon-[mdi--usb] mr-2 size-4"></span> | |
| Add USB Slave | |
| </Button> | |
| <Button | |
| variant="secondary" | |
| onclick={connectRemoteServerSlave} | |
| class="h-8 w-full bg-purple-600 text-sm text-white hover:bg-purple-700" | |
| > | |
| <span class="icon-[mdi--cloud] mr-2 size-4"></span> | |
| Add Remote Server Slave | |
| </Button> | |
| </div> | |
| {#if robot.slaves.length > 0} | |
| <Separator /> | |
| <div class="space-y-2"> | |
| <p class="text-xs font-medium text-blue-300">Connected Slaves:</p> | |
| <div class="max-h-32 space-y-1 overflow-y-auto"> | |
| {#each robot.slaves as slave} | |
| <div class="flex items-center justify-between rounded-md bg-slate-700/50 p-2"> | |
| <div class="flex items-center gap-2"> | |
| <span class="icon-[mdi--circle] size-2 text-green-400"></span> | |
| <span class="text-sm text-slate-300">{slave.name}</span> | |
| <Badge variant="outline" class="text-xs">{slave.id.slice(0, 8)}</Badge> | |
| </div> | |
| <Button | |
| variant="destructive" | |
| size="sm" | |
| onclick={() => disconnectSlave(slave.id)} | |
| class="h-6 px-2 text-xs" | |
| > | |
| <span class="icon-[mdi--close] size-3"></span> | |
| </Button> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| </Card.Content> | |
| </Card.Root> | |
| <!-- Quick Info --> | |
| <div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500"> | |
| <span class="icon-[mdi--information] mr-1 size-3"></span> | |
| Slaves are output targets. Multiple can be connected simultaneously. | |
| </div> | |
| </div> | |
| {/if} | |
| </Dialog.Content> | |
| </Dialog.Root> | |
| <!-- Robot Selection Modal for Remote Server Slaves --> | |
| {#if showRobotSelectionModal} | |
| <Dialog.Root open={true}> | |
| <Dialog.Content class="max-w-md border-slate-600 bg-slate-900 text-slate-100"> | |
| <Dialog.Header> | |
| <Dialog.Title class="flex items-center gap-2 text-slate-100"> | |
| <span class="icon-[ix--robotic-arm] size-5"></span> | |
| Select Server Robot | |
| </Dialog.Title> | |
| <Dialog.Description class="text-slate-400"> | |
| Choose which server robot to connect as a slave target | |
| </Dialog.Description> | |
| </Dialog.Header> | |
| <div class="space-y-4"> | |
| <div class="space-y-2"> | |
| <label for="server-robot-select" class="text-sm font-medium text-slate-300" | |
| >Available robots on server:</label | |
| > | |
| <select | |
| bind:value={selectedServerRobotId} | |
| class="w-full rounded-md border border-slate-600 bg-slate-700 px-3 py-2 text-sm text-slate-100" | |
| id="server-robot-select" | |
| > | |
| {#each availableServerRobots as serverRobot} | |
| <option value={serverRobot.id}> | |
| {serverRobot.name} ({serverRobot.id}) - {serverRobot.robot_type} | |
| </option> | |
| {/each} | |
| </select> | |
| </div> | |
| <Alert.Root> | |
| <span class="icon-[mdi--information] size-4"></span> | |
| <Alert.Description> | |
| This will connect your local robot <strong>"{robot?.id}"</strong> as a slave to receive commands | |
| from the selected server robot. | |
| </Alert.Description> | |
| </Alert.Root> | |
| </div> | |
| <Dialog.Footer class="flex justify-end gap-3"> | |
| <Button variant="outline" onclick={cancelRobotSelection}>Cancel</Button> | |
| <Button | |
| onclick={confirmRobotSelection} | |
| disabled={!selectedServerRobotId} | |
| class="bg-purple-600 hover:bg-purple-700" | |
| > | |
| <span class="icon-[mdi--link] mr-1 size-4"></span> | |
| Connect Slave | |
| </Button> | |
| </Dialog.Footer> | |
| </Dialog.Content> | |
| </Dialog.Root> | |
| {/if} | |