LeRobot-Arena / src /lib /components /interface /overlay /SlaveConnectionModal.svelte
blanchon's picture
Mostly UI Update
18b0fa5
<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}