Spaces:
Running
Running
Update
Browse files- Dockerfile +2 -2
- bun.lock +1 -1
- external/RobotHub-InferenceServer +1 -1
- package.json +1 -1
- src/lib/components/3d/Floor.svelte +7 -1
- src/lib/components/3d/elements/compute/Computes.svelte +1 -1
- src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte +54 -54
- src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte +44 -44
- src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte +43 -43
- src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte +6 -6
- src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte +1 -1
- src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte +1 -1
- src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte +1 -1
- src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte +1 -1
- src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte +1 -1
- src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte +54 -54
- src/lib/components/3d/elements/robot/modal/ManualControlSheet.svelte +21 -21
- src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte +49 -49
- src/lib/components/3d/elements/video/modal/VideoInputConnectionModal.svelte +55 -55
- src/lib/components/3d/elements/video/modal/VideoOutputConnectionModal.svelte +128 -117
- src/lib/components/interface/overlay/AddAIButton.svelte +10 -9
- src/lib/components/interface/overlay/AddRobotButton.svelte +11 -10
- src/lib/components/interface/overlay/AddSensorButton.svelte +11 -10
- src/lib/components/interface/overlay/Overlay.svelte +17 -7
- src/lib/components/interface/overlay/SettingsButton.svelte +1 -1
- src/lib/components/interface/overlay/SettingsSheet.svelte +75 -70
- src/lib/elements/compute/README.md +3 -3
- src/lib/elements/compute/RemoteComputeManager.svelte.ts +1 -1
- src/lib/elements/robot/Robot.svelte.ts +2 -2
- src/lib/elements/robot/drivers/RemoteConsumer.ts +1 -1
- src/lib/elements/robot/drivers/RemoteProducer.ts +1 -1
- src/lib/runes/settings.svelte.ts +1 -1
- src/routes/+layout.svelte +13 -12
- src/routes/+page.svelte +46 -44
- static-server.js +1 -1
Dockerfile
CHANGED
@@ -58,10 +58,10 @@ COPY --chown=user:user static-server.js ./
|
|
58 |
# Switch to non-root user
|
59 |
USER user
|
60 |
|
61 |
-
ARG PUBLIC_TRANSPORT_SERVER_URL=https://blanchon-robothub-
|
62 |
ENV PUBLIC_TRANSPORT_SERVER_URL=${PUBLIC_TRANSPORT_SERVER_URL}
|
63 |
|
64 |
-
ARG PUBLIC_INFERENCE_SERVER_URL=https://blanchon-robothub-
|
65 |
ENV PUBLIC_INFERENCE_SERVER_URL=${PUBLIC_INFERENCE_SERVER_URL}
|
66 |
|
67 |
ARG PORT=8000
|
|
|
58 |
# Switch to non-root user
|
59 |
USER user
|
60 |
|
61 |
+
ARG PUBLIC_TRANSPORT_SERVER_URL=https://blanchon-robothub-transport-server.hf.space/api
|
62 |
ENV PUBLIC_TRANSPORT_SERVER_URL=${PUBLIC_TRANSPORT_SERVER_URL}
|
63 |
|
64 |
+
ARG PUBLIC_INFERENCE_SERVER_URL=https://blanchon-robothub-inferenceserver.hf.space/api
|
65 |
ENV PUBLIC_INFERENCE_SERVER_URL=${PUBLIC_INFERENCE_SERVER_URL}
|
66 |
|
67 |
ARG PORT=8000
|
bun.lock
CHANGED
@@ -35,7 +35,7 @@
|
|
35 |
"eslint-plugin-svelte": "^3.9.1",
|
36 |
"globals": "^16.2.0",
|
37 |
"layerchart": "1.0.11",
|
38 |
-
"mode-watcher": "^1.0.
|
39 |
"prettier": "^3.5.3",
|
40 |
"prettier-plugin-svelte": "^3.4.0",
|
41 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
|
|
35 |
"eslint-plugin-svelte": "^3.9.1",
|
36 |
"globals": "^16.2.0",
|
37 |
"layerchart": "1.0.11",
|
38 |
+
"mode-watcher": "^1.0.8",
|
39 |
"prettier": "^3.5.3",
|
40 |
"prettier-plugin-svelte": "^3.4.0",
|
41 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
external/RobotHub-InferenceServer
CHANGED
@@ -1 +1 @@
|
|
1 |
-
Subproject commit
|
|
|
1 |
+
Subproject commit e3c6edfc01cbf01b6ca1ac19637a282017ef6c07
|
package.json
CHANGED
@@ -32,7 +32,7 @@
|
|
32 |
"eslint-plugin-svelte": "^3.9.1",
|
33 |
"globals": "^16.2.0",
|
34 |
"layerchart": "1.0.11",
|
35 |
-
"mode-watcher": "^1.0.
|
36 |
"prettier": "^3.5.3",
|
37 |
"prettier-plugin-svelte": "^3.4.0",
|
38 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
|
|
32 |
"eslint-plugin-svelte": "^3.9.1",
|
33 |
"globals": "^16.2.0",
|
34 |
"layerchart": "1.0.11",
|
35 |
+
"mode-watcher": "^1.0.8",
|
36 |
"prettier": "^3.5.3",
|
37 |
"prettier-plugin-svelte": "^3.4.0",
|
38 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
src/lib/components/3d/Floor.svelte
CHANGED
@@ -2,6 +2,8 @@
|
|
2 |
import { T } from "@threlte/core";
|
3 |
import { PlaneGeometry } from 'three';
|
4 |
import { Grid } from '@threlte/extras'
|
|
|
|
|
5 |
const floorGeometry = new PlaneGeometry(20, 20);
|
6 |
</script>
|
7 |
|
@@ -20,5 +22,9 @@
|
|
20 |
polygonOffsetUnits={1}
|
21 |
/>
|
22 |
</T.Mesh>
|
23 |
-
<Grid
|
|
|
|
|
|
|
|
|
24 |
|
|
|
2 |
import { T } from "@threlte/core";
|
3 |
import { PlaneGeometry } from 'three';
|
4 |
import { Grid } from '@threlte/extras'
|
5 |
+
import { mode } from "mode-watcher";
|
6 |
+
|
7 |
const floorGeometry = new PlaneGeometry(20, 20);
|
8 |
</script>
|
9 |
|
|
|
22 |
polygonOffsetUnits={1}
|
23 |
/>
|
24 |
</T.Mesh>
|
25 |
+
<Grid
|
26 |
+
backgroundColor={mode.current === 'dark' ? "#dadada" : "#e2e8f0"}
|
27 |
+
cellColor={mode.current === 'dark' ? "#000000" : "#94a3b8"}
|
28 |
+
selectionColor={mode.current === 'dark' ? "#0000ee" : "#3b82f6"}
|
29 |
+
/>
|
30 |
|
src/lib/components/3d/elements/compute/Computes.svelte
CHANGED
@@ -76,7 +76,7 @@
|
|
76 |
{/each}
|
77 |
|
78 |
{#if selectedCompute}
|
79 |
-
<!--
|
80 |
<AISessionConnectionModal bind:open={isAISessionModalOpen} compute={selectedCompute} {workspaceId} />
|
81 |
<!-- Video Input Connection Modal -->
|
82 |
<VideoInputConnectionModal bind:open={isVideoInputModalOpen} compute={selectedCompute} {workspaceId} />
|
|
|
76 |
{/each}
|
77 |
|
78 |
{#if selectedCompute}
|
79 |
+
<!-- Inference Session Creation Modal -->
|
80 |
<AISessionConnectionModal bind:open={isAISessionModalOpen} compute={selectedCompute} {workspaceId} />
|
81 |
<!-- Video Input Connection Modal -->
|
82 |
<VideoInputConnectionModal bind:open={isVideoInputModalOpen} compute={selectedCompute} {workspaceId} />
|
src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte
CHANGED
@@ -58,7 +58,7 @@
|
|
58 |
|
59 |
const result = await remoteComputeManager.createSession(compute.id, config);
|
60 |
if (result.success) {
|
61 |
-
toast.success(`
|
62 |
open = false;
|
63 |
} else {
|
64 |
toast.error(`Failed to create session: ${result.error}`);
|
@@ -78,7 +78,7 @@
|
|
78 |
try {
|
79 |
const result = await remoteComputeManager.startSession(compute.id);
|
80 |
if (result.success) {
|
81 |
-
toast.success('
|
82 |
} else {
|
83 |
toast.error(`Failed to start session: ${result.error}`);
|
84 |
}
|
@@ -97,7 +97,7 @@
|
|
97 |
try {
|
98 |
const result = await remoteComputeManager.stopSession(compute.id);
|
99 |
if (result.success) {
|
100 |
-
toast.success('
|
101 |
} else {
|
102 |
toast.error(`Failed to stop session: ${result.error}`);
|
103 |
}
|
@@ -116,7 +116,7 @@
|
|
116 |
try {
|
117 |
const result = await remoteComputeManager.deleteSession(compute.id);
|
118 |
if (result.success) {
|
119 |
-
toast.success('
|
120 |
} else {
|
121 |
toast.error(`Failed to delete session: ${result.error}`);
|
122 |
}
|
@@ -131,14 +131,14 @@
|
|
131 |
|
132 |
<Dialog.Root bind:open>
|
133 |
<Dialog.Content
|
134 |
-
class="max-h-[80vh] max-w-2xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
135 |
>
|
136 |
<Dialog.Header class="pb-3">
|
137 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
138 |
-
<span class="icon-[mdi--robot-outline] size-5 text-purple-400"></span>
|
139 |
AI Compute Session - {compute.name || 'No Compute Selected'}
|
140 |
</Dialog.Title>
|
141 |
-
<Dialog.Description class="text-sm text-slate-400">
|
142 |
Configure and manage ACT model inference sessions for robot control
|
143 |
</Dialog.Description>
|
144 |
</Dialog.Header>
|
@@ -146,74 +146,74 @@
|
|
146 |
<div class="space-y-4">
|
147 |
<!-- Current Session Status -->
|
148 |
<div
|
149 |
-
class="flex items-center justify-between rounded-lg border border-purple-
|
150 |
>
|
151 |
<div class="flex items-center gap-2">
|
152 |
-
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
153 |
-
<span class="text-sm font-medium text-purple-300">Session Status</span>
|
154 |
</div>
|
155 |
{#if compute.hasSession}
|
156 |
-
<Badge variant="default" class="bg-purple-
|
157 |
{compute.statusInfo.statusText}
|
158 |
</Badge>
|
159 |
{:else}
|
160 |
-
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
161 |
{/if}
|
162 |
</div>
|
163 |
|
164 |
<!-- Current Session Details -->
|
165 |
{#if compute.hasSession && compute.sessionData}
|
166 |
-
<Card.Root class="border-purple-500/30 bg-purple-500/5">
|
167 |
<Card.Header>
|
168 |
-
<Card.Title class="flex items-center gap-2 text-base text-purple-200">
|
169 |
<span class="icon-[mdi--cog] size-4"></span>
|
170 |
Current Session
|
171 |
</Card.Title>
|
172 |
</Card.Header>
|
173 |
<Card.Content>
|
174 |
<div class="space-y-3">
|
175 |
-
<div class="rounded-lg border border-purple-
|
176 |
<div class="grid grid-cols-2 gap-2 text-xs">
|
177 |
<div>
|
178 |
-
<span class="text-purple-
|
179 |
-
<span class="text-purple-
|
180 |
</div>
|
181 |
<div>
|
182 |
-
<span class="text-purple-
|
183 |
-
<span class="text-purple-
|
184 |
</div>
|
185 |
<div>
|
186 |
-
<span class="text-purple-
|
187 |
-
<span class="text-purple-
|
188 |
</div>
|
189 |
<div>
|
190 |
-
<span class="text-purple-
|
191 |
-
<span class="text-purple-
|
192 |
</div>
|
193 |
</div>
|
194 |
</div>
|
195 |
|
196 |
<!-- Connection Details -->
|
197 |
-
<div class="rounded-lg border border-green-
|
198 |
-
<div class="text-sm font-medium text-green-
|
199 |
<div class="space-y-1 text-xs">
|
200 |
<div>
|
201 |
-
<span class="text-green-400">Workspace:</span>
|
202 |
-
<span class="text-green-
|
203 |
</div>
|
204 |
{#each Object.entries(compute.sessionData.camera_room_ids) as [camera, roomId]}
|
205 |
<div>
|
206 |
-
<span class="text-green-400">📹 {camera}:</span>
|
207 |
-
<span class="text-green-
|
208 |
</div>
|
209 |
{/each}
|
210 |
<div>
|
211 |
-
<span class="text-green-400">📥 Joint Input:</span>
|
212 |
-
<span class="text-green-
|
213 |
</div>
|
214 |
<div>
|
215 |
-
<span class="text-green-400">📤 Joint Output:</span>
|
216 |
-
<span class="text-green-
|
217 |
</div>
|
218 |
</div>
|
219 |
</div>
|
@@ -226,7 +226,7 @@
|
|
226 |
size="sm"
|
227 |
onclick={handleStartSession}
|
228 |
disabled={isConnecting}
|
229 |
-
class="bg-green-
|
230 |
>
|
231 |
{#if isConnecting}
|
232 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
@@ -277,58 +277,58 @@
|
|
277 |
|
278 |
<!-- Create New Session -->
|
279 |
{#if !compute.hasSession}
|
280 |
-
<Card.Root class="border-purple-500/30 bg-purple-500/5">
|
281 |
<Card.Header>
|
282 |
-
<Card.Title class="flex items-center gap-2 text-base text-purple-200">
|
283 |
<span class="icon-[mdi--plus-circle] size-4"></span>
|
284 |
-
Create
|
285 |
</Card.Title>
|
286 |
</Card.Header>
|
287 |
<Card.Content>
|
288 |
<div class="space-y-4">
|
289 |
<div class="grid grid-cols-2 gap-4">
|
290 |
<div class="space-y-2">
|
291 |
-
<Label for="sessionId" class="text-purple-300">Session ID</Label>
|
292 |
<Input
|
293 |
id="sessionId"
|
294 |
bind:value={sessionId}
|
295 |
placeholder="my-session-01"
|
296 |
-
class="bg-slate-800 border-slate-600 text-slate-100"
|
297 |
/>
|
298 |
</div>
|
299 |
<div class="space-y-2">
|
300 |
-
<Label for="policyPath" class="text-purple-300">Policy Path</Label>
|
301 |
<Input
|
302 |
id="policyPath"
|
303 |
bind:value={policyPath}
|
304 |
placeholder="./checkpoints/act_so101_beyond"
|
305 |
-
class="bg-slate-800 border-slate-600 text-slate-100"
|
306 |
/>
|
307 |
</div>
|
308 |
</div>
|
309 |
|
310 |
<div class="grid grid-cols-2 gap-4">
|
311 |
<div class="space-y-2">
|
312 |
-
<Label for="cameraNames" class="text-purple-300">Camera Names</Label>
|
313 |
<Input
|
314 |
id="cameraNames"
|
315 |
bind:value={cameraNames}
|
316 |
placeholder="front, wrist, overhead"
|
317 |
-
class="bg-slate-800 border-slate-600 text-slate-100"
|
318 |
/>
|
319 |
-
<p class="text-xs text-slate-400">Comma-separated camera names</p>
|
320 |
</div>
|
321 |
<div class="space-y-2">
|
322 |
-
<Label for="transportServerUrl" class="text-purple-300">Transport Server URL</Label>
|
323 |
<Input
|
324 |
id="transportServerUrl"
|
325 |
value={settings.transportServerUrl}
|
326 |
disabled
|
327 |
placeholder="http://localhost:8000"
|
328 |
-
class="bg-slate-
|
329 |
title="Change this value in the settings panel"
|
330 |
/>
|
331 |
-
<p class="text-xs text-slate-400">Configure in settings panel</p>
|
332 |
</div>
|
333 |
</div>
|
334 |
|
@@ -337,9 +337,9 @@
|
|
337 |
type="checkbox"
|
338 |
id="useWorkspace"
|
339 |
bind:checked={useProvidedWorkspace}
|
340 |
-
class="rounded border-slate-600 bg-slate-800"
|
341 |
/>
|
342 |
-
<Label for="useWorkspace" class="text-purple-
|
343 |
Use current workspace ({workspaceId})
|
344 |
</Label>
|
345 |
</div>
|
@@ -356,14 +356,14 @@
|
|
356 |
variant="default"
|
357 |
onclick={handleCreateSession}
|
358 |
disabled={isConnecting || !sessionId.trim() || !policyPath.trim()}
|
359 |
-
class="w-full bg-purple-
|
360 |
>
|
361 |
{#if isConnecting}
|
362 |
<span class="icon-[mdi--loading] animate-spin mr-2 size-4"></span>
|
363 |
Creating Session...
|
364 |
{:else}
|
365 |
<span class="icon-[mdi--rocket-launch] mr-2 size-4"></span>
|
366 |
-
Create
|
367 |
{/if}
|
368 |
</Button>
|
369 |
</div>
|
@@ -372,9 +372,9 @@
|
|
372 |
{/if}
|
373 |
|
374 |
<!-- Quick Info -->
|
375 |
-
<div class="rounded border border-slate-
|
376 |
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
377 |
-
|
378 |
robot joint states, and control outputs in the inference server system.
|
379 |
</div>
|
380 |
</div>
|
|
|
58 |
|
59 |
const result = await remoteComputeManager.createSession(compute.id, config);
|
60 |
if (result.success) {
|
61 |
+
toast.success(`Inference Session created: ${sessionId}`);
|
62 |
open = false;
|
63 |
} else {
|
64 |
toast.error(`Failed to create session: ${result.error}`);
|
|
|
78 |
try {
|
79 |
const result = await remoteComputeManager.startSession(compute.id);
|
80 |
if (result.success) {
|
81 |
+
toast.success('Inference Session started');
|
82 |
} else {
|
83 |
toast.error(`Failed to start session: ${result.error}`);
|
84 |
}
|
|
|
97 |
try {
|
98 |
const result = await remoteComputeManager.stopSession(compute.id);
|
99 |
if (result.success) {
|
100 |
+
toast.success('Inference Session stopped');
|
101 |
} else {
|
102 |
toast.error(`Failed to stop session: ${result.error}`);
|
103 |
}
|
|
|
116 |
try {
|
117 |
const result = await remoteComputeManager.deleteSession(compute.id);
|
118 |
if (result.success) {
|
119 |
+
toast.success('Inference Session deleted');
|
120 |
} else {
|
121 |
toast.error(`Failed to delete session: ${result.error}`);
|
122 |
}
|
|
|
131 |
|
132 |
<Dialog.Root bind:open>
|
133 |
<Dialog.Content
|
134 |
+
class="max-h-[80vh] max-w-2xl overflow-y-auto border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
135 |
>
|
136 |
<Dialog.Header class="pb-3">
|
137 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
138 |
+
<span class="icon-[mdi--robot-outline] size-5 text-purple-500 dark:text-purple-400"></span>
|
139 |
AI Compute Session - {compute.name || 'No Compute Selected'}
|
140 |
</Dialog.Title>
|
141 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
142 |
Configure and manage ACT model inference sessions for robot control
|
143 |
</Dialog.Description>
|
144 |
</Dialog.Header>
|
|
|
146 |
<div class="space-y-4">
|
147 |
<!-- Current Session Status -->
|
148 |
<div
|
149 |
+
class="flex items-center justify-between rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20"
|
150 |
>
|
151 |
<div class="flex items-center gap-2">
|
152 |
+
<span class="icon-[mdi--brain] size-4 text-purple-500 dark:text-purple-400"></span>
|
153 |
+
<span class="text-sm font-medium text-purple-700 dark:text-purple-300">Session Status</span>
|
154 |
</div>
|
155 |
{#if compute.hasSession}
|
156 |
+
<Badge variant="default" class="bg-purple-500 text-xs dark:bg-purple-600">
|
157 |
{compute.statusInfo.statusText}
|
158 |
</Badge>
|
159 |
{:else}
|
160 |
+
<Badge variant="secondary" class="text-xs text-slate-600 dark:text-slate-400">No Session</Badge>
|
161 |
{/if}
|
162 |
</div>
|
163 |
|
164 |
<!-- Current Session Details -->
|
165 |
{#if compute.hasSession && compute.sessionData}
|
166 |
+
<Card.Root class="border-purple-300/30 bg-purple-100/5 dark:border-purple-500/30 dark:bg-purple-500/5">
|
167 |
<Card.Header>
|
168 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-700 dark:text-purple-200">
|
169 |
<span class="icon-[mdi--cog] size-4"></span>
|
170 |
Current Session
|
171 |
</Card.Title>
|
172 |
</Card.Header>
|
173 |
<Card.Content>
|
174 |
<div class="space-y-3">
|
175 |
+
<div class="rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20">
|
176 |
<div class="grid grid-cols-2 gap-2 text-xs">
|
177 |
<div>
|
178 |
+
<span class="text-purple-700 font-medium dark:text-purple-300">Session ID:</span>
|
179 |
+
<span class="text-purple-800 block dark:text-purple-100">{compute.sessionId}</span>
|
180 |
</div>
|
181 |
<div>
|
182 |
+
<span class="text-purple-700 font-medium dark:text-purple-300">Status:</span>
|
183 |
+
<span class="text-purple-800 block dark:text-purple-100">{compute.statusInfo.emoji} {compute.statusInfo.statusText}</span>
|
184 |
</div>
|
185 |
<div>
|
186 |
+
<span class="text-purple-700 font-medium dark:text-purple-300">Policy:</span>
|
187 |
+
<span class="text-purple-800 block dark:text-purple-100">{compute.sessionConfig?.policyPath}</span>
|
188 |
</div>
|
189 |
<div>
|
190 |
+
<span class="text-purple-700 font-medium dark:text-purple-300">Cameras:</span>
|
191 |
+
<span class="text-purple-800 block dark:text-purple-100">{compute.sessionConfig?.cameraNames.join(', ')}</span>
|
192 |
</div>
|
193 |
</div>
|
194 |
</div>
|
195 |
|
196 |
<!-- Connection Details -->
|
197 |
+
<div class="rounded-lg border border-green-300/30 bg-green-100/20 p-3 dark:border-green-500/30 dark:bg-green-900/20">
|
198 |
+
<div class="text-sm font-medium text-green-700 mb-2 dark:text-green-300">📡 Inference Server Connections</div>
|
199 |
<div class="space-y-1 text-xs">
|
200 |
<div>
|
201 |
+
<span class="text-green-600 dark:text-green-400">Workspace:</span>
|
202 |
+
<span class="text-green-700 font-mono ml-2 dark:text-green-200">{compute.sessionData.workspace_id}</span>
|
203 |
</div>
|
204 |
{#each Object.entries(compute.sessionData.camera_room_ids) as [camera, roomId]}
|
205 |
<div>
|
206 |
+
<span class="text-green-600 dark:text-green-400">📹 {camera}:</span>
|
207 |
+
<span class="text-green-700 font-mono ml-2 dark:text-green-200">{roomId}</span>
|
208 |
</div>
|
209 |
{/each}
|
210 |
<div>
|
211 |
+
<span class="text-green-600 dark:text-green-400">📥 Joint Input:</span>
|
212 |
+
<span class="text-green-700 font-mono ml-2 dark:text-green-200">{compute.sessionData.joint_input_room_id}</span>
|
213 |
</div>
|
214 |
<div>
|
215 |
+
<span class="text-green-600 dark:text-green-400">📤 Joint Output:</span>
|
216 |
+
<span class="text-green-700 font-mono ml-2 dark:text-green-200">{compute.sessionData.joint_output_room_id}</span>
|
217 |
</div>
|
218 |
</div>
|
219 |
</div>
|
|
|
226 |
size="sm"
|
227 |
onclick={handleStartSession}
|
228 |
disabled={isConnecting}
|
229 |
+
class="bg-green-500 hover:bg-green-600 text-xs disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
230 |
>
|
231 |
{#if isConnecting}
|
232 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
|
|
277 |
|
278 |
<!-- Create New Session -->
|
279 |
{#if !compute.hasSession}
|
280 |
+
<Card.Root class="border-purple-300/30 bg-purple-100/5 dark:border-purple-500/30 dark:bg-purple-500/5">
|
281 |
<Card.Header>
|
282 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-700 dark:text-purple-200">
|
283 |
<span class="icon-[mdi--plus-circle] size-4"></span>
|
284 |
+
Create Inference Session
|
285 |
</Card.Title>
|
286 |
</Card.Header>
|
287 |
<Card.Content>
|
288 |
<div class="space-y-4">
|
289 |
<div class="grid grid-cols-2 gap-4">
|
290 |
<div class="space-y-2">
|
291 |
+
<Label for="sessionId" class="text-purple-700 dark:text-purple-300">Session ID</Label>
|
292 |
<Input
|
293 |
id="sessionId"
|
294 |
bind:value={sessionId}
|
295 |
placeholder="my-session-01"
|
296 |
+
class="bg-slate-50 border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100"
|
297 |
/>
|
298 |
</div>
|
299 |
<div class="space-y-2">
|
300 |
+
<Label for="policyPath" class="text-purple-700 dark:text-purple-300">Policy Path</Label>
|
301 |
<Input
|
302 |
id="policyPath"
|
303 |
bind:value={policyPath}
|
304 |
placeholder="./checkpoints/act_so101_beyond"
|
305 |
+
class="bg-slate-50 border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100"
|
306 |
/>
|
307 |
</div>
|
308 |
</div>
|
309 |
|
310 |
<div class="grid grid-cols-2 gap-4">
|
311 |
<div class="space-y-2">
|
312 |
+
<Label for="cameraNames" class="text-purple-700 dark:text-purple-300">Camera Names</Label>
|
313 |
<Input
|
314 |
id="cameraNames"
|
315 |
bind:value={cameraNames}
|
316 |
placeholder="front, wrist, overhead"
|
317 |
+
class="bg-slate-50 border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100"
|
318 |
/>
|
319 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">Comma-separated camera names</p>
|
320 |
</div>
|
321 |
<div class="space-y-2">
|
322 |
+
<Label for="transportServerUrl" class="text-purple-700 dark:text-purple-300">Transport Server URL</Label>
|
323 |
<Input
|
324 |
id="transportServerUrl"
|
325 |
value={settings.transportServerUrl}
|
326 |
disabled
|
327 |
placeholder="http://localhost:8000"
|
328 |
+
class="bg-slate-50 border-slate-300 text-slate-900 opacity-60 cursor-not-allowed dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100"
|
329 |
title="Change this value in the settings panel"
|
330 |
/>
|
331 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">Configure in settings panel</p>
|
332 |
</div>
|
333 |
</div>
|
334 |
|
|
|
337 |
type="checkbox"
|
338 |
id="useWorkspace"
|
339 |
bind:checked={useProvidedWorkspace}
|
340 |
+
class="rounded border-slate-300 bg-slate-50 dark:border-slate-600 dark:bg-slate-800"
|
341 |
/>
|
342 |
+
<Label for="useWorkspace" class="text-purple-700 text-sm dark:text-purple-300">
|
343 |
Use current workspace ({workspaceId})
|
344 |
</Label>
|
345 |
</div>
|
|
|
356 |
variant="default"
|
357 |
onclick={handleCreateSession}
|
358 |
disabled={isConnecting || !sessionId.trim() || !policyPath.trim()}
|
359 |
+
class="w-full bg-purple-500 hover:bg-purple-600 disabled:opacity-50 dark:bg-purple-600 dark:hover:bg-purple-700"
|
360 |
>
|
361 |
{#if isConnecting}
|
362 |
<span class="icon-[mdi--loading] animate-spin mr-2 size-4"></span>
|
363 |
Creating Session...
|
364 |
{:else}
|
365 |
<span class="icon-[mdi--rocket-launch] mr-2 size-4"></span>
|
366 |
+
Create Inference Session
|
367 |
{/if}
|
368 |
</Button>
|
369 |
</div>
|
|
|
372 |
{/if}
|
373 |
|
374 |
<!-- Quick Info -->
|
375 |
+
<div class="rounded border border-slate-300 bg-slate-100/30 p-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-800/30 dark:text-slate-500">
|
376 |
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
377 |
+
Inference Sessions require a trained ACT model and create dedicated communication rooms for video inputs,
|
378 |
robot joint states, and control outputs in the inference server system.
|
379 |
</div>
|
380 |
</div>
|
src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte
CHANGED
@@ -25,7 +25,7 @@
|
|
25 |
|
26 |
async function handleConnectRobotInput() {
|
27 |
if (!compute.hasSession) {
|
28 |
-
toast.error('No
|
29 |
return;
|
30 |
}
|
31 |
|
@@ -36,10 +36,10 @@
|
|
36 |
|
37 |
isConnecting = true;
|
38 |
try {
|
39 |
-
// Get the joint input room ID from the
|
40 |
const jointInputRoomId = compute.sessionData?.joint_input_room_id;
|
41 |
if (!jointInputRoomId) {
|
42 |
-
throw new Error('No joint input room found in
|
43 |
}
|
44 |
|
45 |
// Find the selected robot
|
@@ -53,7 +53,7 @@
|
|
53 |
|
54 |
connectedRobotId = selectedRobotId;
|
55 |
|
56 |
-
toast.success('Robot input connected to
|
57 |
description: `Robot ${selectedRobotId} now sends joint data to AI`
|
58 |
});
|
59 |
|
@@ -98,54 +98,54 @@
|
|
98 |
|
99 |
<Dialog.Root bind:open>
|
100 |
<Dialog.Content
|
101 |
-
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
102 |
>
|
103 |
<Dialog.Header class="pb-3">
|
104 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
105 |
-
<span class="icon-[mdi--robot-industrial] size-5 text-amber-400"></span>
|
106 |
Robot Input - {compute.name || 'No Compute Selected'}
|
107 |
</Dialog.Title>
|
108 |
-
<Dialog.Description class="text-sm text-slate-400">
|
109 |
Connect robot joint data as input for AI inference
|
110 |
</Dialog.Description>
|
111 |
</Dialog.Header>
|
112 |
|
113 |
<div class="space-y-4">
|
114 |
-
<!--
|
115 |
<div
|
116 |
-
class="flex items-center justify-between rounded-lg border border-purple-
|
117 |
>
|
118 |
<div class="flex items-center gap-2">
|
119 |
-
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
120 |
-
<span class="text-sm font-medium text-purple-300">
|
121 |
</div>
|
122 |
{#if compute.hasSession}
|
123 |
-
<Badge variant="default" class="bg-purple-
|
124 |
{compute.statusInfo.statusText}
|
125 |
</Badge>
|
126 |
{:else}
|
127 |
-
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
128 |
{/if}
|
129 |
</div>
|
130 |
|
131 |
{#if !compute.hasSession}
|
132 |
-
<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
|
133 |
<Card.Header>
|
134 |
-
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
135 |
<span class="icon-[mdi--alert] size-4"></span>
|
136 |
-
|
137 |
</Card.Title>
|
138 |
</Card.Header>
|
139 |
-
<Card.Content class="text-sm text-yellow-300">
|
140 |
-
You need to create an
|
141 |
The session provides a joint input room for receiving robot data.
|
142 |
</Card.Content>
|
143 |
</Card.Root>
|
144 |
{:else}
|
145 |
<!-- Robot Selection and Connection -->
|
146 |
-
<Card.Root class="border-amber-500/30 bg-amber-500/5">
|
147 |
<Card.Header>
|
148 |
-
<Card.Title class="flex items-center gap-2 text-base text-amber-200">
|
149 |
<span class="icon-[mdi--robot-industrial] size-4"></span>
|
150 |
Robot Input Connection
|
151 |
</Card.Title>
|
@@ -153,10 +153,10 @@
|
|
153 |
<Card.Content class="space-y-4">
|
154 |
<!-- Available Robots -->
|
155 |
<div class="space-y-2">
|
156 |
-
<div class="text-sm font-medium text-amber-300">Available Robots:</div>
|
157 |
<div class="max-h-40 overflow-y-auto space-y-2">
|
158 |
{#if robots.length === 0}
|
159 |
-
<div class="text-center py-4 text-sm text-slate-400">
|
160 |
No robots available. Add robots first.
|
161 |
</div>
|
162 |
{:else}
|
@@ -164,21 +164,21 @@
|
|
164 |
<button
|
165 |
onclick={() => selectedRobotId = robot.id}
|
166 |
class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
|
167 |
-
? 'border-amber-500 bg-amber-500/20'
|
168 |
-
: 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
|
169 |
>
|
170 |
<div class="flex items-center justify-between">
|
171 |
<div>
|
172 |
-
<div class="text-xs text-slate-400">
|
173 |
ID: {robot.id}
|
174 |
</div>
|
175 |
-
<div class="text-xs text-slate-400">
|
176 |
Producers: {robot.producers.length}
|
177 |
</div>
|
178 |
</div>
|
179 |
<div class="flex items-center gap-2">
|
180 |
{#if robot.producers.length > 0}
|
181 |
-
<Badge variant="default" class="bg-green-
|
182 |
Active
|
183 |
</Badge>
|
184 |
{:else}
|
@@ -196,13 +196,13 @@
|
|
196 |
|
197 |
<!-- Connection Status -->
|
198 |
{#if selectedRobotId}
|
199 |
-
<div class="rounded-lg border border-amber-
|
200 |
<div class="flex items-center justify-between">
|
201 |
<div>
|
202 |
-
<p class="text-sm font-medium text-amber-300">
|
203 |
Selected Robot: {selectedRobotId}
|
204 |
</p>
|
205 |
-
<p class="text-xs text-amber-400/70">
|
206 |
{connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
|
207 |
</p>
|
208 |
</div>
|
@@ -212,7 +212,7 @@
|
|
212 |
size="sm"
|
213 |
onclick={handleConnectRobotInput}
|
214 |
disabled={isConnecting}
|
215 |
-
class="bg-amber-
|
216 |
>
|
217 |
{#if isConnecting}
|
218 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
@@ -240,20 +240,20 @@
|
|
240 |
</Card.Root>
|
241 |
|
242 |
<!-- Session Joint Input Details -->
|
243 |
-
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
244 |
<Card.Header>
|
245 |
-
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
246 |
<span class="icon-[mdi--information] size-4"></span>
|
247 |
-
Data Flow: Robot →
|
248 |
</Card.Title>
|
249 |
</Card.Header>
|
250 |
<Card.Content>
|
251 |
<div class="space-y-2 text-xs">
|
252 |
-
<div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
|
253 |
-
<span class="text-blue-
|
254 |
-
<span class="text-blue-
|
255 |
</div>
|
256 |
-
<div class="text-slate-
|
257 |
The robot will act as a <strong>PRODUCER</strong> and send its current joint positions to this room for AI processing.
|
258 |
The inference server receives this data as a CONSUMER.
|
259 |
All joint values should be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
|
@@ -264,16 +264,16 @@
|
|
264 |
|
265 |
<!-- Connection Status -->
|
266 |
{#if connectedRobotId}
|
267 |
-
<Card.Root class="border-green-500/30 bg-green-500/5">
|
268 |
<Card.Header>
|
269 |
-
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
270 |
<span class="icon-[mdi--check-circle] size-4"></span>
|
271 |
Active Connection
|
272 |
</Card.Title>
|
273 |
</Card.Header>
|
274 |
<Card.Content>
|
275 |
-
<div class="text-sm text-green-300">
|
276 |
-
Robot <span class="font-mono">{connectedRobotId}</span> is now sending joint data to the
|
277 |
The AI model will use this data along with camera inputs for inference.
|
278 |
</div>
|
279 |
</Card.Content>
|
@@ -282,7 +282,7 @@
|
|
282 |
{/if}
|
283 |
|
284 |
<!-- Quick Info -->
|
285 |
-
<div class="rounded border border-slate-
|
286 |
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
287 |
Robot input: Robot acts as PRODUCER sending joint positions → Inference server acts as CONSUMER receiving data for processing.
|
288 |
</div>
|
|
|
25 |
|
26 |
async function handleConnectRobotInput() {
|
27 |
if (!compute.hasSession) {
|
28 |
+
toast.error('No Inference Session available. Create a session first.');
|
29 |
return;
|
30 |
}
|
31 |
|
|
|
36 |
|
37 |
isConnecting = true;
|
38 |
try {
|
39 |
+
// Get the joint input room ID from the Inference Session
|
40 |
const jointInputRoomId = compute.sessionData?.joint_input_room_id;
|
41 |
if (!jointInputRoomId) {
|
42 |
+
throw new Error('No joint input room found in Inference Session');
|
43 |
}
|
44 |
|
45 |
// Find the selected robot
|
|
|
53 |
|
54 |
connectedRobotId = selectedRobotId;
|
55 |
|
56 |
+
toast.success('Robot input connected to Inference Session', {
|
57 |
description: `Robot ${selectedRobotId} now sends joint data to AI`
|
58 |
});
|
59 |
|
|
|
98 |
|
99 |
<Dialog.Root bind:open>
|
100 |
<Dialog.Content
|
101 |
+
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
102 |
>
|
103 |
<Dialog.Header class="pb-3">
|
104 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
105 |
+
<span class="icon-[mdi--robot-industrial] size-5 text-amber-500 dark:text-amber-400"></span>
|
106 |
Robot Input - {compute.name || 'No Compute Selected'}
|
107 |
</Dialog.Title>
|
108 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
109 |
Connect robot joint data as input for AI inference
|
110 |
</Dialog.Description>
|
111 |
</Dialog.Header>
|
112 |
|
113 |
<div class="space-y-4">
|
114 |
+
<!-- Inference Session Status -->
|
115 |
<div
|
116 |
+
class="flex items-center justify-between rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20"
|
117 |
>
|
118 |
<div class="flex items-center gap-2">
|
119 |
+
<span class="icon-[mdi--brain] size-4 text-purple-500 dark:text-purple-400"></span>
|
120 |
+
<span class="text-sm font-medium text-purple-700 dark:text-purple-300">Inference Session</span>
|
121 |
</div>
|
122 |
{#if compute.hasSession}
|
123 |
+
<Badge variant="default" class="bg-purple-500 text-xs dark:bg-purple-600">
|
124 |
{compute.statusInfo.statusText}
|
125 |
</Badge>
|
126 |
{:else}
|
127 |
+
<Badge variant="secondary" class="text-xs text-slate-600 dark:text-slate-400">No Session</Badge>
|
128 |
{/if}
|
129 |
</div>
|
130 |
|
131 |
{#if !compute.hasSession}
|
132 |
+
<Card.Root class="border-yellow-300/30 bg-yellow-100/5 dark:border-yellow-500/30 dark:bg-yellow-500/5">
|
133 |
<Card.Header>
|
134 |
+
<Card.Title class="flex items-center gap-2 text-base text-yellow-700 dark:text-yellow-200">
|
135 |
<span class="icon-[mdi--alert] size-4"></span>
|
136 |
+
Inference Session Required
|
137 |
</Card.Title>
|
138 |
</Card.Header>
|
139 |
+
<Card.Content class="text-sm text-yellow-700 dark:text-yellow-300">
|
140 |
+
You need to create an Inference Session before connecting robot inputs.
|
141 |
The session provides a joint input room for receiving robot data.
|
142 |
</Card.Content>
|
143 |
</Card.Root>
|
144 |
{:else}
|
145 |
<!-- Robot Selection and Connection -->
|
146 |
+
<Card.Root class="border-amber-300/30 bg-amber-100/5 dark:border-amber-500/30 dark:bg-amber-500/5">
|
147 |
<Card.Header>
|
148 |
+
<Card.Title class="flex items-center gap-2 text-base text-amber-700 dark:text-amber-200">
|
149 |
<span class="icon-[mdi--robot-industrial] size-4"></span>
|
150 |
Robot Input Connection
|
151 |
</Card.Title>
|
|
|
153 |
<Card.Content class="space-y-4">
|
154 |
<!-- Available Robots -->
|
155 |
<div class="space-y-2">
|
156 |
+
<div class="text-sm font-medium text-amber-700 dark:text-amber-300">Available Robots:</div>
|
157 |
<div class="max-h-40 overflow-y-auto space-y-2">
|
158 |
{#if robots.length === 0}
|
159 |
+
<div class="text-center py-4 text-sm text-slate-600 dark:text-slate-400">
|
160 |
No robots available. Add robots first.
|
161 |
</div>
|
162 |
{:else}
|
|
|
164 |
<button
|
165 |
onclick={() => selectedRobotId = robot.id}
|
166 |
class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
|
167 |
+
? 'border-amber-400 bg-amber-100/20 dark:border-amber-500 dark:bg-amber-500/20'
|
168 |
+
: 'border-slate-300 bg-slate-50/50 hover:bg-slate-100/50 dark:border-slate-600 dark:bg-slate-800/50 dark:hover:bg-slate-700/50'}"
|
169 |
>
|
170 |
<div class="flex items-center justify-between">
|
171 |
<div>
|
172 |
+
<div class="text-xs text-slate-600 dark:text-slate-400">
|
173 |
ID: {robot.id}
|
174 |
</div>
|
175 |
+
<div class="text-xs text-slate-600 dark:text-slate-400">
|
176 |
Producers: {robot.producers.length}
|
177 |
</div>
|
178 |
</div>
|
179 |
<div class="flex items-center gap-2">
|
180 |
{#if robot.producers.length > 0}
|
181 |
+
<Badge variant="default" class="bg-green-500 text-xs dark:bg-green-600">
|
182 |
Active
|
183 |
</Badge>
|
184 |
{:else}
|
|
|
196 |
|
197 |
<!-- Connection Status -->
|
198 |
{#if selectedRobotId}
|
199 |
+
<div class="rounded-lg border border-amber-300/30 bg-amber-100/20 p-3 dark:border-amber-500/30 dark:bg-amber-900/20">
|
200 |
<div class="flex items-center justify-between">
|
201 |
<div>
|
202 |
+
<p class="text-sm font-medium text-amber-700 dark:text-amber-300">
|
203 |
Selected Robot: {selectedRobotId}
|
204 |
</p>
|
205 |
+
<p class="text-xs text-amber-600/70 dark:text-amber-400/70">
|
206 |
{connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
|
207 |
</p>
|
208 |
</div>
|
|
|
212 |
size="sm"
|
213 |
onclick={handleConnectRobotInput}
|
214 |
disabled={isConnecting}
|
215 |
+
class="bg-amber-500 hover:bg-amber-600 text-xs disabled:opacity-50 dark:bg-amber-600 dark:hover:bg-amber-700"
|
216 |
>
|
217 |
{#if isConnecting}
|
218 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
|
|
240 |
</Card.Root>
|
241 |
|
242 |
<!-- Session Joint Input Details -->
|
243 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/5 dark:border-blue-500/30 dark:bg-blue-500/5">
|
244 |
<Card.Header>
|
245 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-700 dark:text-blue-200">
|
246 |
<span class="icon-[mdi--information] size-4"></span>
|
247 |
+
Data Flow: Robot → Inference Session
|
248 |
</Card.Title>
|
249 |
</Card.Header>
|
250 |
<Card.Content>
|
251 |
<div class="space-y-2 text-xs">
|
252 |
+
<div class="flex justify-between items-center p-2 rounded bg-slate-100/50 dark:bg-slate-800/50">
|
253 |
+
<span class="text-blue-700 font-medium dark:text-blue-300">Joint Input Room:</span>
|
254 |
+
<span class="text-blue-800 font-mono dark:text-blue-200">{compute.sessionData?.joint_input_room_id}</span>
|
255 |
</div>
|
256 |
+
<div class="text-slate-600 text-xs dark:text-slate-400">
|
257 |
The robot will act as a <strong>PRODUCER</strong> and send its current joint positions to this room for AI processing.
|
258 |
The inference server receives this data as a CONSUMER.
|
259 |
All joint values should be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
|
|
|
264 |
|
265 |
<!-- Connection Status -->
|
266 |
{#if connectedRobotId}
|
267 |
+
<Card.Root class="border-green-300/30 bg-green-100/5 dark:border-green-500/30 dark:bg-green-500/5">
|
268 |
<Card.Header>
|
269 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-700 dark:text-green-200">
|
270 |
<span class="icon-[mdi--check-circle] size-4"></span>
|
271 |
Active Connection
|
272 |
</Card.Title>
|
273 |
</Card.Header>
|
274 |
<Card.Content>
|
275 |
+
<div class="text-sm text-green-700 dark:text-green-300">
|
276 |
+
Robot <span class="font-mono">{connectedRobotId}</span> is now sending joint data to the Inference Session as a producer.
|
277 |
The AI model will use this data along with camera inputs for inference.
|
278 |
</div>
|
279 |
</Card.Content>
|
|
|
282 |
{/if}
|
283 |
|
284 |
<!-- Quick Info -->
|
285 |
+
<div class="rounded border border-slate-300 bg-slate-100/30 p-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-800/30 dark:text-slate-500">
|
286 |
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
287 |
Robot input: Robot acts as PRODUCER sending joint positions → Inference server acts as CONSUMER receiving data for processing.
|
288 |
</div>
|
src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte
CHANGED
@@ -25,7 +25,7 @@
|
|
25 |
|
26 |
async function handleConnectRobotOutput() {
|
27 |
if (!compute.hasSession) {
|
28 |
-
toast.error('No
|
29 |
return;
|
30 |
}
|
31 |
|
@@ -36,10 +36,10 @@
|
|
36 |
|
37 |
isConnecting = true;
|
38 |
try {
|
39 |
-
// Get the joint output room ID from the
|
40 |
const jointOutputRoomId = compute.sessionData?.joint_output_room_id;
|
41 |
if (!jointOutputRoomId) {
|
42 |
-
throw new Error('No joint output room found in
|
43 |
}
|
44 |
|
45 |
// Find the selected robot
|
@@ -53,7 +53,7 @@
|
|
53 |
|
54 |
connectedRobotId = selectedRobotId;
|
55 |
|
56 |
-
toast.success('Robot output connected to
|
57 |
description: `Robot ${selectedRobotId} now receives AI commands`
|
58 |
});
|
59 |
|
@@ -95,54 +95,54 @@
|
|
95 |
|
96 |
<Dialog.Root bind:open>
|
97 |
<Dialog.Content
|
98 |
-
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
99 |
>
|
100 |
<Dialog.Header class="pb-3">
|
101 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
102 |
-
<span class="icon-[mdi--robot-outline] size-5 text-blue-400"></span>
|
103 |
Robot Output - {compute.name || 'No Compute Selected'}
|
104 |
</Dialog.Title>
|
105 |
-
<Dialog.Description class="text-sm text-slate-400">
|
106 |
Connect AI command output to control robot actuators
|
107 |
</Dialog.Description>
|
108 |
</Dialog.Header>
|
109 |
|
110 |
<div class="space-y-4">
|
111 |
-
<!--
|
112 |
<div
|
113 |
-
class="flex items-center justify-between rounded-lg border border-purple-
|
114 |
>
|
115 |
<div class="flex items-center gap-2">
|
116 |
-
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
117 |
-
<span class="text-sm font-medium text-purple-300">
|
118 |
</div>
|
119 |
{#if compute.hasSession}
|
120 |
-
<Badge variant="default" class="bg-purple-
|
121 |
{compute.statusInfo.statusText}
|
122 |
</Badge>
|
123 |
{:else}
|
124 |
-
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
125 |
{/if}
|
126 |
</div>
|
127 |
|
128 |
{#if !compute.hasSession}
|
129 |
-
<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
|
130 |
<Card.Header>
|
131 |
-
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
132 |
<span class="icon-[mdi--alert] size-4"></span>
|
133 |
-
|
134 |
</Card.Title>
|
135 |
</Card.Header>
|
136 |
-
<Card.Content class="text-sm text-yellow-300">
|
137 |
-
You need to create an
|
138 |
The session provides a joint output room for sending AI commands.
|
139 |
</Card.Content>
|
140 |
</Card.Root>
|
141 |
{:else}
|
142 |
<!-- Robot Selection and Connection -->
|
143 |
-
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
144 |
<Card.Header>
|
145 |
-
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
146 |
<span class="icon-[mdi--robot-outline] size-4"></span>
|
147 |
Robot Output Connection
|
148 |
</Card.Title>
|
@@ -150,10 +150,10 @@
|
|
150 |
<Card.Content class="space-y-4">
|
151 |
<!-- Available Robots -->
|
152 |
<div class="space-y-2">
|
153 |
-
<div class="text-sm font-medium text-blue-300">Available Robots:</div>
|
154 |
<div class="max-h-40 overflow-y-auto space-y-2">
|
155 |
{#if robots.length === 0}
|
156 |
-
<div class="text-center py-4 text-sm text-slate-400">
|
157 |
No robots available. Add robots first.
|
158 |
</div>
|
159 |
{:else}
|
@@ -161,21 +161,21 @@
|
|
161 |
<button
|
162 |
onclick={() => selectedRobotId = robot.id}
|
163 |
class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
|
164 |
-
? 'border-blue-500 bg-blue-500/20'
|
165 |
-
: 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
|
166 |
>
|
167 |
<div class="flex items-center justify-between">
|
168 |
<div>
|
169 |
-
<div class="text-xs text-slate-400">
|
170 |
ID: {robot.id}
|
171 |
</div>
|
172 |
-
<div class="text-xs text-slate-400">
|
173 |
Consumer: {robot.hasConsumer ? 'Connected' : 'None'}
|
174 |
</div>
|
175 |
</div>
|
176 |
<div class="flex items-center gap-2">
|
177 |
{#if robot.hasConsumer}
|
178 |
-
<Badge variant="default" class="bg-green-
|
179 |
Active
|
180 |
</Badge>
|
181 |
{:else}
|
@@ -193,13 +193,13 @@
|
|
193 |
|
194 |
<!-- Connection Status -->
|
195 |
{#if selectedRobotId}
|
196 |
-
<div class="rounded-lg border border-blue-
|
197 |
<div class="flex items-center justify-between">
|
198 |
<div>
|
199 |
-
<p class="text-sm font-medium text-blue-300">
|
200 |
Selected Robot: {selectedRobotId}
|
201 |
</p>
|
202 |
-
<p class="text-xs text-blue-400/70">
|
203 |
{connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
|
204 |
</p>
|
205 |
</div>
|
@@ -209,7 +209,7 @@
|
|
209 |
size="sm"
|
210 |
onclick={handleConnectRobotOutput}
|
211 |
disabled={isConnecting}
|
212 |
-
class="bg-blue-
|
213 |
>
|
214 |
{#if isConnecting}
|
215 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
@@ -237,20 +237,20 @@
|
|
237 |
</Card.Root>
|
238 |
|
239 |
<!-- Session Joint Output Details -->
|
240 |
-
<Card.Root class="border-orange-500/30 bg-orange-500/5">
|
241 |
<Card.Header>
|
242 |
-
<Card.Title class="flex items-center gap-2 text-base text-orange-200">
|
243 |
<span class="icon-[mdi--information] size-4"></span>
|
244 |
-
Data Flow:
|
245 |
</Card.Title>
|
246 |
</Card.Header>
|
247 |
<Card.Content>
|
248 |
<div class="space-y-2 text-xs">
|
249 |
-
<div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
|
250 |
-
<span class="text-orange-
|
251 |
-
<span class="text-orange-
|
252 |
</div>
|
253 |
-
<div class="text-slate-
|
254 |
The inference server will act as a <strong>PRODUCER</strong> and send predicted joint commands to this room for robot execution.
|
255 |
The robot receives this data as a CONSUMER.
|
256 |
All joint values will be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
|
@@ -261,15 +261,15 @@
|
|
261 |
|
262 |
<!-- Connection Status -->
|
263 |
{#if connectedRobotId}
|
264 |
-
<Card.Root class="border-green-500/30 bg-green-500/5">
|
265 |
<Card.Header>
|
266 |
-
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
267 |
<span class="icon-[mdi--check-circle] size-4"></span>
|
268 |
Active Connection
|
269 |
</Card.Title>
|
270 |
</Card.Header>
|
271 |
<Card.Content>
|
272 |
-
<div class="text-sm text-green-300">
|
273 |
Robot <span class="font-mono">{connectedRobotId}</span> is now receiving AI commands as a consumer.
|
274 |
The robot will execute joint movements based on AI inference results.
|
275 |
</div>
|
@@ -279,7 +279,7 @@
|
|
279 |
{/if}
|
280 |
|
281 |
<!-- Quick Info -->
|
282 |
-
<div class="rounded border border-slate-
|
283 |
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
284 |
Robot output: Inference server acts as PRODUCER sending commands → Robot acts as CONSUMER receiving and executing movements.
|
285 |
</div>
|
|
|
25 |
|
26 |
async function handleConnectRobotOutput() {
|
27 |
if (!compute.hasSession) {
|
28 |
+
toast.error('No Inference Session available. Create a session first.');
|
29 |
return;
|
30 |
}
|
31 |
|
|
|
36 |
|
37 |
isConnecting = true;
|
38 |
try {
|
39 |
+
// Get the joint output room ID from the Inference Session
|
40 |
const jointOutputRoomId = compute.sessionData?.joint_output_room_id;
|
41 |
if (!jointOutputRoomId) {
|
42 |
+
throw new Error('No joint output room found in Inference Session');
|
43 |
}
|
44 |
|
45 |
// Find the selected robot
|
|
|
53 |
|
54 |
connectedRobotId = selectedRobotId;
|
55 |
|
56 |
+
toast.success('Robot output connected to Inference Session', {
|
57 |
description: `Robot ${selectedRobotId} now receives AI commands`
|
58 |
});
|
59 |
|
|
|
95 |
|
96 |
<Dialog.Root bind:open>
|
97 |
<Dialog.Content
|
98 |
+
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
99 |
>
|
100 |
<Dialog.Header class="pb-3">
|
101 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
102 |
+
<span class="icon-[mdi--robot-outline] size-5 text-blue-500 dark:text-blue-400"></span>
|
103 |
Robot Output - {compute.name || 'No Compute Selected'}
|
104 |
</Dialog.Title>
|
105 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
106 |
Connect AI command output to control robot actuators
|
107 |
</Dialog.Description>
|
108 |
</Dialog.Header>
|
109 |
|
110 |
<div class="space-y-4">
|
111 |
+
<!-- Inference Session Status -->
|
112 |
<div
|
113 |
+
class="flex items-center justify-between rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20"
|
114 |
>
|
115 |
<div class="flex items-center gap-2">
|
116 |
+
<span class="icon-[mdi--brain] size-4 text-purple-500 dark:text-purple-400"></span>
|
117 |
+
<span class="text-sm font-medium text-purple-700 dark:text-purple-300">Inference Session</span>
|
118 |
</div>
|
119 |
{#if compute.hasSession}
|
120 |
+
<Badge variant="default" class="bg-purple-500 text-xs dark:bg-purple-600">
|
121 |
{compute.statusInfo.statusText}
|
122 |
</Badge>
|
123 |
{:else}
|
124 |
+
<Badge variant="secondary" class="text-xs text-slate-600 dark:text-slate-400">No Session</Badge>
|
125 |
{/if}
|
126 |
</div>
|
127 |
|
128 |
{#if !compute.hasSession}
|
129 |
+
<Card.Root class="border-yellow-300/30 bg-yellow-100/5 dark:border-yellow-500/30 dark:bg-yellow-500/5">
|
130 |
<Card.Header>
|
131 |
+
<Card.Title class="flex items-center gap-2 text-base text-yellow-700 dark:text-yellow-200">
|
132 |
<span class="icon-[mdi--alert] size-4"></span>
|
133 |
+
Inference Session Required
|
134 |
</Card.Title>
|
135 |
</Card.Header>
|
136 |
+
<Card.Content class="text-sm text-yellow-700 dark:text-yellow-300">
|
137 |
+
You need to create an Inference Session before connecting robot outputs.
|
138 |
The session provides a joint output room for sending AI commands.
|
139 |
</Card.Content>
|
140 |
</Card.Root>
|
141 |
{:else}
|
142 |
<!-- Robot Selection and Connection -->
|
143 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/5 dark:border-blue-500/30 dark:bg-blue-500/5">
|
144 |
<Card.Header>
|
145 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-700 dark:text-blue-200">
|
146 |
<span class="icon-[mdi--robot-outline] size-4"></span>
|
147 |
Robot Output Connection
|
148 |
</Card.Title>
|
|
|
150 |
<Card.Content class="space-y-4">
|
151 |
<!-- Available Robots -->
|
152 |
<div class="space-y-2">
|
153 |
+
<div class="text-sm font-medium text-blue-700 dark:text-blue-300">Available Robots:</div>
|
154 |
<div class="max-h-40 overflow-y-auto space-y-2">
|
155 |
{#if robots.length === 0}
|
156 |
+
<div class="text-center py-4 text-sm text-slate-600 dark:text-slate-400">
|
157 |
No robots available. Add robots first.
|
158 |
</div>
|
159 |
{:else}
|
|
|
161 |
<button
|
162 |
onclick={() => selectedRobotId = robot.id}
|
163 |
class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
|
164 |
+
? 'border-blue-400 bg-blue-100/20 dark:border-blue-500 dark:bg-blue-500/20'
|
165 |
+
: 'border-slate-300 bg-slate-50/50 hover:bg-slate-100/50 dark:border-slate-600 dark:bg-slate-800/50 dark:hover:bg-slate-700/50'}"
|
166 |
>
|
167 |
<div class="flex items-center justify-between">
|
168 |
<div>
|
169 |
+
<div class="text-xs text-slate-600 dark:text-slate-400">
|
170 |
ID: {robot.id}
|
171 |
</div>
|
172 |
+
<div class="text-xs text-slate-600 dark:text-slate-400">
|
173 |
Consumer: {robot.hasConsumer ? 'Connected' : 'None'}
|
174 |
</div>
|
175 |
</div>
|
176 |
<div class="flex items-center gap-2">
|
177 |
{#if robot.hasConsumer}
|
178 |
+
<Badge variant="default" class="bg-green-500 text-xs dark:bg-green-600">
|
179 |
Active
|
180 |
</Badge>
|
181 |
{:else}
|
|
|
193 |
|
194 |
<!-- Connection Status -->
|
195 |
{#if selectedRobotId}
|
196 |
+
<div class="rounded-lg border border-blue-300/30 bg-blue-100/20 p-3 dark:border-blue-500/30 dark:bg-blue-900/20">
|
197 |
<div class="flex items-center justify-between">
|
198 |
<div>
|
199 |
+
<p class="text-sm font-medium text-blue-700 dark:text-blue-300">
|
200 |
Selected Robot: {selectedRobotId}
|
201 |
</p>
|
202 |
+
<p class="text-xs text-blue-600/70 dark:text-blue-400/70">
|
203 |
{connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
|
204 |
</p>
|
205 |
</div>
|
|
|
209 |
size="sm"
|
210 |
onclick={handleConnectRobotOutput}
|
211 |
disabled={isConnecting}
|
212 |
+
class="bg-blue-500 hover:bg-blue-600 text-xs disabled:opacity-50 dark:bg-blue-600 dark:hover:bg-blue-700"
|
213 |
>
|
214 |
{#if isConnecting}
|
215 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
|
|
237 |
</Card.Root>
|
238 |
|
239 |
<!-- Session Joint Output Details -->
|
240 |
+
<Card.Root class="border-orange-300/30 bg-orange-100/5 dark:border-orange-500/30 dark:bg-orange-500/5">
|
241 |
<Card.Header>
|
242 |
+
<Card.Title class="flex items-center gap-2 text-base text-orange-700 dark:text-orange-200">
|
243 |
<span class="icon-[mdi--information] size-4"></span>
|
244 |
+
Data Flow: Inference Session → Robot
|
245 |
</Card.Title>
|
246 |
</Card.Header>
|
247 |
<Card.Content>
|
248 |
<div class="space-y-2 text-xs">
|
249 |
+
<div class="flex justify-between items-center p-2 rounded bg-slate-100/50 dark:bg-slate-800/50">
|
250 |
+
<span class="text-orange-700 font-medium dark:text-orange-300">Joint Output Room:</span>
|
251 |
+
<span class="text-orange-800 font-mono dark:text-orange-200">{compute.sessionData?.joint_output_room_id}</span>
|
252 |
</div>
|
253 |
+
<div class="text-slate-600 text-xs dark:text-slate-400">
|
254 |
The inference server will act as a <strong>PRODUCER</strong> and send predicted joint commands to this room for robot execution.
|
255 |
The robot receives this data as a CONSUMER.
|
256 |
All joint values will be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
|
|
|
261 |
|
262 |
<!-- Connection Status -->
|
263 |
{#if connectedRobotId}
|
264 |
+
<Card.Root class="border-green-300/30 bg-green-100/5 dark:border-green-500/30 dark:bg-green-500/5">
|
265 |
<Card.Header>
|
266 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-700 dark:text-green-200">
|
267 |
<span class="icon-[mdi--check-circle] size-4"></span>
|
268 |
Active Connection
|
269 |
</Card.Title>
|
270 |
</Card.Header>
|
271 |
<Card.Content>
|
272 |
+
<div class="text-sm text-green-700 dark:text-green-300">
|
273 |
Robot <span class="font-mono">{connectedRobotId}</span> is now receiving AI commands as a consumer.
|
274 |
The robot will execute joint movements based on AI inference results.
|
275 |
</div>
|
|
|
279 |
{/if}
|
280 |
|
281 |
<!-- Quick Info -->
|
282 |
+
<div class="rounded border border-slate-300 bg-slate-100/30 p-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-800/30 dark:text-slate-500">
|
283 |
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
284 |
Robot output: Inference server acts as PRODUCER sending commands → Robot acts as CONSUMER receiving and executing movements.
|
285 |
</div>
|
src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte
CHANGED
@@ -32,7 +32,7 @@
|
|
32 |
|
33 |
async function handleConnectLocalCamera() {
|
34 |
if (!compute.hasSession) {
|
35 |
-
toast.error('No
|
36 |
return;
|
37 |
}
|
38 |
|
@@ -65,7 +65,7 @@
|
|
65 |
// Start streaming
|
66 |
await videoProducer.startCamera();
|
67 |
|
68 |
-
toast.success(`Camera connected to
|
69 |
description: `Local camera streaming to ${selectedCameraName} input`
|
70 |
});
|
71 |
|
@@ -124,13 +124,13 @@
|
|
124 |
</Dialog.Header>
|
125 |
|
126 |
<div class="space-y-4">
|
127 |
-
<!--
|
128 |
<div
|
129 |
class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
|
130 |
>
|
131 |
<div class="flex items-center gap-2">
|
132 |
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
133 |
-
<span class="text-sm font-medium text-purple-300">
|
134 |
</div>
|
135 |
{#if compute.hasSession}
|
136 |
<Badge variant="default" class="bg-purple-600 text-xs">
|
@@ -146,11 +146,11 @@
|
|
146 |
<Card.Header>
|
147 |
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
148 |
<span class="icon-[mdi--alert] size-4"></span>
|
149 |
-
|
150 |
</Card.Title>
|
151 |
</Card.Header>
|
152 |
<Card.Content class="text-sm text-yellow-300">
|
153 |
-
You need to create an
|
154 |
The session defines which camera names are available for connection.
|
155 |
</Card.Content>
|
156 |
</Card.Root>
|
|
|
32 |
|
33 |
async function handleConnectLocalCamera() {
|
34 |
if (!compute.hasSession) {
|
35 |
+
toast.error('No Inference Session available. Create a session first.');
|
36 |
return;
|
37 |
}
|
38 |
|
|
|
65 |
// Start streaming
|
66 |
await videoProducer.startCamera();
|
67 |
|
68 |
+
toast.success(`Camera connected to Inference Session`, {
|
69 |
description: `Local camera streaming to ${selectedCameraName} input`
|
70 |
});
|
71 |
|
|
|
124 |
</Dialog.Header>
|
125 |
|
126 |
<div class="space-y-4">
|
127 |
+
<!-- Inference Session Status -->
|
128 |
<div
|
129 |
class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
|
130 |
>
|
131 |
<div class="flex items-center gap-2">
|
132 |
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
133 |
+
<span class="text-sm font-medium text-purple-300">Inference Session</span>
|
134 |
</div>
|
135 |
{#if compute.hasSession}
|
136 |
<Badge variant="default" class="bg-purple-600 text-xs">
|
|
|
146 |
<Card.Header>
|
147 |
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
148 |
<span class="icon-[mdi--alert] size-4"></span>
|
149 |
+
Inference Session Required
|
150 |
</Card.Title>
|
151 |
</Card.Header>
|
152 |
<Card.Content class="text-sm text-yellow-300">
|
153 |
+
You need to create an Inference Session before connecting video inputs.
|
154 |
The session defines which camera names are available for connection.
|
155 |
</Card.Content>
|
156 |
</Card.Root>
|
src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte
CHANGED
@@ -23,7 +23,7 @@
|
|
23 |
|
24 |
<!--
|
25 |
@component
|
26 |
-
Compact input box showing the status of video and robot inputs for
|
27 |
Displays input connection information when session exists or connection prompt when disconnected.
|
28 |
-->
|
29 |
|
|
|
23 |
|
24 |
<!--
|
25 |
@component
|
26 |
+
Compact input box showing the status of video and robot inputs for Inference Sessions.
|
27 |
Displays input connection information when session exists or connection prompt when disconnected.
|
28 |
-->
|
29 |
|
src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte
CHANGED
@@ -28,7 +28,7 @@
|
|
28 |
|
29 |
<!--
|
30 |
@component
|
31 |
-
Compact output box showing the status of robot outputs for
|
32 |
Displays output connection information when session exists or connection prompt when disconnected.
|
33 |
-->
|
34 |
|
|
|
28 |
|
29 |
<!--
|
30 |
@component
|
31 |
+
Compact output box showing the status of robot outputs for Inference Sessions.
|
32 |
Displays output connection information when session exists or connection prompt when disconnected.
|
33 |
-->
|
34 |
|
src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte
CHANGED
@@ -22,7 +22,7 @@
|
|
22 |
|
23 |
<!--
|
24 |
@component
|
25 |
-
Compact robot input box showing the status of robot joint states input for
|
26 |
Displays robot connection information when session exists or connection prompt when disconnected.
|
27 |
-->
|
28 |
|
|
|
22 |
|
23 |
<!--
|
24 |
@component
|
25 |
+
Compact robot input box showing the status of robot joint states input for Inference Sessions.
|
26 |
Displays robot connection information when session exists or connection prompt when disconnected.
|
27 |
-->
|
28 |
|
src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte
CHANGED
@@ -16,7 +16,7 @@
|
|
16 |
|
17 |
<!--
|
18 |
@component
|
19 |
-
Robot output box showing the status of robot joint commands output from
|
20 |
Displays robot command output information when session exists or connection prompt when disconnected.
|
21 |
-->
|
22 |
|
|
|
16 |
|
17 |
<!--
|
18 |
@component
|
19 |
+
Robot output box showing the status of robot joint commands output from Inference Sessions.
|
20 |
Displays robot command output information when session exists or connection prompt when disconnected.
|
21 |
-->
|
22 |
|
src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte
CHANGED
@@ -23,7 +23,7 @@
|
|
23 |
|
24 |
<!--
|
25 |
@component
|
26 |
-
Compact video input box showing the status of camera video streams for
|
27 |
Displays video connection information when session exists or connection prompt when disconnected.
|
28 |
-->
|
29 |
|
|
|
23 |
|
24 |
<!--
|
25 |
@component
|
26 |
+
Compact video input box showing the status of camera video streams for Inference Sessions.
|
27 |
Displays video connection information when session exists or connection prompt when disconnected.
|
28 |
-->
|
29 |
|
src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte
CHANGED
@@ -193,14 +193,14 @@
|
|
193 |
|
194 |
<Dialog.Root bind:open>
|
195 |
<Dialog.Content
|
196 |
-
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-600 bg-slate-900 text-slate-100"
|
197 |
>
|
198 |
<Dialog.Header class="pb-3">
|
199 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
200 |
-
<span class="icon-[mdi--account-supervisor] size-5 text-green-400"></span>
|
201 |
Input Connection - Robot {robot.id}
|
202 |
</Dialog.Title>
|
203 |
-
<Dialog.Description class="text-sm text-slate-400">
|
204 |
Configure how this robot receives commands. Choose between direct hardware control or remote collaboration.
|
205 |
</Dialog.Description>
|
206 |
</Dialog.Header>
|
@@ -209,10 +209,10 @@
|
|
209 |
<div class="space-y-4 pb-4">
|
210 |
<!-- Error display -->
|
211 |
{#if error}
|
212 |
-
<Alert.Root class="border-red-500/30 bg-red-900/20">
|
213 |
-
<span class="icon-[mdi--alert-circle] size-4 text-red-400"></span>
|
214 |
-
<Alert.Title class="text-red-300">Connection Error</Alert.Title>
|
215 |
-
<Alert.Description class="text-red-
|
216 |
{error}
|
217 |
</Alert.Description>
|
218 |
</Alert.Root>
|
@@ -220,24 +220,24 @@
|
|
220 |
|
221 |
<!-- USB Calibration Panel -->
|
222 |
{#if showUSBCalibration}
|
223 |
-
<Card.Root class="border-orange-500/30 bg-orange-900/20">
|
224 |
<Card.Header>
|
225 |
<div class="flex justify-between items-center">
|
226 |
-
<Card.Title class="text-lg font-semibold text-orange-200">
|
227 |
Hardware Calibration Required
|
228 |
</Card.Title>
|
229 |
<button
|
230 |
onclick={onCalibrationCancel}
|
231 |
-
class="text-gray-400 hover:text-white"
|
232 |
>
|
233 |
✕
|
234 |
</button>
|
235 |
</div>
|
236 |
</Card.Header>
|
237 |
<Card.Content class="space-y-4">
|
238 |
-
<Alert.Root class="border-orange-500/30 bg-orange-500/10">
|
239 |
-
<span class="icon-[mdi--information] size-4 text-orange-400"></span>
|
240 |
-
<Alert.Description class="text-orange-
|
241 |
Before connecting to the physical robot, calibration is required to map the servo positions to software values. This ensures accurate control.
|
242 |
</Alert.Description>
|
243 |
</Alert.Root>
|
@@ -253,23 +253,23 @@
|
|
253 |
{:else}
|
254 |
|
255 |
<!-- Current Status Overview -->
|
256 |
-
<Card.Root class="border-green-500/30 bg-green-900/20">
|
257 |
<Card.Content class="p-4">
|
258 |
<div class="flex items-center justify-between">
|
259 |
<div class="flex items-center gap-2">
|
260 |
-
<span class="icon-[mdi--connection] size-4 text-green-400"></span>
|
261 |
-
<span class="text-sm font-medium text-green-300">Current Input Source</span>
|
262 |
</div>
|
263 |
{#if robot.hasConsumer}
|
264 |
-
<Badge variant="default" class="bg-green-
|
265 |
{robot.consumer?.name || 'Connected'}
|
266 |
</Badge>
|
267 |
{:else}
|
268 |
-
<Badge variant="secondary" class="text-xs text-slate-400">No Input Connected</Badge>
|
269 |
{/if}
|
270 |
</div>
|
271 |
{#if robot.hasConsumer}
|
272 |
-
<div class="mt-2 text-xs text-green-400/70">
|
273 |
Status: {robot.consumer?.status.isConnected ? 'Connected' : 'Disconnected'}
|
274 |
</div>
|
275 |
{/if}
|
@@ -277,24 +277,24 @@
|
|
277 |
</Card.Root>
|
278 |
|
279 |
<!-- Local Hardware Connection -->
|
280 |
-
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
281 |
<Card.Header>
|
282 |
-
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
283 |
<span class="icon-[mdi--usb-port] size-4"></span>
|
284 |
Local Hardware (USB)
|
285 |
</Card.Title>
|
286 |
-
<Card.Description class="text-xs text-blue-300/70">
|
287 |
Read physical robot movements in real-time
|
288 |
</Card.Description>
|
289 |
</Card.Header>
|
290 |
<Card.Content class="space-y-3">
|
291 |
{#if robot.hasConsumer && robot.consumer?.name === 'USB Consumer'}
|
292 |
<!-- USB Connected State -->
|
293 |
-
<div class="rounded-lg border border-blue-
|
294 |
<div class="flex items-center justify-between">
|
295 |
<div>
|
296 |
-
<p class="text-sm font-medium text-blue-300">Hardware Connected</p>
|
297 |
-
<p class="text-xs text-blue-400/70">Reading physical servo positions</p>
|
298 |
</div>
|
299 |
<Button
|
300 |
variant="destructive"
|
@@ -314,14 +314,14 @@
|
|
314 |
variant="secondary"
|
315 |
onclick={connectUSBInput}
|
316 |
disabled={isConnecting || robot.hasConsumer}
|
317 |
-
class="w-full bg-blue-
|
318 |
>
|
319 |
<span class="icon-[mdi--usb] mr-2 size-4"></span>
|
320 |
{isConnecting ? 'Connecting...' : 'Connect to Hardware'}
|
321 |
</Button>
|
322 |
|
323 |
{#if robot.hasConsumer}
|
324 |
-
<p class="text-xs text-slate-500">
|
325 |
Disconnect current input to connect USB hardware
|
326 |
</p>
|
327 |
{/if}
|
@@ -330,15 +330,15 @@
|
|
330 |
</Card.Root>
|
331 |
|
332 |
<!-- Remote Collaboration -->
|
333 |
-
<Card.Root class="border-purple-500/30 bg-purple-500/5">
|
334 |
<Card.Header>
|
335 |
<div class="flex items-center justify-between">
|
336 |
<div>
|
337 |
-
<Card.Title class="flex items-center gap-2 text-base text-purple-200">
|
338 |
<span class="icon-[mdi--cloud-sync] size-4"></span>
|
339 |
Remote Collaboration (Rooms)
|
340 |
</Card.Title>
|
341 |
-
<Card.Description class="text-xs text-purple-300/70">
|
342 |
Receive commands from AI systems, remote users, or other software
|
343 |
</Card.Description>
|
344 |
</div>
|
@@ -347,7 +347,7 @@
|
|
347 |
size="sm"
|
348 |
onclick={refreshRooms}
|
349 |
disabled={robotManager.roomsLoading || isConnecting}
|
350 |
-
class="h-7 px-2 text-xs text-purple-300 hover:text-purple-200 hover:bg-purple-500/20"
|
351 |
>
|
352 |
{#if robotManager.roomsLoading}
|
353 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
@@ -362,11 +362,11 @@
|
|
362 |
<Card.Content class="space-y-4">
|
363 |
{#if robot.hasConsumer && robot.consumer?.name?.includes('Remote Consumer')}
|
364 |
<!-- Remote Connected State -->
|
365 |
-
<div class="rounded-lg border border-purple-
|
366 |
<div class="flex items-center justify-between">
|
367 |
<div>
|
368 |
-
<p class="text-sm font-medium text-purple-300">Room Connected</p>
|
369 |
-
<p class="text-xs text-purple-400/70">Receiving remote commands</p>
|
370 |
</div>
|
371 |
<Button
|
372 |
variant="destructive"
|
@@ -382,20 +382,20 @@
|
|
382 |
</div>
|
383 |
{:else}
|
384 |
<!-- Create New Room -->
|
385 |
-
<div class="rounded border-2 border-dashed border-green-
|
386 |
<div class="space-y-2">
|
387 |
<div class="flex items-center gap-2">
|
388 |
-
<span class="icon-[mdi--plus-circle] size-4 text-green-400"></span>
|
389 |
-
<p class="text-sm font-medium text-green-300">Create New Room</p>
|
390 |
</div>
|
391 |
-
<p class="text-xs text-green-400/70">
|
392 |
Create a room where others can send commands to this robot
|
393 |
</p>
|
394 |
<input
|
395 |
bind:value={customRoomId}
|
396 |
placeholder={`Room ID (default: ${robot.id})`}
|
397 |
disabled={isConnecting || robot.hasConsumer}
|
398 |
-
class="w-full px-2 py-1 bg-slate-
|
399 |
/>
|
400 |
<div class="flex gap-1">
|
401 |
<Button
|
@@ -403,7 +403,7 @@
|
|
403 |
size="sm"
|
404 |
onclick={createRoom}
|
405 |
disabled={isConnecting || robot.hasConsumer}
|
406 |
-
class="h-6 px-2 text-xs bg-green-
|
407 |
>
|
408 |
Create Only
|
409 |
</Button>
|
@@ -412,7 +412,7 @@
|
|
412 |
size="sm"
|
413 |
onclick={createRoomAndJoinAsInput}
|
414 |
disabled={isConnecting || robot.hasConsumer}
|
415 |
-
class="h-6 px-2 text-xs bg-green-
|
416 |
>
|
417 |
Create & Join as Input
|
418 |
</Button>
|
@@ -423,26 +423,26 @@
|
|
423 |
<!-- Existing Rooms -->
|
424 |
<div class="space-y-2">
|
425 |
<div class="flex items-center justify-between">
|
426 |
-
<span class="text-xs font-medium text-purple-300">Join Existing Room:</span>
|
427 |
-
<span class="text-xs text-slate-400">
|
428 |
{robotManager.rooms.length} room{robotManager.rooms.length !== 1 ? 's' : ''} available
|
429 |
</span>
|
430 |
</div>
|
431 |
|
432 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
433 |
{#if robotManager.rooms.length === 0}
|
434 |
-
<div class="text-center py-3 text-xs text-slate-400">
|
435 |
{robotManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
436 |
</div>
|
437 |
{:else}
|
438 |
{#each robotManager.rooms as room}
|
439 |
-
<div class="rounded border border-slate-
|
440 |
<div class="flex items-start justify-between gap-3">
|
441 |
<div class="flex-1 min-w-0">
|
442 |
-
<p class="text-xs font-medium text-slate-
|
443 |
{room.id}
|
444 |
</p>
|
445 |
-
<div class="flex gap-3 text-xs text-slate-400">
|
446 |
<span>{room.has_producer ? '📤 Has Output' : '📥 No Output'}</span>
|
447 |
<span>👥 {room.participants?.total || 0} users</span>
|
448 |
</div>
|
@@ -455,7 +455,7 @@
|
|
455 |
joinRoomAsInput();
|
456 |
}}
|
457 |
disabled={isConnecting || robot.hasConsumer}
|
458 |
-
class="h-6 px-2 text-xs bg-purple-
|
459 |
>
|
460 |
<span class="icon-[mdi--login] mr-1 size-3"></span>
|
461 |
Join as Input
|
@@ -468,7 +468,7 @@
|
|
468 |
</div>
|
469 |
|
470 |
{#if robot.hasConsumer}
|
471 |
-
<p class="text-xs text-slate-500">
|
472 |
Disconnect current input to join a room
|
473 |
</p>
|
474 |
{/if}
|
@@ -477,10 +477,10 @@
|
|
477 |
</Card.Root>
|
478 |
|
479 |
<!-- Help Information -->
|
480 |
-
<Alert.Root class="border-slate-700 bg-slate-800/30">
|
481 |
-
<span class="icon-[mdi--help-circle] size-4 text-slate-400"></span>
|
482 |
-
<Alert.Title class="text-slate-300">Input Sources</Alert.Title>
|
483 |
-
<Alert.Description class="text-slate-
|
484 |
<strong>USB:</strong> Read physical movements • <strong>Remote:</strong> Receive network commands • Only one active at a time
|
485 |
</Alert.Description>
|
486 |
</Alert.Root>
|
|
|
193 |
|
194 |
<Dialog.Root bind:open>
|
195 |
<Dialog.Content
|
196 |
+
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
197 |
>
|
198 |
<Dialog.Header class="pb-3">
|
199 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
200 |
+
<span class="icon-[mdi--account-supervisor] size-5 text-green-500 dark:text-green-400"></span>
|
201 |
Input Connection - Robot {robot.id}
|
202 |
</Dialog.Title>
|
203 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
204 |
Configure how this robot receives commands. Choose between direct hardware control or remote collaboration.
|
205 |
</Dialog.Description>
|
206 |
</Dialog.Header>
|
|
|
209 |
<div class="space-y-4 pb-4">
|
210 |
<!-- Error display -->
|
211 |
{#if error}
|
212 |
+
<Alert.Root class="border-red-300/30 bg-red-100/20 dark:border-red-500/30 dark:bg-red-900/20">
|
213 |
+
<span class="icon-[mdi--alert-circle] size-4 text-red-500 dark:text-red-400"></span>
|
214 |
+
<Alert.Title class="text-red-700 dark:text-red-300">Connection Error</Alert.Title>
|
215 |
+
<Alert.Description class="text-red-600 text-sm dark:text-red-400">
|
216 |
{error}
|
217 |
</Alert.Description>
|
218 |
</Alert.Root>
|
|
|
220 |
|
221 |
<!-- USB Calibration Panel -->
|
222 |
{#if showUSBCalibration}
|
223 |
+
<Card.Root class="border-orange-300/30 bg-orange-100/20 dark:border-orange-500/30 dark:bg-orange-900/20">
|
224 |
<Card.Header>
|
225 |
<div class="flex justify-between items-center">
|
226 |
+
<Card.Title class="text-lg font-semibold text-orange-700 dark:text-orange-200">
|
227 |
Hardware Calibration Required
|
228 |
</Card.Title>
|
229 |
<button
|
230 |
onclick={onCalibrationCancel}
|
231 |
+
class="text-slate-600 hover:text-slate-900 dark:text-gray-400 dark:hover:text-white"
|
232 |
>
|
233 |
✕
|
234 |
</button>
|
235 |
</div>
|
236 |
</Card.Header>
|
237 |
<Card.Content class="space-y-4">
|
238 |
+
<Alert.Root class="border-orange-300/30 bg-orange-100/10 dark:border-orange-500/30 dark:bg-orange-500/10">
|
239 |
+
<span class="icon-[mdi--information] size-4 text-orange-500 dark:text-orange-400"></span>
|
240 |
+
<Alert.Description class="text-orange-700 text-sm dark:text-orange-200">
|
241 |
Before connecting to the physical robot, calibration is required to map the servo positions to software values. This ensures accurate control.
|
242 |
</Alert.Description>
|
243 |
</Alert.Root>
|
|
|
253 |
{:else}
|
254 |
|
255 |
<!-- Current Status Overview -->
|
256 |
+
<Card.Root class="border-green-300/30 bg-green-100/20 dark:border-green-500/30 dark:bg-green-900/20">
|
257 |
<Card.Content class="p-4">
|
258 |
<div class="flex items-center justify-between">
|
259 |
<div class="flex items-center gap-2">
|
260 |
+
<span class="icon-[mdi--connection] size-4 text-green-500 dark:text-green-400"></span>
|
261 |
+
<span class="text-sm font-medium text-green-700 dark:text-green-300">Current Input Source</span>
|
262 |
</div>
|
263 |
{#if robot.hasConsumer}
|
264 |
+
<Badge variant="default" class="bg-green-500 text-xs dark:bg-green-600">
|
265 |
{robot.consumer?.name || 'Connected'}
|
266 |
</Badge>
|
267 |
{:else}
|
268 |
+
<Badge variant="secondary" class="text-xs text-slate-600 dark:text-slate-400">No Input Connected</Badge>
|
269 |
{/if}
|
270 |
</div>
|
271 |
{#if robot.hasConsumer}
|
272 |
+
<div class="mt-2 text-xs text-green-600/70 dark:text-green-400/70">
|
273 |
Status: {robot.consumer?.status.isConnected ? 'Connected' : 'Disconnected'}
|
274 |
</div>
|
275 |
{/if}
|
|
|
277 |
</Card.Root>
|
278 |
|
279 |
<!-- Local Hardware Connection -->
|
280 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/5 dark:border-blue-500/30 dark:bg-blue-500/5">
|
281 |
<Card.Header>
|
282 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-700 dark:text-blue-200">
|
283 |
<span class="icon-[mdi--usb-port] size-4"></span>
|
284 |
Local Hardware (USB)
|
285 |
</Card.Title>
|
286 |
+
<Card.Description class="text-xs text-blue-600/70 dark:text-blue-300/70">
|
287 |
Read physical robot movements in real-time
|
288 |
</Card.Description>
|
289 |
</Card.Header>
|
290 |
<Card.Content class="space-y-3">
|
291 |
{#if robot.hasConsumer && robot.consumer?.name === 'USB Consumer'}
|
292 |
<!-- USB Connected State -->
|
293 |
+
<div class="rounded-lg border border-blue-300/30 bg-blue-100/20 p-3 dark:border-blue-500/30 dark:bg-blue-900/20">
|
294 |
<div class="flex items-center justify-between">
|
295 |
<div>
|
296 |
+
<p class="text-sm font-medium text-blue-700 dark:text-blue-300">Hardware Connected</p>
|
297 |
+
<p class="text-xs text-blue-600/70 dark:text-blue-400/70">Reading physical servo positions</p>
|
298 |
</div>
|
299 |
<Button
|
300 |
variant="destructive"
|
|
|
314 |
variant="secondary"
|
315 |
onclick={connectUSBInput}
|
316 |
disabled={isConnecting || robot.hasConsumer}
|
317 |
+
class="w-full bg-blue-500 text-sm text-white hover:bg-blue-600 disabled:opacity-50 dark:bg-blue-600 dark:hover:bg-blue-700"
|
318 |
>
|
319 |
<span class="icon-[mdi--usb] mr-2 size-4"></span>
|
320 |
{isConnecting ? 'Connecting...' : 'Connect to Hardware'}
|
321 |
</Button>
|
322 |
|
323 |
{#if robot.hasConsumer}
|
324 |
+
<p class="text-xs text-slate-600 dark:text-slate-500">
|
325 |
Disconnect current input to connect USB hardware
|
326 |
</p>
|
327 |
{/if}
|
|
|
330 |
</Card.Root>
|
331 |
|
332 |
<!-- Remote Collaboration -->
|
333 |
+
<Card.Root class="border-purple-300/30 bg-purple-100/5 dark:border-purple-500/30 dark:bg-purple-500/5">
|
334 |
<Card.Header>
|
335 |
<div class="flex items-center justify-between">
|
336 |
<div>
|
337 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-700 dark:text-purple-200">
|
338 |
<span class="icon-[mdi--cloud-sync] size-4"></span>
|
339 |
Remote Collaboration (Rooms)
|
340 |
</Card.Title>
|
341 |
+
<Card.Description class="text-xs text-purple-600/70 dark:text-purple-300/70">
|
342 |
Receive commands from AI systems, remote users, or other software
|
343 |
</Card.Description>
|
344 |
</div>
|
|
|
347 |
size="sm"
|
348 |
onclick={refreshRooms}
|
349 |
disabled={robotManager.roomsLoading || isConnecting}
|
350 |
+
class="h-7 px-2 text-xs text-purple-700 hover:text-purple-800 hover:bg-purple-200/20 dark:text-purple-300 dark:hover:text-purple-200 dark:hover:bg-purple-500/20"
|
351 |
>
|
352 |
{#if robotManager.roomsLoading}
|
353 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
|
|
362 |
<Card.Content class="space-y-4">
|
363 |
{#if robot.hasConsumer && robot.consumer?.name?.includes('Remote Consumer')}
|
364 |
<!-- Remote Connected State -->
|
365 |
+
<div class="rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20">
|
366 |
<div class="flex items-center justify-between">
|
367 |
<div>
|
368 |
+
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Room Connected</p>
|
369 |
+
<p class="text-xs text-purple-600/70 dark:text-purple-400/70">Receiving remote commands</p>
|
370 |
</div>
|
371 |
<Button
|
372 |
variant="destructive"
|
|
|
382 |
</div>
|
383 |
{:else}
|
384 |
<!-- Create New Room -->
|
385 |
+
<div class="rounded border-2 border-dashed border-green-400/50 bg-green-100/5 p-3 dark:border-green-500/50 dark:bg-green-500/5">
|
386 |
<div class="space-y-2">
|
387 |
<div class="flex items-center gap-2">
|
388 |
+
<span class="icon-[mdi--plus-circle] size-4 text-green-500 dark:text-green-400"></span>
|
389 |
+
<p class="text-sm font-medium text-green-700 dark:text-green-300">Create New Room</p>
|
390 |
</div>
|
391 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
392 |
Create a room where others can send commands to this robot
|
393 |
</p>
|
394 |
<input
|
395 |
bind:value={customRoomId}
|
396 |
placeholder={`Room ID (default: ${robot.id})`}
|
397 |
disabled={isConnecting || robot.hasConsumer}
|
398 |
+
class="w-full px-2 py-1 bg-slate-50 border border-slate-300 rounded text-xs text-slate-900 disabled:opacity-50 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100"
|
399 |
/>
|
400 |
<div class="flex gap-1">
|
401 |
<Button
|
|
|
403 |
size="sm"
|
404 |
onclick={createRoom}
|
405 |
disabled={isConnecting || robot.hasConsumer}
|
406 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
407 |
>
|
408 |
Create Only
|
409 |
</Button>
|
|
|
412 |
size="sm"
|
413 |
onclick={createRoomAndJoinAsInput}
|
414 |
disabled={isConnecting || robot.hasConsumer}
|
415 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
416 |
>
|
417 |
Create & Join as Input
|
418 |
</Button>
|
|
|
423 |
<!-- Existing Rooms -->
|
424 |
<div class="space-y-2">
|
425 |
<div class="flex items-center justify-between">
|
426 |
+
<span class="text-xs font-medium text-purple-700 dark:text-purple-300">Join Existing Room:</span>
|
427 |
+
<span class="text-xs text-slate-600 dark:text-slate-400">
|
428 |
{robotManager.rooms.length} room{robotManager.rooms.length !== 1 ? 's' : ''} available
|
429 |
</span>
|
430 |
</div>
|
431 |
|
432 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
433 |
{#if robotManager.rooms.length === 0}
|
434 |
+
<div class="text-center py-3 text-xs text-slate-600 dark:text-slate-400">
|
435 |
{robotManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
436 |
</div>
|
437 |
{:else}
|
438 |
{#each robotManager.rooms as room}
|
439 |
+
<div class="rounded border border-slate-300 bg-slate-50/50 p-2 dark:border-slate-600 dark:bg-slate-800/50">
|
440 |
<div class="flex items-start justify-between gap-3">
|
441 |
<div class="flex-1 min-w-0">
|
442 |
+
<p class="text-xs font-medium text-slate-800 truncate dark:text-slate-200">
|
443 |
{room.id}
|
444 |
</p>
|
445 |
+
<div class="flex gap-3 text-xs text-slate-600 dark:text-slate-400">
|
446 |
<span>{room.has_producer ? '📤 Has Output' : '📥 No Output'}</span>
|
447 |
<span>👥 {room.participants?.total || 0} users</span>
|
448 |
</div>
|
|
|
455 |
joinRoomAsInput();
|
456 |
}}
|
457 |
disabled={isConnecting || robot.hasConsumer}
|
458 |
+
class="h-6 px-2 text-xs bg-purple-500 hover:bg-purple-600 shrink-0 disabled:opacity-50 dark:bg-purple-600 dark:hover:bg-purple-700"
|
459 |
>
|
460 |
<span class="icon-[mdi--login] mr-1 size-3"></span>
|
461 |
Join as Input
|
|
|
468 |
</div>
|
469 |
|
470 |
{#if robot.hasConsumer}
|
471 |
+
<p class="text-xs text-slate-600 dark:text-slate-500">
|
472 |
Disconnect current input to join a room
|
473 |
</p>
|
474 |
{/if}
|
|
|
477 |
</Card.Root>
|
478 |
|
479 |
<!-- Help Information -->
|
480 |
+
<Alert.Root class="border-slate-300 bg-slate-100/30 dark:border-slate-700 dark:bg-slate-800/30">
|
481 |
+
<span class="icon-[mdi--help-circle] size-4 text-slate-600 dark:text-slate-400"></span>
|
482 |
+
<Alert.Title class="text-slate-700 dark:text-slate-300">Input Sources</Alert.Title>
|
483 |
+
<Alert.Description class="text-slate-600 text-xs dark:text-slate-400">
|
484 |
<strong>USB:</strong> Read physical movements • <strong>Remote:</strong> Receive network commands • Only one active at a time
|
485 |
</Alert.Description>
|
486 |
</Alert.Root>
|
src/lib/components/3d/elements/robot/modal/ManualControlSheet.svelte
CHANGED
@@ -19,16 +19,16 @@
|
|
19 |
<Sheet.Content
|
20 |
trapFocus={false}
|
21 |
side="right"
|
22 |
-
class="w-80 gap-0 border-l border-slate-
|
23 |
>
|
24 |
<!-- Header -->
|
25 |
-
<Sheet.Header class="border-b border-slate-
|
26 |
<div class="flex items-center justify-between">
|
27 |
<div class="flex items-center gap-3">
|
28 |
-
<span class="icon-[mdi--tune] size-6 text-purple-400"></span>
|
29 |
<div>
|
30 |
-
<Sheet.Title class="text-xl font-semibold text-slate-100">Manual Control</Sheet.Title>
|
31 |
-
<p class="mt-1 text-sm text-slate-400">Direct robot joint manipulation</p>
|
32 |
</div>
|
33 |
</div>
|
34 |
</div>
|
@@ -36,25 +36,25 @@
|
|
36 |
|
37 |
{#if robot}
|
38 |
<!-- Content -->
|
39 |
-
<div class="scrollbar-thin scrollbar-track-slate-
|
40 |
<div class="space-y-6 py-4">
|
41 |
<!-- Manual Joint Controls -->
|
42 |
{#if robot.isManualControlEnabled}
|
43 |
<div class="space-y-4">
|
44 |
<div class="mb-3 flex items-center gap-3">
|
45 |
-
<span class="icon-[lucide--rotate-3d] size-5 text-purple-400"></span>
|
46 |
-
<h3 class="text-lg font-medium text-slate-100">Joint Controls</h3>
|
47 |
-
<Badge variant="default" class="ml-auto bg-purple-
|
48 |
{robot.jointArray.length}
|
49 |
</Badge>
|
50 |
</div>
|
51 |
|
52 |
-
<p class="text-xs text-slate-400">
|
53 |
Each joint can be moved independently using sliders. Values are normalized percentages.
|
54 |
</p>
|
55 |
|
56 |
{#if robot.jointArray.length === 0}
|
57 |
-
<p class="py-4 text-center text-xs text-slate-
|
58 |
{:else}
|
59 |
<div class="space-y-3">
|
60 |
{#each robot.jointArray as joint (joint.name)}
|
@@ -62,15 +62,15 @@
|
|
62 |
{@const minValue = isGripper ? 0 : -100}
|
63 |
{@const maxValue = isGripper ? 100 : 100}
|
64 |
|
65 |
-
<div class="space-y-2 rounded-lg border border-slate-
|
66 |
<div class="flex items-center justify-between">
|
67 |
-
<span class="text-sm font-medium text-slate-200">{joint.name}</span>
|
68 |
<div class="flex items-center gap-2 text-xs">
|
69 |
-
<span class="font-mono text-purple-400">
|
70 |
{joint.value.toFixed(1)}{isGripper ? '%' : '%'}
|
71 |
</span>
|
72 |
{#if joint.limits}
|
73 |
-
<span class="font-mono text-slate-500 text-[10px]">
|
74 |
({joint.limits.lower.toFixed(1)}° to {joint.limits.upper.toFixed(1)}°)
|
75 |
</span>
|
76 |
{/if}
|
@@ -87,9 +87,9 @@
|
|
87 |
const val = parseFloat((e.target as HTMLInputElement).value);
|
88 |
robot.updateJoint(joint.name, val);
|
89 |
}}
|
90 |
-
class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-600"
|
91 |
/>
|
92 |
-
<div class="flex justify-between text-xs text-slate-500">
|
93 |
<span>{minValue}{isGripper ? '% (closed)' : '%'}</span>
|
94 |
<span>{maxValue}{isGripper ? '% (open)' : '%'}</span>
|
95 |
</div>
|
@@ -102,12 +102,12 @@
|
|
102 |
{:else}
|
103 |
<div class="space-y-4">
|
104 |
<div class="mb-3 flex items-center gap-3">
|
105 |
-
<h3 class="text-lg font-medium text-slate-100">Input Control Active</h3>
|
106 |
</div>
|
107 |
|
108 |
-
<Alert.Root class="border-purple-500/30 bg-purple-500/10">
|
109 |
-
<Alert.Title class="text-sm text-purple-200">Input Control Active</Alert.Title>
|
110 |
-
<Alert.Description class="text-xs text-purple-300">
|
111 |
Robot controlled by: <strong>{robot.consumer?.name || 'External Input'}</strong><br />
|
112 |
Disconnect input to enable manual control.
|
113 |
</Alert.Description>
|
|
|
19 |
<Sheet.Content
|
20 |
trapFocus={false}
|
21 |
side="right"
|
22 |
+
class="w-80 gap-0 border-l border-slate-300 bg-gradient-to-b from-slate-100 to-slate-200 p-0 text-slate-900 dark:border-slate-600 dark:bg-gradient-to-b dark:from-slate-700 dark:to-slate-800 dark:text-white sm:w-96"
|
23 |
>
|
24 |
<!-- Header -->
|
25 |
+
<Sheet.Header class="border-b border-slate-300 bg-slate-200/80 p-6 backdrop-blur-sm dark:border-slate-600 dark:bg-slate-700/80">
|
26 |
<div class="flex items-center justify-between">
|
27 |
<div class="flex items-center gap-3">
|
28 |
+
<span class="icon-[mdi--tune] size-6 text-purple-500 dark:text-purple-400"></span>
|
29 |
<div>
|
30 |
+
<Sheet.Title class="text-xl font-semibold text-slate-900 dark:text-slate-100">Manual Control</Sheet.Title>
|
31 |
+
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Direct robot joint manipulation</p>
|
32 |
</div>
|
33 |
</div>
|
34 |
</div>
|
|
|
36 |
|
37 |
{#if robot}
|
38 |
<!-- Content -->
|
39 |
+
<div class="scrollbar-thin scrollbar-track-slate-300 scrollbar-thumb-slate-500 flex-1 overflow-y-auto px-4 dark:scrollbar-track-slate-700 dark:scrollbar-thumb-slate-500">
|
40 |
<div class="space-y-6 py-4">
|
41 |
<!-- Manual Joint Controls -->
|
42 |
{#if robot.isManualControlEnabled}
|
43 |
<div class="space-y-4">
|
44 |
<div class="mb-3 flex items-center gap-3">
|
45 |
+
<span class="icon-[lucide--rotate-3d] size-5 text-purple-500 dark:text-purple-400"></span>
|
46 |
+
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100">Joint Controls</h3>
|
47 |
+
<Badge variant="default" class="ml-auto bg-purple-500 text-xs dark:bg-purple-600">
|
48 |
{robot.jointArray.length}
|
49 |
</Badge>
|
50 |
</div>
|
51 |
|
52 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
53 |
Each joint can be moved independently using sliders. Values are normalized percentages.
|
54 |
</p>
|
55 |
|
56 |
{#if robot.jointArray.length === 0}
|
57 |
+
<p class="py-4 text-center text-xs text-slate-600 italic dark:text-slate-500">No joints available</p>
|
58 |
{:else}
|
59 |
<div class="space-y-3">
|
60 |
{#each robot.jointArray as joint (joint.name)}
|
|
|
62 |
{@const minValue = isGripper ? 0 : -100}
|
63 |
{@const maxValue = isGripper ? 100 : 100}
|
64 |
|
65 |
+
<div class="space-y-2 rounded-lg border border-slate-300 bg-slate-100/50 p-3 dark:border-slate-600 dark:bg-slate-800/50">
|
66 |
<div class="flex items-center justify-between">
|
67 |
+
<span class="text-sm font-medium text-slate-800 dark:text-slate-200">{joint.name}</span>
|
68 |
<div class="flex items-center gap-2 text-xs">
|
69 |
+
<span class="font-mono text-purple-600 dark:text-purple-400">
|
70 |
{joint.value.toFixed(1)}{isGripper ? '%' : '%'}
|
71 |
</span>
|
72 |
{#if joint.limits}
|
73 |
+
<span class="font-mono text-slate-500 text-[10px] dark:text-slate-500">
|
74 |
({joint.limits.lower.toFixed(1)}° to {joint.limits.upper.toFixed(1)}°)
|
75 |
</span>
|
76 |
{/if}
|
|
|
87 |
const val = parseFloat((e.target as HTMLInputElement).value);
|
88 |
robot.updateJoint(joint.name, val);
|
89 |
}}
|
90 |
+
class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-300 dark:bg-slate-600"
|
91 |
/>
|
92 |
+
<div class="flex justify-between text-xs text-slate-600 dark:text-slate-500">
|
93 |
<span>{minValue}{isGripper ? '% (closed)' : '%'}</span>
|
94 |
<span>{maxValue}{isGripper ? '% (open)' : '%'}</span>
|
95 |
</div>
|
|
|
102 |
{:else}
|
103 |
<div class="space-y-4">
|
104 |
<div class="mb-3 flex items-center gap-3">
|
105 |
+
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100">Input Control Active</h3>
|
106 |
</div>
|
107 |
|
108 |
+
<Alert.Root class="border-purple-300/30 bg-purple-100/10 dark:border-purple-500/30 dark:bg-purple-500/10">
|
109 |
+
<Alert.Title class="text-sm text-purple-700 dark:text-purple-200">Input Control Active</Alert.Title>
|
110 |
+
<Alert.Description class="text-xs text-purple-700 dark:text-purple-300">
|
111 |
Robot controlled by: <strong>{robot.consumer?.name || 'External Input'}</strong><br />
|
112 |
Disconnect input to enable manual control.
|
113 |
</Alert.Description>
|
src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte
CHANGED
@@ -202,14 +202,14 @@
|
|
202 |
|
203 |
<Dialog.Root bind:open>
|
204 |
<Dialog.Content
|
205 |
-
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-600 bg-slate-900 text-slate-100"
|
206 |
>
|
207 |
<Dialog.Header class="pb-3">
|
208 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
209 |
-
<span class="icon-[mdi--devices] size-5 text-blue-400"></span>
|
210 |
Output Connection - Robot {robot.id}
|
211 |
</Dialog.Title>
|
212 |
-
<Dialog.Description class="text-sm text-slate-400">
|
213 |
Configure where this robot sends its movements. Multiple outputs can be active simultaneously.
|
214 |
</Dialog.Description>
|
215 |
</Dialog.Header>
|
@@ -218,10 +218,10 @@
|
|
218 |
<div class="space-y-4 pb-4">
|
219 |
<!-- Error display -->
|
220 |
{#if error}
|
221 |
-
<Alert.Root class="border-red-500/30 bg-red-900/20">
|
222 |
-
<span class="icon-[mdi--alert-circle] size-4 text-red-400"></span>
|
223 |
-
<Alert.Title class="text-red-300">Connection Error</Alert.Title>
|
224 |
-
<Alert.Description class="text-red-
|
225 |
{error}
|
226 |
</Alert.Description>
|
227 |
</Alert.Root>
|
@@ -229,24 +229,24 @@
|
|
229 |
|
230 |
<!-- USB Calibration Panel -->
|
231 |
{#if showUSBCalibration}
|
232 |
-
<Card.Root class="border-orange-500/30 bg-orange-900/20">
|
233 |
<Card.Header>
|
234 |
<div class="flex justify-between items-center">
|
235 |
-
<Card.Title class="text-lg font-semibold text-orange-200">
|
236 |
Hardware Calibration Required
|
237 |
</Card.Title>
|
238 |
<button
|
239 |
onclick={onCalibrationCancel}
|
240 |
-
class="text-gray-400 hover:text-white"
|
241 |
>
|
242 |
✕
|
243 |
</button>
|
244 |
</div>
|
245 |
</Card.Header>
|
246 |
<Card.Content class="space-y-4">
|
247 |
-
<Alert.Root class="border-orange-500/30 bg-orange-500/10">
|
248 |
-
<span class="icon-[mdi--information] size-4 text-orange-400"></span>
|
249 |
-
<Alert.Description class="text-orange-
|
250 |
Before connecting to the physical robot, calibration is required to map the servo positions to software values. This ensures accurate control.
|
251 |
</Alert.Description>
|
252 |
</Alert.Root>
|
@@ -262,14 +262,14 @@
|
|
262 |
{:else}
|
263 |
|
264 |
<!-- Current Status Overview -->
|
265 |
-
<Card.Root class="border-blue-500/30 bg-blue-900/20">
|
266 |
<Card.Content class="p-4">
|
267 |
<div class="flex items-center justify-between">
|
268 |
<div class="flex items-center gap-2">
|
269 |
-
<span class="icon-[mdi--broadcast] size-4 text-blue-400"></span>
|
270 |
-
<span class="text-sm font-medium text-blue-300">Active Outputs</span>
|
271 |
</div>
|
272 |
-
<Badge variant="default" class="bg-blue-
|
273 |
{outputDriverCount} Connected
|
274 |
</Badge>
|
275 |
</div>
|
@@ -277,13 +277,13 @@
|
|
277 |
</Card.Root>
|
278 |
|
279 |
<!-- Local Hardware Connection -->
|
280 |
-
<Card.Root class="border-green-500/30 bg-green-500/5">
|
281 |
<Card.Header>
|
282 |
-
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
283 |
<span class="icon-[mdi--usb-port] size-4"></span>
|
284 |
Local Hardware (USB)
|
285 |
</Card.Title>
|
286 |
-
<Card.Description class="text-xs text-green-300/70">
|
287 |
Send commands directly to physical robot hardware
|
288 |
</Card.Description>
|
289 |
</Card.Header>
|
@@ -292,7 +292,7 @@
|
|
292 |
variant="secondary"
|
293 |
onclick={connectUSBOutput}
|
294 |
disabled={isConnecting}
|
295 |
-
class="w-full bg-green-
|
296 |
>
|
297 |
<span class="icon-[mdi--usb] mr-2 size-4"></span>
|
298 |
{isConnecting ? 'Connecting...' : 'Add USB Output'}
|
@@ -301,15 +301,15 @@
|
|
301 |
</Card.Root>
|
302 |
|
303 |
<!-- Remote Collaboration -->
|
304 |
-
<Card.Root class="border-orange-500/30 bg-orange-500/5">
|
305 |
<Card.Header>
|
306 |
<div class="flex items-center justify-between">
|
307 |
<div>
|
308 |
-
<Card.Title class="flex items-center gap-2 text-base text-orange-200">
|
309 |
<span class="icon-[mdi--cloud-sync] size-4"></span>
|
310 |
Remote Collaboration (Rooms)
|
311 |
</Card.Title>
|
312 |
-
<Card.Description class="text-xs text-orange-300/70">
|
313 |
Broadcast robot movements to remote systems and AI
|
314 |
</Card.Description>
|
315 |
</div>
|
@@ -318,7 +318,7 @@
|
|
318 |
size="sm"
|
319 |
onclick={refreshRooms}
|
320 |
disabled={robotManager.roomsLoading || isConnecting}
|
321 |
-
class="h-7 px-2 text-xs text-orange-300 hover:text-orange-200 hover:bg-orange-500/20"
|
322 |
>
|
323 |
{#if robotManager.roomsLoading}
|
324 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
@@ -332,20 +332,20 @@
|
|
332 |
</Card.Header>
|
333 |
<Card.Content class="space-y-4">
|
334 |
<!-- Create New Room -->
|
335 |
-
<div class="rounded border-2 border-dashed border-green-
|
336 |
<div class="space-y-2">
|
337 |
<div class="flex items-center gap-2">
|
338 |
-
<span class="icon-[mdi--plus-circle] size-4 text-green-400"></span>
|
339 |
-
<p class="text-sm font-medium text-green-300">Create New Room</p>
|
340 |
</div>
|
341 |
-
<p class="text-xs text-green-400/70">
|
342 |
Create a room to broadcast this robot's movements
|
343 |
</p>
|
344 |
<input
|
345 |
bind:value={customRoomId}
|
346 |
placeholder={`Room ID (default: ${robot.id})`}
|
347 |
disabled={isConnecting}
|
348 |
-
class="w-full px-2 py-1 bg-slate-
|
349 |
/>
|
350 |
<div class="flex gap-1">
|
351 |
<Button
|
@@ -353,7 +353,7 @@
|
|
353 |
size="sm"
|
354 |
onclick={createRoom}
|
355 |
disabled={isConnecting}
|
356 |
-
class="h-6 px-2 text-xs bg-green-600 hover:bg-green-700"
|
357 |
>
|
358 |
Create Only
|
359 |
</Button>
|
@@ -362,7 +362,7 @@
|
|
362 |
size="sm"
|
363 |
onclick={createRoomAndJoinAsOutput}
|
364 |
disabled={isConnecting}
|
365 |
-
class="h-6 px-2 text-xs bg-green-600 hover:bg-green-700"
|
366 |
>
|
367 |
Create & Join as Output
|
368 |
</Button>
|
@@ -373,26 +373,26 @@
|
|
373 |
<!-- Existing Rooms -->
|
374 |
<div class="space-y-2">
|
375 |
<div class="flex items-center justify-between">
|
376 |
-
<span class="text-xs font-medium text-orange-300">Join Existing Room:</span>
|
377 |
-
<span class="text-xs text-slate-400">
|
378 |
{robotManager.rooms.length} room{robotManager.rooms.length !== 1 ? 's' : ''} available
|
379 |
</span>
|
380 |
</div>
|
381 |
|
382 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
383 |
{#if robotManager.rooms.length === 0}
|
384 |
-
<div class="text-center py-3 text-xs text-slate-400">
|
385 |
{robotManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
386 |
</div>
|
387 |
{:else}
|
388 |
{#each robotManager.rooms as room}
|
389 |
-
<div class="rounded border border-slate-
|
390 |
<div class="flex items-start justify-between gap-3">
|
391 |
<div class="flex-1 min-w-0">
|
392 |
-
<p class="text-xs font-medium text-slate-
|
393 |
{room.id}
|
394 |
</p>
|
395 |
-
<div class="flex gap-3 text-xs text-slate-400">
|
396 |
<span>{room.has_producer ? '🔴 Occupied' : '🟢 Available'}</span>
|
397 |
<span>👥 {room.participants?.total || 0} users</span>
|
398 |
</div>
|
@@ -406,7 +406,7 @@
|
|
406 |
joinRoomAsOutput();
|
407 |
}}
|
408 |
disabled={isConnecting}
|
409 |
-
class="h-6 px-2 text-xs bg-orange-
|
410 |
>
|
411 |
<span class="icon-[mdi--login] mr-1 size-3"></span>
|
412 |
Join as Output
|
@@ -432,9 +432,9 @@
|
|
432 |
|
433 |
<!-- Connected Outputs -->
|
434 |
{#if producers.length > 0}
|
435 |
-
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
436 |
<Card.Header>
|
437 |
-
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
438 |
<span class="icon-[mdi--connection] size-4"></span>
|
439 |
Connected Outputs
|
440 |
</Card.Title>
|
@@ -442,12 +442,12 @@
|
|
442 |
<Card.Content>
|
443 |
<div class="max-h-32 space-y-2 overflow-y-auto">
|
444 |
{#each producers as producer}
|
445 |
-
<div class="flex items-center justify-between rounded-md bg-slate-
|
446 |
<div class="flex items-center gap-2">
|
447 |
<span
|
448 |
-
class="size-2 rounded-full {producer.status.isConnected ? 'bg-green-400' : 'bg-red-400'}"
|
449 |
></span>
|
450 |
-
<span class="text-sm text-slate-300">{producer.name}</span>
|
451 |
<Badge variant="secondary" class="text-xs">{producer.id.slice(0, 12)}</Badge>
|
452 |
</div>
|
453 |
<Button
|
@@ -467,10 +467,10 @@
|
|
467 |
{/if}
|
468 |
|
469 |
<!-- Help Information -->
|
470 |
-
<Alert.Root class="border-slate-700 bg-slate-800/30">
|
471 |
-
<span class="icon-[mdi--help-circle] size-4 text-slate-400"></span>
|
472 |
-
<Alert.Title class="text-slate-300">Output Sources</Alert.Title>
|
473 |
-
<Alert.Description class="text-slate-
|
474 |
<strong>USB:</strong> Control physical hardware • <strong>Remote:</strong> Broadcast to network • Multiple outputs can be active
|
475 |
</Alert.Description>
|
476 |
</Alert.Root>
|
|
|
202 |
|
203 |
<Dialog.Root bind:open>
|
204 |
<Dialog.Content
|
205 |
+
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
206 |
>
|
207 |
<Dialog.Header class="pb-3">
|
208 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
209 |
+
<span class="icon-[mdi--devices] size-5 text-blue-500 dark:text-blue-400"></span>
|
210 |
Output Connection - Robot {robot.id}
|
211 |
</Dialog.Title>
|
212 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
213 |
Configure where this robot sends its movements. Multiple outputs can be active simultaneously.
|
214 |
</Dialog.Description>
|
215 |
</Dialog.Header>
|
|
|
218 |
<div class="space-y-4 pb-4">
|
219 |
<!-- Error display -->
|
220 |
{#if error}
|
221 |
+
<Alert.Root class="border-red-300/30 bg-red-100/20 dark:border-red-500/30 dark:bg-red-900/20">
|
222 |
+
<span class="icon-[mdi--alert-circle] size-4 text-red-500 dark:text-red-400"></span>
|
223 |
+
<Alert.Title class="text-red-700 dark:text-red-300">Connection Error</Alert.Title>
|
224 |
+
<Alert.Description class="text-red-600 text-sm dark:text-red-400">
|
225 |
{error}
|
226 |
</Alert.Description>
|
227 |
</Alert.Root>
|
|
|
229 |
|
230 |
<!-- USB Calibration Panel -->
|
231 |
{#if showUSBCalibration}
|
232 |
+
<Card.Root class="border-orange-300/30 bg-orange-100/20 dark:border-orange-500/30 dark:bg-orange-900/20">
|
233 |
<Card.Header>
|
234 |
<div class="flex justify-between items-center">
|
235 |
+
<Card.Title class="text-lg font-semibold text-orange-700 dark:text-orange-200">
|
236 |
Hardware Calibration Required
|
237 |
</Card.Title>
|
238 |
<button
|
239 |
onclick={onCalibrationCancel}
|
240 |
+
class="text-slate-600 hover:text-slate-900 dark:text-gray-400 dark:hover:text-white"
|
241 |
>
|
242 |
✕
|
243 |
</button>
|
244 |
</div>
|
245 |
</Card.Header>
|
246 |
<Card.Content class="space-y-4">
|
247 |
+
<Alert.Root class="border-orange-300/30 bg-orange-100/10 dark:border-orange-500/30 dark:bg-orange-500/10">
|
248 |
+
<span class="icon-[mdi--information] size-4 text-orange-500 dark:text-orange-400"></span>
|
249 |
+
<Alert.Description class="text-orange-700 text-sm dark:text-orange-200">
|
250 |
Before connecting to the physical robot, calibration is required to map the servo positions to software values. This ensures accurate control.
|
251 |
</Alert.Description>
|
252 |
</Alert.Root>
|
|
|
262 |
{:else}
|
263 |
|
264 |
<!-- Current Status Overview -->
|
265 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/20 dark:border-blue-500/30 dark:bg-blue-900/20">
|
266 |
<Card.Content class="p-4">
|
267 |
<div class="flex items-center justify-between">
|
268 |
<div class="flex items-center gap-2">
|
269 |
+
<span class="icon-[mdi--broadcast] size-4 text-blue-500 dark:text-blue-400"></span>
|
270 |
+
<span class="text-sm font-medium text-blue-700 dark:text-blue-300">Active Outputs</span>
|
271 |
</div>
|
272 |
+
<Badge variant="default" class="bg-blue-500 text-xs dark:bg-blue-600">
|
273 |
{outputDriverCount} Connected
|
274 |
</Badge>
|
275 |
</div>
|
|
|
277 |
</Card.Root>
|
278 |
|
279 |
<!-- Local Hardware Connection -->
|
280 |
+
<Card.Root class="border-green-300/30 bg-green-100/5 dark:border-green-500/30 dark:bg-green-500/5">
|
281 |
<Card.Header>
|
282 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-700 dark:text-green-200">
|
283 |
<span class="icon-[mdi--usb-port] size-4"></span>
|
284 |
Local Hardware (USB)
|
285 |
</Card.Title>
|
286 |
+
<Card.Description class="text-xs text-green-600/70 dark:text-green-300/70">
|
287 |
Send commands directly to physical robot hardware
|
288 |
</Card.Description>
|
289 |
</Card.Header>
|
|
|
292 |
variant="secondary"
|
293 |
onclick={connectUSBOutput}
|
294 |
disabled={isConnecting}
|
295 |
+
class="w-full bg-green-500 text-sm text-white hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
|
296 |
>
|
297 |
<span class="icon-[mdi--usb] mr-2 size-4"></span>
|
298 |
{isConnecting ? 'Connecting...' : 'Add USB Output'}
|
|
|
301 |
</Card.Root>
|
302 |
|
303 |
<!-- Remote Collaboration -->
|
304 |
+
<Card.Root class="border-orange-300/30 bg-orange-100/5 dark:border-orange-500/30 dark:bg-orange-500/5">
|
305 |
<Card.Header>
|
306 |
<div class="flex items-center justify-between">
|
307 |
<div>
|
308 |
+
<Card.Title class="flex items-center gap-2 text-base text-orange-700 dark:text-orange-200">
|
309 |
<span class="icon-[mdi--cloud-sync] size-4"></span>
|
310 |
Remote Collaboration (Rooms)
|
311 |
</Card.Title>
|
312 |
+
<Card.Description class="text-xs text-orange-600/70 dark:text-orange-300/70">
|
313 |
Broadcast robot movements to remote systems and AI
|
314 |
</Card.Description>
|
315 |
</div>
|
|
|
318 |
size="sm"
|
319 |
onclick={refreshRooms}
|
320 |
disabled={robotManager.roomsLoading || isConnecting}
|
321 |
+
class="h-7 px-2 text-xs text-orange-700 hover:text-orange-800 hover:bg-orange-200/20 dark:text-orange-300 dark:hover:text-orange-200 dark:hover:bg-orange-500/20"
|
322 |
>
|
323 |
{#if robotManager.roomsLoading}
|
324 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
|
|
332 |
</Card.Header>
|
333 |
<Card.Content class="space-y-4">
|
334 |
<!-- Create New Room -->
|
335 |
+
<div class="rounded border-2 border-dashed border-green-400/50 bg-green-100/5 p-3 dark:border-green-500/50 dark:bg-green-500/5">
|
336 |
<div class="space-y-2">
|
337 |
<div class="flex items-center gap-2">
|
338 |
+
<span class="icon-[mdi--plus-circle] size-4 text-green-500 dark:text-green-400"></span>
|
339 |
+
<p class="text-sm font-medium text-green-700 dark:text-green-300">Create New Room</p>
|
340 |
</div>
|
341 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
342 |
Create a room to broadcast this robot's movements
|
343 |
</p>
|
344 |
<input
|
345 |
bind:value={customRoomId}
|
346 |
placeholder={`Room ID (default: ${robot.id})`}
|
347 |
disabled={isConnecting}
|
348 |
+
class="w-full px-2 py-1 bg-slate-50 border border-slate-300 rounded text-xs text-slate-900 disabled:opacity-50 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100"
|
349 |
/>
|
350 |
<div class="flex gap-1">
|
351 |
<Button
|
|
|
353 |
size="sm"
|
354 |
onclick={createRoom}
|
355 |
disabled={isConnecting}
|
356 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
|
357 |
>
|
358 |
Create Only
|
359 |
</Button>
|
|
|
362 |
size="sm"
|
363 |
onclick={createRoomAndJoinAsOutput}
|
364 |
disabled={isConnecting}
|
365 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700"
|
366 |
>
|
367 |
Create & Join as Output
|
368 |
</Button>
|
|
|
373 |
<!-- Existing Rooms -->
|
374 |
<div class="space-y-2">
|
375 |
<div class="flex items-center justify-between">
|
376 |
+
<span class="text-xs font-medium text-orange-700 dark:text-orange-300">Join Existing Room:</span>
|
377 |
+
<span class="text-xs text-slate-600 dark:text-slate-400">
|
378 |
{robotManager.rooms.length} room{robotManager.rooms.length !== 1 ? 's' : ''} available
|
379 |
</span>
|
380 |
</div>
|
381 |
|
382 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
383 |
{#if robotManager.rooms.length === 0}
|
384 |
+
<div class="text-center py-3 text-xs text-slate-600 dark:text-slate-400">
|
385 |
{robotManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
386 |
</div>
|
387 |
{:else}
|
388 |
{#each robotManager.rooms as room}
|
389 |
+
<div class="rounded border border-slate-300 bg-slate-50/50 p-2 dark:border-slate-600 dark:bg-slate-800/50">
|
390 |
<div class="flex items-start justify-between gap-3">
|
391 |
<div class="flex-1 min-w-0">
|
392 |
+
<p class="text-xs font-medium text-slate-800 truncate dark:text-slate-200">
|
393 |
{room.id}
|
394 |
</p>
|
395 |
+
<div class="flex gap-3 text-xs text-slate-600 dark:text-slate-400">
|
396 |
<span>{room.has_producer ? '🔴 Occupied' : '🟢 Available'}</span>
|
397 |
<span>👥 {room.participants?.total || 0} users</span>
|
398 |
</div>
|
|
|
406 |
joinRoomAsOutput();
|
407 |
}}
|
408 |
disabled={isConnecting}
|
409 |
+
class="h-6 px-2 text-xs bg-orange-500 hover:bg-orange-600 shrink-0 dark:bg-orange-600 dark:hover:bg-orange-700"
|
410 |
>
|
411 |
<span class="icon-[mdi--login] mr-1 size-3"></span>
|
412 |
Join as Output
|
|
|
432 |
|
433 |
<!-- Connected Outputs -->
|
434 |
{#if producers.length > 0}
|
435 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/5 dark:border-blue-500/30 dark:bg-blue-500/5">
|
436 |
<Card.Header>
|
437 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-700 dark:text-blue-200">
|
438 |
<span class="icon-[mdi--connection] size-4"></span>
|
439 |
Connected Outputs
|
440 |
</Card.Title>
|
|
|
442 |
<Card.Content>
|
443 |
<div class="max-h-32 space-y-2 overflow-y-auto">
|
444 |
{#each producers as producer}
|
445 |
+
<div class="flex items-center justify-between rounded-md bg-slate-100/50 p-2 dark:bg-slate-700/50">
|
446 |
<div class="flex items-center gap-2">
|
447 |
<span
|
448 |
+
class="size-2 rounded-full {producer.status.isConnected ? 'bg-green-500 dark:bg-green-400' : 'bg-red-500 dark:bg-red-400'}"
|
449 |
></span>
|
450 |
+
<span class="text-sm text-slate-700 dark:text-slate-300">{producer.name}</span>
|
451 |
<Badge variant="secondary" class="text-xs">{producer.id.slice(0, 12)}</Badge>
|
452 |
</div>
|
453 |
<Button
|
|
|
467 |
{/if}
|
468 |
|
469 |
<!-- Help Information -->
|
470 |
+
<Alert.Root class="border-slate-300 bg-slate-100/30 dark:border-slate-700 dark:bg-slate-800/30">
|
471 |
+
<span class="icon-[mdi--help-circle] size-4 text-slate-600 dark:text-slate-400"></span>
|
472 |
+
<Alert.Title class="text-slate-700 dark:text-slate-300">Output Sources</Alert.Title>
|
473 |
+
<Alert.Description class="text-slate-600 text-xs dark:text-slate-400">
|
474 |
<strong>USB:</strong> Control physical hardware • <strong>Remote:</strong> Broadcast to network • Multiple outputs can be active
|
475 |
</Alert.Description>
|
476 |
</Alert.Root>
|
src/lib/components/3d/elements/video/modal/VideoInputConnectionModal.svelte
CHANGED
@@ -166,14 +166,14 @@
|
|
166 |
|
167 |
<Dialog.Root bind:open>
|
168 |
<Dialog.Content
|
169 |
-
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-600 bg-slate-900 text-slate-100"
|
170 |
>
|
171 |
<Dialog.Header class="pb-3">
|
172 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
173 |
-
<span class="icon-[mdi--video-input-component] size-5 text-green-400"></span>
|
174 |
Video Input - {video?.name || 'No Video Selected'}
|
175 |
</Dialog.Title>
|
176 |
-
<Dialog.Description class="text-sm text-slate-400">
|
177 |
Configure video input source: local camera for recording or remote streams from rooms
|
178 |
</Dialog.Description>
|
179 |
</Dialog.Header>
|
@@ -182,32 +182,32 @@
|
|
182 |
<div class="space-y-4 pb-4">
|
183 |
<!-- Error display -->
|
184 |
{#if error}
|
185 |
-
<Alert.Root class="border-red-500/30 bg-red-900/20">
|
186 |
-
<span class="icon-[mdi--alert-circle] size-4 text-red-400"></span>
|
187 |
-
<Alert.Title class="text-red-300">Connection Error</Alert.Title>
|
188 |
-
<Alert.Description class="text-red-
|
189 |
{error}
|
190 |
</Alert.Description>
|
191 |
</Alert.Root>
|
192 |
{/if}
|
193 |
<!-- Current Status Overview -->
|
194 |
-
<Card.Root class="border-green-500/30 bg-green-900/20">
|
195 |
<Card.Content class="p-4">
|
196 |
<div class="flex items-center justify-between">
|
197 |
<div class="flex items-center gap-2">
|
198 |
-
<span class="icon-[mdi--video-input-component] size-4 text-green-400"></span>
|
199 |
-
<span class="text-sm font-medium text-green-300">Current Video Input</span>
|
200 |
</div>
|
201 |
{#if video?.hasInput}
|
202 |
-
<Badge variant="default" class="bg-green-
|
203 |
{video.input.type === 'local-camera' ? 'Local Camera' : 'Remote Stream'}
|
204 |
</Badge>
|
205 |
{:else}
|
206 |
-
<Badge variant="secondary" class="text-xs text-slate-400">No Input Connected</Badge>
|
207 |
{/if}
|
208 |
</div>
|
209 |
{#if video?.hasInput}
|
210 |
-
<div class="mt-2 text-xs text-green-400/70">
|
211 |
{#if video.input.roomId}
|
212 |
Room: {video.input.roomId}
|
213 |
{:else}
|
@@ -220,30 +220,30 @@
|
|
220 |
|
221 |
<!-- Current Input Details -->
|
222 |
{#if video?.hasInput}
|
223 |
-
<Card.Root class="border-green-500/30 bg-green-500/5">
|
224 |
<Card.Header>
|
225 |
-
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
226 |
<span class="icon-[mdi--video] size-4"></span>
|
227 |
Current Input
|
228 |
</Card.Title>
|
229 |
</Card.Header>
|
230 |
<Card.Content>
|
231 |
-
<div class="rounded-lg border border-green-
|
232 |
<div class="flex items-center justify-between">
|
233 |
<div>
|
234 |
-
<p class="text-sm font-medium text-green-300">
|
235 |
{video.input.type === 'local-camera' ? 'Local Camera' : 'Remote Stream'}
|
236 |
</p>
|
237 |
{#if video.input.roomId}
|
238 |
-
<p class="text-xs text-green-400/70">
|
239 |
Room: {video.input.roomId}
|
240 |
</p>
|
241 |
{/if}
|
242 |
{#if video.input.stream}
|
243 |
-
<p class="text-xs text-green-400/70">
|
244 |
Video: {video.input.stream.getVideoTracks().length} tracks
|
245 |
</p>
|
246 |
-
<p class="text-xs text-green-400/70">
|
247 |
Audio: {video.input.stream.getAudioTracks().length} tracks
|
248 |
</p>
|
249 |
{/if}
|
@@ -264,24 +264,24 @@
|
|
264 |
{/if}
|
265 |
|
266 |
<!-- Local Camera -->
|
267 |
-
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
268 |
<Card.Header>
|
269 |
-
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
270 |
<span class="icon-[mdi--camera] size-4"></span>
|
271 |
Local Camera
|
272 |
</Card.Title>
|
273 |
-
<Card.Description class="text-xs text-blue-300/70">
|
274 |
Use your device camera for direct video capture and recording
|
275 |
</Card.Description>
|
276 |
</Card.Header>
|
277 |
<Card.Content class="space-y-3">
|
278 |
{#if video?.hasInput && video.input.type === 'local-camera'}
|
279 |
<!-- Camera Connected State -->
|
280 |
-
<div class="rounded-lg border border-blue-
|
281 |
<div class="flex items-center justify-between">
|
282 |
<div>
|
283 |
-
<p class="text-sm font-medium text-blue-300">Camera Connected</p>
|
284 |
-
<p class="text-xs text-blue-400/70">Local device camera active</p>
|
285 |
</div>
|
286 |
<Button
|
287 |
variant="destructive"
|
@@ -301,14 +301,14 @@
|
|
301 |
variant="secondary"
|
302 |
onclick={handleConnectCamera}
|
303 |
disabled={isConnecting || video?.hasInput}
|
304 |
-
class="w-full bg-blue-
|
305 |
>
|
306 |
<span class="icon-[mdi--camera] mr-2 size-4"></span>
|
307 |
{isConnecting ? 'Connecting...' : 'Connect to Camera'}
|
308 |
</Button>
|
309 |
|
310 |
{#if video?.hasInput}
|
311 |
-
<p class="text-xs text-slate-500">
|
312 |
Disconnect current input to connect camera
|
313 |
</p>
|
314 |
{/if}
|
@@ -317,15 +317,15 @@
|
|
317 |
</Card.Root>
|
318 |
|
319 |
<!-- Remote Collaboration -->
|
320 |
-
<Card.Root class="border-purple-500/30 bg-purple-500/5">
|
321 |
<Card.Header>
|
322 |
<div class="flex items-center justify-between">
|
323 |
<div>
|
324 |
-
<Card.Title class="flex items-center gap-2 text-base text-purple-200">
|
325 |
<span class="icon-[mdi--cloud-download] size-4"></span>
|
326 |
Remote Collaboration (Rooms)
|
327 |
</Card.Title>
|
328 |
-
<Card.Description class="text-xs text-purple-300/70">
|
329 |
Receive video streams from remote cameras or AI systems
|
330 |
</Card.Description>
|
331 |
</div>
|
@@ -334,7 +334,7 @@
|
|
334 |
size="sm"
|
335 |
onclick={refreshRooms}
|
336 |
disabled={videoManager.roomsLoading || isConnecting}
|
337 |
-
class="h-7 px-2 text-xs text-purple-300 hover:text-purple-200 hover:bg-purple-500/20"
|
338 |
>
|
339 |
{#if videoManager.roomsLoading}
|
340 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
@@ -349,11 +349,11 @@
|
|
349 |
<Card.Content class="space-y-4">
|
350 |
{#if video?.hasInput && video.input.type !== 'local-camera'}
|
351 |
<!-- Remote Connected State -->
|
352 |
-
<div class="rounded-lg border border-purple-
|
353 |
<div class="flex items-center justify-between">
|
354 |
<div>
|
355 |
-
<p class="text-sm font-medium text-purple-300">Room Connected</p>
|
356 |
-
<p class="text-xs text-purple-400/70">Receiving remote video stream</p>
|
357 |
</div>
|
358 |
<Button
|
359 |
variant="destructive"
|
@@ -369,20 +369,20 @@
|
|
369 |
</div>
|
370 |
{:else}
|
371 |
<!-- Create New Room -->
|
372 |
-
<div class="rounded border-2 border-dashed border-green-
|
373 |
<div class="space-y-2">
|
374 |
<div class="flex items-center gap-2">
|
375 |
-
<span class="icon-[mdi--plus-circle] size-4 text-green-400"></span>
|
376 |
-
<p class="text-sm font-medium text-green-300">Create New Room</p>
|
377 |
</div>
|
378 |
-
<p class="text-xs text-green-400/70">
|
379 |
Create a room to receive video from others
|
380 |
</p>
|
381 |
<input
|
382 |
bind:value={customRoomId}
|
383 |
placeholder={`Room ID (default: ${video.id})`}
|
384 |
disabled={isConnecting || video?.hasInput}
|
385 |
-
class="w-full px-2 py-1 bg-slate-
|
386 |
/>
|
387 |
<div class="flex gap-1">
|
388 |
<Button
|
@@ -390,7 +390,7 @@
|
|
390 |
size="sm"
|
391 |
onclick={createRoom}
|
392 |
disabled={isConnecting || video?.hasInput}
|
393 |
-
class="h-6 px-2 text-xs bg-green-
|
394 |
>
|
395 |
Create Only
|
396 |
</Button>
|
@@ -399,7 +399,7 @@
|
|
399 |
size="sm"
|
400 |
onclick={createRoomAndConnect}
|
401 |
disabled={isConnecting || video?.hasInput}
|
402 |
-
class="h-6 px-2 text-xs bg-green-
|
403 |
>
|
404 |
Create & Connect
|
405 |
</Button>
|
@@ -410,26 +410,26 @@
|
|
410 |
<!-- Existing Rooms -->
|
411 |
<div class="space-y-2">
|
412 |
<div class="flex items-center justify-between">
|
413 |
-
<span class="text-xs font-medium text-purple-300">Join Existing Room:</span>
|
414 |
-
<span class="text-xs text-slate-400">
|
415 |
{videoManager.rooms.length} room{videoManager.rooms.length !== 1 ? 's' : ''} available
|
416 |
</span>
|
417 |
</div>
|
418 |
|
419 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
420 |
{#if videoManager.rooms.length === 0}
|
421 |
-
<div class="text-center py-3 text-xs text-slate-400">
|
422 |
{videoManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
423 |
</div>
|
424 |
{:else}
|
425 |
{#each videoManager.rooms as room}
|
426 |
-
<div class="rounded border border-slate-
|
427 |
<div class="flex items-start justify-between gap-3">
|
428 |
<div class="flex-1 min-w-0">
|
429 |
-
<p class="text-xs font-medium text-slate-
|
430 |
{room.id}
|
431 |
</p>
|
432 |
-
<div class="flex gap-3 text-xs text-slate-400">
|
433 |
<span>{room.participants?.producer ? '📹 Has Output' : '📭 No Output'}</span>
|
434 |
<span>👥 {room.participants?.consumers?.length || 0} inputs</span>
|
435 |
</div>
|
@@ -440,7 +440,7 @@
|
|
440 |
size="sm"
|
441 |
onclick={() => handleConnectToRoom(room.id)}
|
442 |
disabled={isConnecting || video?.hasInput}
|
443 |
-
class="h-6 px-2 text-xs bg-purple-
|
444 |
>
|
445 |
<span class="icon-[mdi--download] mr-1 size-3"></span>
|
446 |
Join as Input
|
@@ -463,7 +463,7 @@
|
|
463 |
</div>
|
464 |
|
465 |
{#if video?.hasInput}
|
466 |
-
<p class="text-xs text-slate-500">
|
467 |
Disconnect current input to join a room
|
468 |
</p>
|
469 |
{/if}
|
@@ -472,10 +472,10 @@
|
|
472 |
</Card.Root>
|
473 |
|
474 |
<!-- Help Information -->
|
475 |
-
<Alert.Root class="border-slate-700 bg-slate-800/30">
|
476 |
-
<span class="icon-[mdi--help-circle] size-4 text-slate-400"></span>
|
477 |
-
<Alert.Title class="text-slate-300">Video Input Sources</Alert.Title>
|
478 |
-
<Alert.Description class="text-slate-
|
479 |
<strong>Camera:</strong> Local device camera • <strong>Remote:</strong> Video streams from rooms • Only one active at a time
|
480 |
</Alert.Description>
|
481 |
</Alert.Root>
|
|
|
166 |
|
167 |
<Dialog.Root bind:open>
|
168 |
<Dialog.Content
|
169 |
+
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
170 |
>
|
171 |
<Dialog.Header class="pb-3">
|
172 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
173 |
+
<span class="icon-[mdi--video-input-component] size-5 text-green-500 dark:text-green-400"></span>
|
174 |
Video Input - {video?.name || 'No Video Selected'}
|
175 |
</Dialog.Title>
|
176 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
177 |
Configure video input source: local camera for recording or remote streams from rooms
|
178 |
</Dialog.Description>
|
179 |
</Dialog.Header>
|
|
|
182 |
<div class="space-y-4 pb-4">
|
183 |
<!-- Error display -->
|
184 |
{#if error}
|
185 |
+
<Alert.Root class="border-red-300/30 bg-red-100/20 dark:border-red-500/30 dark:bg-red-900/20">
|
186 |
+
<span class="icon-[mdi--alert-circle] size-4 text-red-500 dark:text-red-400"></span>
|
187 |
+
<Alert.Title class="text-red-700 dark:text-red-300">Connection Error</Alert.Title>
|
188 |
+
<Alert.Description class="text-red-600 text-sm dark:text-red-400">
|
189 |
{error}
|
190 |
</Alert.Description>
|
191 |
</Alert.Root>
|
192 |
{/if}
|
193 |
<!-- Current Status Overview -->
|
194 |
+
<Card.Root class="border-green-300/30 bg-green-100/20 dark:border-green-500/30 dark:bg-green-900/20">
|
195 |
<Card.Content class="p-4">
|
196 |
<div class="flex items-center justify-between">
|
197 |
<div class="flex items-center gap-2">
|
198 |
+
<span class="icon-[mdi--video-input-component] size-4 text-green-500 dark:text-green-400"></span>
|
199 |
+
<span class="text-sm font-medium text-green-700 dark:text-green-300">Current Video Input</span>
|
200 |
</div>
|
201 |
{#if video?.hasInput}
|
202 |
+
<Badge variant="default" class="bg-green-500 text-xs dark:bg-green-600">
|
203 |
{video.input.type === 'local-camera' ? 'Local Camera' : 'Remote Stream'}
|
204 |
</Badge>
|
205 |
{:else}
|
206 |
+
<Badge variant="secondary" class="text-xs text-slate-600 dark:text-slate-400">No Input Connected</Badge>
|
207 |
{/if}
|
208 |
</div>
|
209 |
{#if video?.hasInput}
|
210 |
+
<div class="mt-2 text-xs text-green-600/70 dark:text-green-400/70">
|
211 |
{#if video.input.roomId}
|
212 |
Room: {video.input.roomId}
|
213 |
{:else}
|
|
|
220 |
|
221 |
<!-- Current Input Details -->
|
222 |
{#if video?.hasInput}
|
223 |
+
<Card.Root class="border-green-300/30 bg-green-100/5 dark:border-green-500/30 dark:bg-green-500/5">
|
224 |
<Card.Header>
|
225 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-700 dark:text-green-200">
|
226 |
<span class="icon-[mdi--video] size-4"></span>
|
227 |
Current Input
|
228 |
</Card.Title>
|
229 |
</Card.Header>
|
230 |
<Card.Content>
|
231 |
+
<div class="rounded-lg border border-green-300/30 bg-green-100/20 p-3 dark:border-green-500/30 dark:bg-green-900/20">
|
232 |
<div class="flex items-center justify-between">
|
233 |
<div>
|
234 |
+
<p class="text-sm font-medium text-green-700 dark:text-green-300">
|
235 |
{video.input.type === 'local-camera' ? 'Local Camera' : 'Remote Stream'}
|
236 |
</p>
|
237 |
{#if video.input.roomId}
|
238 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
239 |
Room: {video.input.roomId}
|
240 |
</p>
|
241 |
{/if}
|
242 |
{#if video.input.stream}
|
243 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
244 |
Video: {video.input.stream.getVideoTracks().length} tracks
|
245 |
</p>
|
246 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
247 |
Audio: {video.input.stream.getAudioTracks().length} tracks
|
248 |
</p>
|
249 |
{/if}
|
|
|
264 |
{/if}
|
265 |
|
266 |
<!-- Local Camera -->
|
267 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/5 dark:border-blue-500/30 dark:bg-blue-500/5">
|
268 |
<Card.Header>
|
269 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-700 dark:text-blue-200">
|
270 |
<span class="icon-[mdi--camera] size-4"></span>
|
271 |
Local Camera
|
272 |
</Card.Title>
|
273 |
+
<Card.Description class="text-xs text-blue-600/70 dark:text-blue-300/70">
|
274 |
Use your device camera for direct video capture and recording
|
275 |
</Card.Description>
|
276 |
</Card.Header>
|
277 |
<Card.Content class="space-y-3">
|
278 |
{#if video?.hasInput && video.input.type === 'local-camera'}
|
279 |
<!-- Camera Connected State -->
|
280 |
+
<div class="rounded-lg border border-blue-300/30 bg-blue-100/20 p-3 dark:border-blue-500/30 dark:bg-blue-900/20">
|
281 |
<div class="flex items-center justify-between">
|
282 |
<div>
|
283 |
+
<p class="text-sm font-medium text-blue-700 dark:text-blue-300">Camera Connected</p>
|
284 |
+
<p class="text-xs text-blue-600/70 dark:text-blue-400/70">Local device camera active</p>
|
285 |
</div>
|
286 |
<Button
|
287 |
variant="destructive"
|
|
|
301 |
variant="secondary"
|
302 |
onclick={handleConnectCamera}
|
303 |
disabled={isConnecting || video?.hasInput}
|
304 |
+
class="w-full bg-blue-500 text-sm text-white hover:bg-blue-600 disabled:opacity-50 dark:bg-blue-600 dark:hover:bg-blue-700"
|
305 |
>
|
306 |
<span class="icon-[mdi--camera] mr-2 size-4"></span>
|
307 |
{isConnecting ? 'Connecting...' : 'Connect to Camera'}
|
308 |
</Button>
|
309 |
|
310 |
{#if video?.hasInput}
|
311 |
+
<p class="text-xs text-slate-600 dark:text-slate-500">
|
312 |
Disconnect current input to connect camera
|
313 |
</p>
|
314 |
{/if}
|
|
|
317 |
</Card.Root>
|
318 |
|
319 |
<!-- Remote Collaboration -->
|
320 |
+
<Card.Root class="border-purple-300/30 bg-purple-100/5 dark:border-purple-500/30 dark:bg-purple-500/5">
|
321 |
<Card.Header>
|
322 |
<div class="flex items-center justify-between">
|
323 |
<div>
|
324 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-700 dark:text-purple-200">
|
325 |
<span class="icon-[mdi--cloud-download] size-4"></span>
|
326 |
Remote Collaboration (Rooms)
|
327 |
</Card.Title>
|
328 |
+
<Card.Description class="text-xs text-purple-600/70 dark:text-purple-300/70">
|
329 |
Receive video streams from remote cameras or AI systems
|
330 |
</Card.Description>
|
331 |
</div>
|
|
|
334 |
size="sm"
|
335 |
onclick={refreshRooms}
|
336 |
disabled={videoManager.roomsLoading || isConnecting}
|
337 |
+
class="h-7 px-2 text-xs text-purple-700 hover:text-purple-800 hover:bg-purple-200/20 dark:text-purple-300 dark:hover:text-purple-200 dark:hover:bg-purple-500/20"
|
338 |
>
|
339 |
{#if videoManager.roomsLoading}
|
340 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
|
|
349 |
<Card.Content class="space-y-4">
|
350 |
{#if video?.hasInput && video.input.type !== 'local-camera'}
|
351 |
<!-- Remote Connected State -->
|
352 |
+
<div class="rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20">
|
353 |
<div class="flex items-center justify-between">
|
354 |
<div>
|
355 |
+
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Room Connected</p>
|
356 |
+
<p class="text-xs text-purple-600/70 dark:text-purple-400/70">Receiving remote video stream</p>
|
357 |
</div>
|
358 |
<Button
|
359 |
variant="destructive"
|
|
|
369 |
</div>
|
370 |
{:else}
|
371 |
<!-- Create New Room -->
|
372 |
+
<div class="rounded border-2 border-dashed border-green-400/50 bg-green-100/5 p-3 dark:border-green-500/50 dark:bg-green-500/5">
|
373 |
<div class="space-y-2">
|
374 |
<div class="flex items-center gap-2">
|
375 |
+
<span class="icon-[mdi--plus-circle] size-4 text-green-500 dark:text-green-400"></span>
|
376 |
+
<p class="text-sm font-medium text-green-700 dark:text-green-300">Create New Room</p>
|
377 |
</div>
|
378 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
379 |
Create a room to receive video from others
|
380 |
</p>
|
381 |
<input
|
382 |
bind:value={customRoomId}
|
383 |
placeholder={`Room ID (default: ${video.id})`}
|
384 |
disabled={isConnecting || video?.hasInput}
|
385 |
+
class="w-full px-2 py-1 bg-slate-50 border border-slate-300 rounded text-xs text-slate-900 disabled:opacity-50 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100"
|
386 |
/>
|
387 |
<div class="flex gap-1">
|
388 |
<Button
|
|
|
390 |
size="sm"
|
391 |
onclick={createRoom}
|
392 |
disabled={isConnecting || video?.hasInput}
|
393 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
394 |
>
|
395 |
Create Only
|
396 |
</Button>
|
|
|
399 |
size="sm"
|
400 |
onclick={createRoomAndConnect}
|
401 |
disabled={isConnecting || video?.hasInput}
|
402 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
403 |
>
|
404 |
Create & Connect
|
405 |
</Button>
|
|
|
410 |
<!-- Existing Rooms -->
|
411 |
<div class="space-y-2">
|
412 |
<div class="flex items-center justify-between">
|
413 |
+
<span class="text-xs font-medium text-purple-700 dark:text-purple-300">Join Existing Room:</span>
|
414 |
+
<span class="text-xs text-slate-600 dark:text-slate-400">
|
415 |
{videoManager.rooms.length} room{videoManager.rooms.length !== 1 ? 's' : ''} available
|
416 |
</span>
|
417 |
</div>
|
418 |
|
419 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
420 |
{#if videoManager.rooms.length === 0}
|
421 |
+
<div class="text-center py-3 text-xs text-slate-600 dark:text-slate-400">
|
422 |
{videoManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
423 |
</div>
|
424 |
{:else}
|
425 |
{#each videoManager.rooms as room}
|
426 |
+
<div class="rounded border border-slate-300 bg-slate-50/50 p-2 dark:border-slate-600 dark:bg-slate-800/50">
|
427 |
<div class="flex items-start justify-between gap-3">
|
428 |
<div class="flex-1 min-w-0">
|
429 |
+
<p class="text-xs font-medium text-slate-800 truncate dark:text-slate-200">
|
430 |
{room.id}
|
431 |
</p>
|
432 |
+
<div class="flex gap-3 text-xs text-slate-600 dark:text-slate-400">
|
433 |
<span>{room.participants?.producer ? '📹 Has Output' : '📭 No Output'}</span>
|
434 |
<span>👥 {room.participants?.consumers?.length || 0} inputs</span>
|
435 |
</div>
|
|
|
440 |
size="sm"
|
441 |
onclick={() => handleConnectToRoom(room.id)}
|
442 |
disabled={isConnecting || video?.hasInput}
|
443 |
+
class="h-6 px-2 text-xs bg-purple-500 hover:bg-purple-600 shrink-0 disabled:opacity-50 dark:bg-purple-600 dark:hover:bg-purple-700"
|
444 |
>
|
445 |
<span class="icon-[mdi--download] mr-1 size-3"></span>
|
446 |
Join as Input
|
|
|
463 |
</div>
|
464 |
|
465 |
{#if video?.hasInput}
|
466 |
+
<p class="text-xs text-slate-600 dark:text-slate-500">
|
467 |
Disconnect current input to join a room
|
468 |
</p>
|
469 |
{/if}
|
|
|
472 |
</Card.Root>
|
473 |
|
474 |
<!-- Help Information -->
|
475 |
+
<Alert.Root class="border-slate-300 bg-slate-100/30 dark:border-slate-700 dark:bg-slate-800/30">
|
476 |
+
<span class="icon-[mdi--help-circle] size-4 text-slate-600 dark:text-slate-400"></span>
|
477 |
+
<Alert.Title class="text-slate-700 dark:text-slate-300">Video Input Sources</Alert.Title>
|
478 |
+
<Alert.Description class="text-slate-600 text-xs dark:text-slate-400">
|
479 |
<strong>Camera:</strong> Local device camera • <strong>Remote:</strong> Video streams from rooms • Only one active at a time
|
480 |
</Alert.Description>
|
481 |
</Alert.Root>
|
src/lib/components/3d/elements/video/modal/VideoOutputConnectionModal.svelte
CHANGED
@@ -126,15 +126,15 @@
|
|
126 |
|
127 |
<Dialog.Root bind:open>
|
128 |
<Dialog.Content
|
129 |
-
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-600 bg-slate-900 text-slate-100"
|
130 |
>
|
131 |
<Dialog.Header class="pb-3">
|
132 |
-
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
133 |
-
<span class="icon-[mdi--video] size-5 text-
|
134 |
Video Output - {video?.name || 'No Video Selected'}
|
135 |
</Dialog.Title>
|
136 |
-
<Dialog.Description class="text-sm text-slate-400">
|
137 |
-
|
138 |
</Dialog.Description>
|
139 |
</Dialog.Header>
|
140 |
|
@@ -142,102 +142,67 @@
|
|
142 |
<div class="space-y-4 pb-4">
|
143 |
<!-- Error display -->
|
144 |
{#if error}
|
145 |
-
<Alert.Root class="border-red-500/30 bg-red-900/20">
|
146 |
-
<span class="icon-[mdi--alert-circle] size-4 text-red-400"></span>
|
147 |
-
<Alert.Title class="text-red-300">
|
148 |
-
<Alert.Description class="text-red-
|
149 |
{error}
|
150 |
</Alert.Description>
|
151 |
</Alert.Root>
|
152 |
{/if}
|
|
|
153 |
<!-- Current Status Overview -->
|
154 |
-
<Card.Root class="border-
|
155 |
<Card.Content class="p-4">
|
156 |
<div class="flex items-center justify-between">
|
157 |
<div class="flex items-center gap-2">
|
158 |
-
<span class="icon-[mdi--video] size-4 text-
|
159 |
-
<span class="text-sm font-medium text-
|
160 |
</div>
|
161 |
{#if video?.hasOutput}
|
162 |
-
<Badge variant="default" class="bg-
|
163 |
-
|
164 |
</Badge>
|
165 |
{:else}
|
166 |
-
<Badge variant="secondary" class="text-xs text-slate-400">
|
167 |
{/if}
|
168 |
</div>
|
169 |
{#if video?.hasOutput}
|
170 |
-
<div class="mt-2 text-xs text-
|
171 |
{#if video.output.roomId}
|
172 |
-
Room: {video.output.roomId}
|
173 |
{:else}
|
174 |
-
|
175 |
{/if}
|
176 |
</div>
|
177 |
{/if}
|
178 |
</Card.Content>
|
179 |
</Card.Root>
|
180 |
|
181 |
-
<!-- Output Requirements -->
|
182 |
-
{#if !video?.canOutput}
|
183 |
-
<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
|
184 |
-
<Card.Header>
|
185 |
-
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
186 |
-
<span class="icon-[mdi--alert] size-4"></span>
|
187 |
-
Requirements
|
188 |
-
</Card.Title>
|
189 |
-
</Card.Header>
|
190 |
-
<Card.Content>
|
191 |
-
<div class="space-y-2 text-sm text-yellow-300">
|
192 |
-
{#if !video?.hasInput}
|
193 |
-
<div class="flex items-center gap-2">
|
194 |
-
<span class="icon-[mdi--close-circle] size-3 text-red-400"></span>
|
195 |
-
No video input connected
|
196 |
-
</div>
|
197 |
-
{:else if video.input.type !== 'local-camera'}
|
198 |
-
<div class="flex items-center gap-2">
|
199 |
-
<span class="icon-[mdi--close-circle] size-3 text-red-400"></span>
|
200 |
-
Input must be local camera (cannot re-broadcast remote streams)
|
201 |
-
</div>
|
202 |
-
{/if}
|
203 |
-
<div class="mt-2 text-xs text-yellow-400/70">
|
204 |
-
To enable output, first connect to your local camera in the Video Input modal.
|
205 |
-
</div>
|
206 |
-
</div>
|
207 |
-
</Card.Content>
|
208 |
-
</Card.Root>
|
209 |
-
{/if}
|
210 |
-
|
211 |
<!-- Current Output Details -->
|
212 |
{#if video?.hasOutput}
|
213 |
-
<Card.Root class="border-
|
214 |
<Card.Header>
|
215 |
-
<Card.Title class="flex items-center gap-2 text-base text-
|
216 |
-
<span class="icon-[mdi--
|
217 |
-
|
218 |
</Card.Title>
|
219 |
</Card.Header>
|
220 |
<Card.Content>
|
221 |
-
<div class="rounded-lg border border-
|
222 |
<div class="flex items-center justify-between">
|
223 |
<div>
|
224 |
-
<p class="text-sm font-medium text-
|
225 |
-
|
226 |
</p>
|
227 |
{#if video.output.roomId}
|
228 |
-
<p class="text-xs text-
|
229 |
-
Room
|
230 |
</p>
|
231 |
{/if}
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
{#if video.input.stream}
|
236 |
-
<p class="text-xs text-blue-400/70">
|
237 |
-
Video: {video.input.stream.getVideoTracks().length} tracks
|
238 |
-
</p>
|
239 |
-
<p class="text-xs text-blue-400/70">
|
240 |
-
Audio: {video.input.stream.getAudioTracks().length} tracks
|
241 |
</p>
|
242 |
{/if}
|
243 |
</div>
|
@@ -247,7 +212,7 @@
|
|
247 |
onclick={handleStopOutput}
|
248 |
class="h-7 px-2 text-xs"
|
249 |
>
|
250 |
-
<span class="icon-[mdi--
|
251 |
Stop
|
252 |
</Button>
|
253 |
</div>
|
@@ -256,17 +221,70 @@
|
|
256 |
</Card.Root>
|
257 |
{/if}
|
258 |
|
259 |
-
<!--
|
260 |
-
<Card.Root class="border-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
<Card.Header>
|
262 |
<div class="flex items-center justify-between">
|
263 |
<div>
|
264 |
-
<Card.Title class="flex items-center gap-2 text-base text-
|
265 |
-
<span class="icon-[mdi--
|
266 |
-
Remote
|
267 |
</Card.Title>
|
268 |
-
<Card.Description class="text-xs text-
|
269 |
-
Broadcast
|
270 |
</Card.Description>
|
271 |
</div>
|
272 |
<Button
|
@@ -274,7 +292,7 @@
|
|
274 |
size="sm"
|
275 |
onclick={refreshRooms}
|
276 |
disabled={videoManager.roomsLoading || isConnecting}
|
277 |
-
class="h-7 px-2 text-xs text-
|
278 |
>
|
279 |
{#if videoManager.roomsLoading}
|
280 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
@@ -287,13 +305,13 @@
|
|
287 |
</div>
|
288 |
</Card.Header>
|
289 |
<Card.Content class="space-y-4">
|
290 |
-
{#if video?.hasOutput}
|
291 |
-
<!--
|
292 |
-
<div class="rounded-lg border border-
|
293 |
<div class="flex items-center justify-between">
|
294 |
<div>
|
295 |
-
<p class="text-sm font-medium text-
|
296 |
-
<p class="text-xs text-
|
297 |
</div>
|
298 |
<Button
|
299 |
variant="destructive"
|
@@ -302,27 +320,27 @@
|
|
302 |
disabled={isConnecting}
|
303 |
class="h-7 px-2 text-xs"
|
304 |
>
|
305 |
-
<span class="icon-[mdi--
|
306 |
-
{isConnecting ? 'Stopping...' : 'Stop'}
|
307 |
</Button>
|
308 |
</div>
|
309 |
</div>
|
310 |
-
{:else
|
311 |
<!-- Create New Room -->
|
312 |
-
<div class="rounded border-2 border-dashed border-green-
|
313 |
<div class="space-y-2">
|
314 |
<div class="flex items-center gap-2">
|
315 |
-
<span class="icon-[mdi--plus-circle] size-4 text-green-400"></span>
|
316 |
-
<p class="text-sm font-medium text-green-300">Create New Room</p>
|
317 |
</div>
|
318 |
-
<p class="text-xs text-green-400/70">
|
319 |
-
Create a room to broadcast your
|
320 |
</p>
|
321 |
<input
|
322 |
bind:value={customRoomId}
|
323 |
placeholder={`Room ID (default: ${video.id})`}
|
324 |
disabled={isConnecting || video?.hasOutput}
|
325 |
-
class="w-full px-2 py-1 bg-slate-
|
326 |
/>
|
327 |
<div class="flex gap-1">
|
328 |
<Button
|
@@ -330,7 +348,7 @@
|
|
330 |
size="sm"
|
331 |
onclick={createRoom}
|
332 |
disabled={isConnecting || video?.hasOutput}
|
333 |
-
class="h-6 px-2 text-xs bg-green-
|
334 |
>
|
335 |
Create Only
|
336 |
</Button>
|
@@ -339,9 +357,9 @@
|
|
339 |
size="sm"
|
340 |
onclick={createRoomAndStartOutput}
|
341 |
disabled={isConnecting || video?.hasOutput}
|
342 |
-
class="h-6 px-2 text-xs bg-green-
|
343 |
>
|
344 |
-
Create &
|
345 |
</Button>
|
346 |
</div>
|
347 |
</div>
|
@@ -350,28 +368,28 @@
|
|
350 |
<!-- Existing Rooms -->
|
351 |
<div class="space-y-2">
|
352 |
<div class="flex items-center justify-between">
|
353 |
-
<span class="text-xs font-medium text-
|
354 |
-
<span class="text-xs text-slate-400">
|
355 |
{videoManager.rooms.length} room{videoManager.rooms.length !== 1 ? 's' : ''} available
|
356 |
</span>
|
357 |
</div>
|
358 |
|
359 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
360 |
{#if videoManager.rooms.length === 0}
|
361 |
-
<div class="text-center py-3 text-xs text-slate-400">
|
362 |
{videoManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
363 |
</div>
|
364 |
{:else}
|
365 |
{#each videoManager.rooms as room}
|
366 |
-
<div class="rounded border border-slate-
|
367 |
<div class="flex items-start justify-between gap-3">
|
368 |
<div class="flex-1 min-w-0">
|
369 |
-
<p class="text-xs font-medium text-slate-
|
370 |
{room.id}
|
371 |
</p>
|
372 |
-
<div class="flex gap-3 text-xs text-slate-400">
|
373 |
-
<span>{room.participants?.producer ? '🔴
|
374 |
-
<span>👥 {room.participants?.consumers?.length || 0}
|
375 |
</div>
|
376 |
</div>
|
377 |
{#if !room.participants?.producer}
|
@@ -380,9 +398,9 @@
|
|
380 |
size="sm"
|
381 |
onclick={() => handleStartOutputToRoom(room.id)}
|
382 |
disabled={isConnecting || video?.hasOutput}
|
383 |
-
class="h-6 px-2 text-xs bg-
|
384 |
>
|
385 |
-
<span class="icon-[mdi--
|
386 |
Join as Output
|
387 |
</Button>
|
388 |
{:else}
|
@@ -392,7 +410,7 @@
|
|
392 |
disabled
|
393 |
class="text-xs opacity-50 shrink-0"
|
394 |
>
|
395 |
-
|
396 |
</Button>
|
397 |
{/if}
|
398 |
</div>
|
@@ -403,27 +421,20 @@
|
|
403 |
</div>
|
404 |
|
405 |
{#if video?.hasOutput}
|
406 |
-
<p class="text-xs text-slate-500">
|
407 |
-
Stop current
|
408 |
</p>
|
409 |
{/if}
|
410 |
-
{:else}
|
411 |
-
<!-- Cannot output -->
|
412 |
-
<div class="space-y-3">
|
413 |
-
<div class="text-center text-sm text-slate-500">
|
414 |
-
Connect to local camera first to enable broadcasting
|
415 |
-
</div>
|
416 |
-
</div>
|
417 |
{/if}
|
418 |
</Card.Content>
|
419 |
</Card.Root>
|
420 |
|
421 |
<!-- Help Information -->
|
422 |
-
<Alert.Root class="border-slate-700 bg-slate-800/30">
|
423 |
-
<span class="icon-[mdi--help-circle] size-4 text-slate-400"></span>
|
424 |
-
<Alert.Title class="text-slate-300">Video
|
425 |
-
<Alert.Description class="text-slate-
|
426 |
-
<strong>
|
427 |
</Alert.Description>
|
428 |
</Alert.Root>
|
429 |
</div>
|
|
|
126 |
|
127 |
<Dialog.Root bind:open>
|
128 |
<Dialog.Content
|
129 |
+
class="max-h-[85vh] max-w-4xl overflow-hidden border-slate-300 bg-slate-100 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100"
|
130 |
>
|
131 |
<Dialog.Header class="pb-3">
|
132 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-900 dark:text-slate-100">
|
133 |
+
<span class="icon-[mdi--video-wireless-outline] size-5 text-orange-500 dark:text-orange-400"></span>
|
134 |
Video Output - {video?.name || 'No Video Selected'}
|
135 |
</Dialog.Title>
|
136 |
+
<Dialog.Description class="text-sm text-slate-600 dark:text-slate-400">
|
137 |
+
Configure video output: local recording or remote broadcast to rooms
|
138 |
</Dialog.Description>
|
139 |
</Dialog.Header>
|
140 |
|
|
|
142 |
<div class="space-y-4 pb-4">
|
143 |
<!-- Error display -->
|
144 |
{#if error}
|
145 |
+
<Alert.Root class="border-red-300/30 bg-red-100/20 dark:border-red-500/30 dark:bg-red-900/20">
|
146 |
+
<span class="icon-[mdi--alert-circle] size-4 text-red-500 dark:text-red-400"></span>
|
147 |
+
<Alert.Title class="text-red-700 dark:text-red-300">Connection Error</Alert.Title>
|
148 |
+
<Alert.Description class="text-red-600 text-sm dark:text-red-400">
|
149 |
{error}
|
150 |
</Alert.Description>
|
151 |
</Alert.Root>
|
152 |
{/if}
|
153 |
+
|
154 |
<!-- Current Status Overview -->
|
155 |
+
<Card.Root class="border-orange-300/30 bg-orange-100/20 dark:border-orange-500/30 dark:bg-orange-900/20">
|
156 |
<Card.Content class="p-4">
|
157 |
<div class="flex items-center justify-between">
|
158 |
<div class="flex items-center gap-2">
|
159 |
+
<span class="icon-[mdi--video-wireless-outline] size-4 text-orange-500 dark:text-orange-400"></span>
|
160 |
+
<span class="text-sm font-medium text-orange-700 dark:text-orange-300">Current Video Output</span>
|
161 |
</div>
|
162 |
{#if video?.hasOutput}
|
163 |
+
<Badge variant="default" class="bg-orange-500 text-xs dark:bg-orange-600">
|
164 |
+
{video.output.type === 'recording' ? 'Recording' : 'Remote Broadcast'}
|
165 |
</Badge>
|
166 |
{:else}
|
167 |
+
<Badge variant="secondary" class="text-xs text-slate-600 dark:text-slate-400">No Output Active</Badge>
|
168 |
{/if}
|
169 |
</div>
|
170 |
{#if video?.hasOutput}
|
171 |
+
<div class="mt-2 text-xs text-orange-600/70 dark:text-orange-400/70">
|
172 |
{#if video.output.roomId}
|
173 |
+
Broadcasting to Room: {video.output.roomId}
|
174 |
{:else}
|
175 |
+
Recording to local storage
|
176 |
{/if}
|
177 |
</div>
|
178 |
{/if}
|
179 |
</Card.Content>
|
180 |
</Card.Root>
|
181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
<!-- Current Output Details -->
|
183 |
{#if video?.hasOutput}
|
184 |
+
<Card.Root class="border-orange-300/30 bg-orange-100/5 dark:border-orange-500/30 dark:bg-orange-500/5">
|
185 |
<Card.Header>
|
186 |
+
<Card.Title class="flex items-center gap-2 text-base text-orange-700 dark:text-orange-200">
|
187 |
+
<span class="icon-[mdi--video-wireless] size-4"></span>
|
188 |
+
Current Output
|
189 |
</Card.Title>
|
190 |
</Card.Header>
|
191 |
<Card.Content>
|
192 |
+
<div class="rounded-lg border border-orange-300/30 bg-orange-100/20 p-3 dark:border-orange-500/30 dark:bg-orange-900/20">
|
193 |
<div class="flex items-center justify-between">
|
194 |
<div>
|
195 |
+
<p class="text-sm font-medium text-orange-700 dark:text-orange-300">
|
196 |
+
{video.output.type === 'recording' ? 'Local Recording' : 'Remote Broadcast'}
|
197 |
</p>
|
198 |
{#if video.output.roomId}
|
199 |
+
<p class="text-xs text-orange-600/70 dark:text-orange-400/70">
|
200 |
+
Room: {video.output.roomId}
|
201 |
</p>
|
202 |
{/if}
|
203 |
+
{#if video.output.stream}
|
204 |
+
<p class="text-xs text-orange-600/70 dark:text-orange-400/70">
|
205 |
+
Status: Active • {video.output.stream.getVideoTracks().length} video tracks
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
</p>
|
207 |
{/if}
|
208 |
</div>
|
|
|
212 |
onclick={handleStopOutput}
|
213 |
class="h-7 px-2 text-xs"
|
214 |
>
|
215 |
+
<span class="icon-[mdi--close-circle] mr-1 size-3"></span>
|
216 |
Stop
|
217 |
</Button>
|
218 |
</div>
|
|
|
221 |
</Card.Root>
|
222 |
{/if}
|
223 |
|
224 |
+
<!-- Local Recording -->
|
225 |
+
<Card.Root class="border-blue-300/30 bg-blue-100/5 dark:border-blue-500/30 dark:bg-blue-500/5">
|
226 |
+
<Card.Header>
|
227 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-700 dark:text-blue-200">
|
228 |
+
<span class="icon-[mdi--record-rec] size-4"></span>
|
229 |
+
Local Recording
|
230 |
+
</Card.Title>
|
231 |
+
<Card.Description class="text-xs text-blue-600/70 dark:text-blue-300/70">
|
232 |
+
Record video directly to your device for later use
|
233 |
+
</Card.Description>
|
234 |
+
</Card.Header>
|
235 |
+
<Card.Content class="space-y-3">
|
236 |
+
{#if video?.hasOutput && video.output.type === 'recording'}
|
237 |
+
<!-- Recording Active State -->
|
238 |
+
<div class="rounded-lg border border-blue-300/30 bg-blue-100/20 p-3 dark:border-blue-500/30 dark:bg-blue-900/20">
|
239 |
+
<div class="flex items-center justify-between">
|
240 |
+
<div>
|
241 |
+
<p class="text-sm font-medium text-blue-700 dark:text-blue-300">Recording Active</p>
|
242 |
+
<p class="text-xs text-blue-600/70 dark:text-blue-400/70">Saving to local device</p>
|
243 |
+
</div>
|
244 |
+
<Button
|
245 |
+
variant="destructive"
|
246 |
+
size="sm"
|
247 |
+
onclick={handleStopOutput}
|
248 |
+
disabled={isConnecting}
|
249 |
+
class="h-7 px-2 text-xs"
|
250 |
+
>
|
251 |
+
<span class="icon-[mdi--stop] mr-1 size-3"></span>
|
252 |
+
{isConnecting ? 'Stopping...' : 'Stop Recording'}
|
253 |
+
</Button>
|
254 |
+
</div>
|
255 |
+
</div>
|
256 |
+
{:else}
|
257 |
+
<!-- Recording Start Button -->
|
258 |
+
<Button
|
259 |
+
variant="secondary"
|
260 |
+
onclick={handleStartRecording}
|
261 |
+
disabled={isConnecting || video?.hasOutput}
|
262 |
+
class="w-full bg-blue-500 text-sm text-white hover:bg-blue-600 disabled:opacity-50 dark:bg-blue-600 dark:hover:bg-blue-700"
|
263 |
+
>
|
264 |
+
<span class="icon-[mdi--record] mr-2 size-4"></span>
|
265 |
+
{isConnecting ? 'Starting...' : 'Start Recording'}
|
266 |
+
</Button>
|
267 |
+
|
268 |
+
{#if video?.hasOutput}
|
269 |
+
<p class="text-xs text-slate-600 dark:text-slate-500">
|
270 |
+
Stop current output to start recording
|
271 |
+
</p>
|
272 |
+
{/if}
|
273 |
+
{/if}
|
274 |
+
</Card.Content>
|
275 |
+
</Card.Root>
|
276 |
+
|
277 |
+
<!-- Remote Collaboration -->
|
278 |
+
<Card.Root class="border-purple-300/30 bg-purple-100/5 dark:border-purple-500/30 dark:bg-purple-500/5">
|
279 |
<Card.Header>
|
280 |
<div class="flex items-center justify-between">
|
281 |
<div>
|
282 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-700 dark:text-purple-200">
|
283 |
+
<span class="icon-[mdi--cloud-upload] size-4"></span>
|
284 |
+
Remote Collaboration (Rooms)
|
285 |
</Card.Title>
|
286 |
+
<Card.Description class="text-xs text-purple-600/70 dark:text-purple-300/70">
|
287 |
+
Broadcast video stream to remote systems and users
|
288 |
</Card.Description>
|
289 |
</div>
|
290 |
<Button
|
|
|
292 |
size="sm"
|
293 |
onclick={refreshRooms}
|
294 |
disabled={videoManager.roomsLoading || isConnecting}
|
295 |
+
class="h-7 px-2 text-xs text-purple-700 hover:text-purple-800 hover:bg-purple-200/20 dark:text-purple-300 dark:hover:text-purple-200 dark:hover:bg-purple-500/20"
|
296 |
>
|
297 |
{#if videoManager.roomsLoading}
|
298 |
<span class="icon-[mdi--loading] animate-spin size-3 mr-1"></span>
|
|
|
305 |
</div>
|
306 |
</Card.Header>
|
307 |
<Card.Content class="space-y-4">
|
308 |
+
{#if video?.hasOutput && video.output.type !== 'recording'}
|
309 |
+
<!-- Remote Connected State -->
|
310 |
+
<div class="rounded-lg border border-purple-300/30 bg-purple-100/20 p-3 dark:border-purple-500/30 dark:bg-purple-900/20">
|
311 |
<div class="flex items-center justify-between">
|
312 |
<div>
|
313 |
+
<p class="text-sm font-medium text-purple-700 dark:text-purple-300">Broadcasting to Room</p>
|
314 |
+
<p class="text-xs text-purple-600/70 dark:text-purple-400/70">Video stream active</p>
|
315 |
</div>
|
316 |
<Button
|
317 |
variant="destructive"
|
|
|
320 |
disabled={isConnecting}
|
321 |
class="h-7 px-2 text-xs"
|
322 |
>
|
323 |
+
<span class="icon-[mdi--close-circle] mr-1 size-3"></span>
|
324 |
+
{isConnecting ? 'Stopping...' : 'Stop Broadcast'}
|
325 |
</Button>
|
326 |
</div>
|
327 |
</div>
|
328 |
+
{:else}
|
329 |
<!-- Create New Room -->
|
330 |
+
<div class="rounded border-2 border-dashed border-green-400/50 bg-green-100/5 p-3 dark:border-green-500/50 dark:bg-green-500/5">
|
331 |
<div class="space-y-2">
|
332 |
<div class="flex items-center gap-2">
|
333 |
+
<span class="icon-[mdi--plus-circle] size-4 text-green-500 dark:text-green-400"></span>
|
334 |
+
<p class="text-sm font-medium text-green-700 dark:text-green-300">Create New Room</p>
|
335 |
</div>
|
336 |
+
<p class="text-xs text-green-600/70 dark:text-green-400/70">
|
337 |
+
Create a room to broadcast your video
|
338 |
</p>
|
339 |
<input
|
340 |
bind:value={customRoomId}
|
341 |
placeholder={`Room ID (default: ${video.id})`}
|
342 |
disabled={isConnecting || video?.hasOutput}
|
343 |
+
class="w-full px-2 py-1 bg-slate-50 border border-slate-300 rounded text-xs text-slate-900 disabled:opacity-50 dark:bg-slate-700 dark:border-slate-600 dark:text-slate-100"
|
344 |
/>
|
345 |
<div class="flex gap-1">
|
346 |
<Button
|
|
|
348 |
size="sm"
|
349 |
onclick={createRoom}
|
350 |
disabled={isConnecting || video?.hasOutput}
|
351 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
352 |
>
|
353 |
Create Only
|
354 |
</Button>
|
|
|
357 |
size="sm"
|
358 |
onclick={createRoomAndStartOutput}
|
359 |
disabled={isConnecting || video?.hasOutput}
|
360 |
+
class="h-6 px-2 text-xs bg-green-500 hover:bg-green-600 disabled:opacity-50 dark:bg-green-600 dark:hover:bg-green-700"
|
361 |
>
|
362 |
+
Create & Broadcast
|
363 |
</Button>
|
364 |
</div>
|
365 |
</div>
|
|
|
368 |
<!-- Existing Rooms -->
|
369 |
<div class="space-y-2">
|
370 |
<div class="flex items-center justify-between">
|
371 |
+
<span class="text-xs font-medium text-purple-700 dark:text-purple-300">Join Existing Room:</span>
|
372 |
+
<span class="text-xs text-slate-600 dark:text-slate-400">
|
373 |
{videoManager.rooms.length} room{videoManager.rooms.length !== 1 ? 's' : ''} available
|
374 |
</span>
|
375 |
</div>
|
376 |
|
377 |
<div class="max-h-40 space-y-2 overflow-y-auto">
|
378 |
{#if videoManager.rooms.length === 0}
|
379 |
+
<div class="text-center py-3 text-xs text-slate-600 dark:text-slate-400">
|
380 |
{videoManager.roomsLoading ? 'Loading rooms...' : 'No rooms available. Create one to get started.'}
|
381 |
</div>
|
382 |
{:else}
|
383 |
{#each videoManager.rooms as room}
|
384 |
+
<div class="rounded border border-slate-300 bg-slate-50/50 p-2 dark:border-slate-600 dark:bg-slate-800/50">
|
385 |
<div class="flex items-start justify-between gap-3">
|
386 |
<div class="flex-1 min-w-0">
|
387 |
+
<p class="text-xs font-medium text-slate-800 truncate dark:text-slate-200">
|
388 |
{room.id}
|
389 |
</p>
|
390 |
+
<div class="flex gap-3 text-xs text-slate-600 dark:text-slate-400">
|
391 |
+
<span>{room.participants?.producer ? '🔴 Has Output' : '🟢 Available'}</span>
|
392 |
+
<span>👥 {room.participants?.consumers?.length || 0} inputs</span>
|
393 |
</div>
|
394 |
</div>
|
395 |
{#if !room.participants?.producer}
|
|
|
398 |
size="sm"
|
399 |
onclick={() => handleStartOutputToRoom(room.id)}
|
400 |
disabled={isConnecting || video?.hasOutput}
|
401 |
+
class="h-6 px-2 text-xs bg-purple-500 hover:bg-purple-600 shrink-0 disabled:opacity-50 dark:bg-purple-600 dark:hover:bg-purple-700"
|
402 |
>
|
403 |
+
<span class="icon-[mdi--upload] mr-1 size-3"></span>
|
404 |
Join as Output
|
405 |
</Button>
|
406 |
{:else}
|
|
|
410 |
disabled
|
411 |
class="text-xs opacity-50 shrink-0"
|
412 |
>
|
413 |
+
Has Output
|
414 |
</Button>
|
415 |
{/if}
|
416 |
</div>
|
|
|
421 |
</div>
|
422 |
|
423 |
{#if video?.hasOutput}
|
424 |
+
<p class="text-xs text-slate-600 dark:text-slate-500">
|
425 |
+
Stop current output to join a room
|
426 |
</p>
|
427 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
428 |
{/if}
|
429 |
</Card.Content>
|
430 |
</Card.Root>
|
431 |
|
432 |
<!-- Help Information -->
|
433 |
+
<Alert.Root class="border-slate-300 bg-slate-100/30 dark:border-slate-700 dark:bg-slate-800/30">
|
434 |
+
<span class="icon-[mdi--help-circle] size-4 text-slate-600 dark:text-slate-400"></span>
|
435 |
+
<Alert.Title class="text-slate-700 dark:text-slate-300">Video Output Options</Alert.Title>
|
436 |
+
<Alert.Description class="text-slate-600 text-xs dark:text-slate-400">
|
437 |
+
<strong>Recording:</strong> Save locally • <strong>Remote:</strong> Broadcast to rooms • Only one active at a time
|
438 |
</Alert.Description>
|
439 |
</Alert.Root>
|
440 |
</div>
|
src/lib/components/interface/overlay/AddAIButton.svelte
CHANGED
@@ -77,7 +77,7 @@
|
|
77 |
variant="default"
|
78 |
size="sm"
|
79 |
onclick={quickAddAI}
|
80 |
-
class="group rounded-r-none border-0 bg-purple-
|
81 |
>
|
82 |
<span
|
83 |
class={[
|
@@ -97,8 +97,9 @@
|
|
97 |
variant="default"
|
98 |
size="sm"
|
99 |
class={cn(
|
100 |
-
"rounded-l-none border-0 border-l border-purple-
|
101 |
-
|
|
|
102 |
)}
|
103 |
>
|
104 |
<span
|
@@ -113,12 +114,12 @@
|
|
113 |
</DropdownMenu.Trigger>
|
114 |
|
115 |
<DropdownMenu.Content
|
116 |
-
class="w-56 border-purple-
|
117 |
align="center"
|
118 |
>
|
119 |
<DropdownMenu.Group>
|
120 |
<DropdownMenu.GroupHeading
|
121 |
-
class="text-xs font-semibold tracking-wider text-purple-
|
122 |
>
|
123 |
AI Types
|
124 |
</DropdownMenu.GroupHeading>
|
@@ -126,8 +127,8 @@
|
|
126 |
{#each aiOptions as ai}
|
127 |
<DropdownMenu.Item
|
128 |
class={[
|
129 |
-
"group group cursor-pointer bg-purple-
|
130 |
-
"data-highlighted:bg-purple-700"
|
131 |
]}
|
132 |
onclick={async () => await addAI(ai.id)}
|
133 |
disabled={!ai.enabled}
|
@@ -135,14 +136,14 @@
|
|
135 |
<span
|
136 |
class={[
|
137 |
ai.icon,
|
138 |
-
"mr-3 size-4 text-purple-
|
139 |
]}
|
140 |
></span>
|
141 |
<div class="flex flex-1 flex-col">
|
142 |
<span class="font-medium text-white transition-colors duration-200"
|
143 |
>{formatAIType(ai.id)}</span
|
144 |
>
|
145 |
-
<span class="text-xs text-purple-
|
146 |
{getAIDescription(ai.id)}
|
147 |
</span>
|
148 |
</div>
|
|
|
77 |
variant="default"
|
78 |
size="sm"
|
79 |
onclick={quickAddAI}
|
80 |
+
class="group rounded-r-none border-0 bg-purple-500 text-white transition-all duration-200 hover:bg-purple-400 dark:bg-purple-600 dark:hover:bg-purple-500"
|
81 |
>
|
82 |
<span
|
83 |
class={[
|
|
|
97 |
variant="default"
|
98 |
size="sm"
|
99 |
class={cn(
|
100 |
+
"rounded-l-none border-0 border-l border-purple-400/30 bg-purple-500 px-2 text-white transition-all duration-200 hover:bg-purple-400",
|
101 |
+
"dark:border-purple-500/30 dark:bg-purple-600 dark:hover:bg-purple-500",
|
102 |
+
open && "bg-purple-600 shadow-inner dark:bg-purple-700"
|
103 |
)}
|
104 |
>
|
105 |
<span
|
|
|
114 |
</DropdownMenu.Trigger>
|
115 |
|
116 |
<DropdownMenu.Content
|
117 |
+
class="w-56 border-purple-400/30 bg-purple-500 backdrop-blur-sm dark:border-purple-500/30 dark:bg-purple-600"
|
118 |
align="center"
|
119 |
>
|
120 |
<DropdownMenu.Group>
|
121 |
<DropdownMenu.GroupHeading
|
122 |
+
class="text-xs font-semibold tracking-wider text-purple-100 uppercase dark:text-purple-200"
|
123 |
>
|
124 |
AI Types
|
125 |
</DropdownMenu.GroupHeading>
|
|
|
127 |
{#each aiOptions as ai}
|
128 |
<DropdownMenu.Item
|
129 |
class={[
|
130 |
+
"group group cursor-pointer bg-purple-500 text-white transition-all duration-200",
|
131 |
+
"data-highlighted:bg-purple-600 dark:bg-purple-600 dark:data-highlighted:bg-purple-700"
|
132 |
]}
|
133 |
onclick={async () => await addAI(ai.id)}
|
134 |
disabled={!ai.enabled}
|
|
|
136 |
<span
|
137 |
class={[
|
138 |
ai.icon,
|
139 |
+
"mr-3 size-4 text-purple-100 transition-colors duration-200 dark:text-purple-200"
|
140 |
]}
|
141 |
></span>
|
142 |
<div class="flex flex-1 flex-col">
|
143 |
<span class="font-medium text-white transition-colors duration-200"
|
144 |
>{formatAIType(ai.id)}</span
|
145 |
>
|
146 |
+
<span class="text-xs text-purple-100 transition-colors duration-200 dark:text-purple-200">
|
147 |
{getAIDescription(ai.id)}
|
148 |
</span>
|
149 |
</div>
|
src/lib/components/interface/overlay/AddRobotButton.svelte
CHANGED
@@ -55,7 +55,7 @@
|
|
55 |
variant="default"
|
56 |
size="sm"
|
57 |
onclick={quickAddSO100}
|
58 |
-
class="group rounded-r-none border-0 bg-emerald-
|
59 |
>
|
60 |
<span
|
61 |
class={[
|
@@ -75,8 +75,9 @@
|
|
75 |
variant="default"
|
76 |
size="sm"
|
77 |
class={cn(
|
78 |
-
"rounded-l-none border-0 border-l border-emerald-
|
79 |
-
|
|
|
80 |
)}
|
81 |
>
|
82 |
<span
|
@@ -91,12 +92,12 @@
|
|
91 |
</DropdownMenu.Trigger>
|
92 |
|
93 |
<DropdownMenu.Content
|
94 |
-
class="w-56 border-emerald-
|
95 |
align="center"
|
96 |
>
|
97 |
<DropdownMenu.Group>
|
98 |
<DropdownMenu.GroupHeading
|
99 |
-
class="text-xs font-semibold tracking-wider text-emerald-
|
100 |
>
|
101 |
Robot Types
|
102 |
</DropdownMenu.GroupHeading>
|
@@ -104,29 +105,29 @@
|
|
104 |
{#each robotTypes as robotType}
|
105 |
<DropdownMenu.Item
|
106 |
class={[
|
107 |
-
"group group cursor-pointer bg-emerald-
|
108 |
-
"data-highlighted:bg-emerald-700
|
109 |
]}
|
110 |
onclick={async () => await addRobot(robotType)}
|
111 |
>
|
112 |
<span
|
113 |
class={[
|
114 |
robotType === "so-arm100" ? "icon-[ix--robotic-arm]" : "icon-[mdi--robot-industrial]",
|
115 |
-
"mr-3 size-4 text-emerald-
|
116 |
]}
|
117 |
></span>
|
118 |
<div class="flex flex-1 flex-col">
|
119 |
<span class="font-medium text-white transition-colors duration-200"
|
120 |
>{robotType.replace(/-/g, " ").toUpperCase()}</span
|
121 |
>
|
122 |
-
<span class="text-xs text-emerald-
|
123 |
{robotType === "so-arm100" ? "6-DOF Robotic Arm" : "Industrial Robot"}
|
124 |
</span>
|
125 |
</div>
|
126 |
{#if robotType === "so-arm100"}
|
127 |
<Badge
|
128 |
variant="secondary"
|
129 |
-
class="ml-2 bg-emerald-
|
130 |
>
|
131 |
Default
|
132 |
</Badge>
|
|
|
55 |
variant="default"
|
56 |
size="sm"
|
57 |
onclick={quickAddSO100}
|
58 |
+
class="group rounded-r-none border-0 bg-emerald-500 text-white transition-all duration-200 hover:bg-emerald-400 dark:bg-emerald-600 dark:hover:bg-emerald-500"
|
59 |
>
|
60 |
<span
|
61 |
class={[
|
|
|
75 |
variant="default"
|
76 |
size="sm"
|
77 |
class={cn(
|
78 |
+
"rounded-l-none border-0 border-l border-emerald-400/30 bg-emerald-500 px-2 text-white transition-all duration-200 hover:bg-emerald-400",
|
79 |
+
"dark:border-emerald-500/30 dark:bg-emerald-600 dark:hover:bg-emerald-500",
|
80 |
+
open && "bg-emerald-600 shadow-inner dark:bg-emerald-700"
|
81 |
)}
|
82 |
>
|
83 |
<span
|
|
|
92 |
</DropdownMenu.Trigger>
|
93 |
|
94 |
<DropdownMenu.Content
|
95 |
+
class="w-56 border-emerald-400/30 bg-emerald-500 backdrop-blur-sm dark:border-emerald-500/30 dark:bg-emerald-600"
|
96 |
align="center"
|
97 |
>
|
98 |
<DropdownMenu.Group>
|
99 |
<DropdownMenu.GroupHeading
|
100 |
+
class="text-xs font-semibold tracking-wider text-emerald-100 uppercase dark:text-emerald-200"
|
101 |
>
|
102 |
Robot Types
|
103 |
</DropdownMenu.GroupHeading>
|
|
|
105 |
{#each robotTypes as robotType}
|
106 |
<DropdownMenu.Item
|
107 |
class={[
|
108 |
+
"group group cursor-pointer bg-emerald-500 text-white transition-all duration-200",
|
109 |
+
"data-highlighted:bg-emerald-600 dark:bg-emerald-600 dark:data-highlighted:bg-emerald-700"
|
110 |
]}
|
111 |
onclick={async () => await addRobot(robotType)}
|
112 |
>
|
113 |
<span
|
114 |
class={[
|
115 |
robotType === "so-arm100" ? "icon-[ix--robotic-arm]" : "icon-[mdi--robot-industrial]",
|
116 |
+
"mr-3 size-4 text-emerald-100 transition-colors duration-200 dark:text-emerald-200"
|
117 |
]}
|
118 |
></span>
|
119 |
<div class="flex flex-1 flex-col">
|
120 |
<span class="font-medium text-white transition-colors duration-200"
|
121 |
>{robotType.replace(/-/g, " ").toUpperCase()}</span
|
122 |
>
|
123 |
+
<span class="text-xs text-emerald-100 transition-colors duration-200 dark:text-emerald-200">
|
124 |
{robotType === "so-arm100" ? "6-DOF Robotic Arm" : "Industrial Robot"}
|
125 |
</span>
|
126 |
</div>
|
127 |
{#if robotType === "so-arm100"}
|
128 |
<Badge
|
129 |
variant="secondary"
|
130 |
+
class="ml-2 bg-emerald-600 text-xs text-emerald-100 group-data-highlighted:bg-emerald-300 group-data-highlighted:text-emerald-900 dark:bg-emerald-700 dark:text-emerald-100 dark:group-data-highlighted:bg-emerald-400 dark:group-data-highlighted:text-emerald-900"
|
131 |
>
|
132 |
Default
|
133 |
</Badge>
|
src/lib/components/interface/overlay/AddSensorButton.svelte
CHANGED
@@ -86,7 +86,7 @@
|
|
86 |
variant="default"
|
87 |
size="sm"
|
88 |
onclick={quickAddCamera}
|
89 |
-
class="group rounded-r-none border-0 bg-blue-
|
90 |
>
|
91 |
<span
|
92 |
class={[
|
@@ -106,8 +106,9 @@
|
|
106 |
variant="default"
|
107 |
size="sm"
|
108 |
class={cn(
|
109 |
-
"rounded-l-none border-0 border-l border-blue-
|
110 |
-
|
|
|
111 |
)}
|
112 |
>
|
113 |
<span
|
@@ -122,12 +123,12 @@
|
|
122 |
</DropdownMenu.Trigger>
|
123 |
|
124 |
<DropdownMenu.Content
|
125 |
-
class="w-56 border-blue-
|
126 |
align="center"
|
127 |
>
|
128 |
<DropdownMenu.Group>
|
129 |
<DropdownMenu.GroupHeading
|
130 |
-
class="text-xs font-semibold tracking-wider text-blue-
|
131 |
>
|
132 |
Sensor Types
|
133 |
</DropdownMenu.GroupHeading>
|
@@ -135,8 +136,8 @@
|
|
135 |
{#each sensorConfigs as sensor}
|
136 |
<DropdownMenu.Item
|
137 |
class={[
|
138 |
-
"group group cursor-pointer bg-blue-
|
139 |
-
"data-highlighted:bg-blue-700"
|
140 |
]}
|
141 |
disabled={!sensor.enabled}
|
142 |
onclick={async () => await addSensor(sensor.id)}
|
@@ -144,21 +145,21 @@
|
|
144 |
<span
|
145 |
class={[
|
146 |
sensor.icon,
|
147 |
-
"mr-3 size-4 text-blue-
|
148 |
]}
|
149 |
></span>
|
150 |
<div class="flex flex-1 flex-col">
|
151 |
<span class="font-medium text-white transition-colors duration-200"
|
152 |
>{sensor.label}</span
|
153 |
>
|
154 |
-
<span class="text-xs text-blue-
|
155 |
{sensor.description}
|
156 |
</span>
|
157 |
</div>
|
158 |
{#if sensor.isDefault}
|
159 |
<Badge
|
160 |
variant="secondary"
|
161 |
-
class="ml-2 bg-blue-
|
162 |
>
|
163 |
Default
|
164 |
</Badge>
|
|
|
86 |
variant="default"
|
87 |
size="sm"
|
88 |
onclick={quickAddCamera}
|
89 |
+
class="group rounded-r-none border-0 bg-blue-500 text-white transition-all duration-200 hover:bg-blue-400 dark:bg-blue-600 dark:hover:bg-blue-500"
|
90 |
>
|
91 |
<span
|
92 |
class={[
|
|
|
106 |
variant="default"
|
107 |
size="sm"
|
108 |
class={cn(
|
109 |
+
"rounded-l-none border-0 border-l border-blue-400/30 bg-blue-500 px-2 text-white transition-all duration-200 hover:bg-blue-400",
|
110 |
+
"dark:border-blue-500/30 dark:bg-blue-600 dark:hover:bg-blue-500",
|
111 |
+
open && "bg-blue-600 shadow-inner dark:bg-blue-700"
|
112 |
)}
|
113 |
>
|
114 |
<span
|
|
|
123 |
</DropdownMenu.Trigger>
|
124 |
|
125 |
<DropdownMenu.Content
|
126 |
+
class="w-56 border-blue-400/30 bg-blue-500 backdrop-blur-sm dark:border-blue-500/30 dark:bg-blue-600"
|
127 |
align="center"
|
128 |
>
|
129 |
<DropdownMenu.Group>
|
130 |
<DropdownMenu.GroupHeading
|
131 |
+
class="text-xs font-semibold tracking-wider text-blue-100 uppercase dark:text-blue-200"
|
132 |
>
|
133 |
Sensor Types
|
134 |
</DropdownMenu.GroupHeading>
|
|
|
136 |
{#each sensorConfigs as sensor}
|
137 |
<DropdownMenu.Item
|
138 |
class={[
|
139 |
+
"group group cursor-pointer bg-blue-500 text-white transition-all duration-200",
|
140 |
+
"data-highlighted:bg-blue-600 dark:bg-blue-600 dark:data-highlighted:bg-blue-700"
|
141 |
]}
|
142 |
disabled={!sensor.enabled}
|
143 |
onclick={async () => await addSensor(sensor.id)}
|
|
|
145 |
<span
|
146 |
class={[
|
147 |
sensor.icon,
|
148 |
+
"mr-3 size-4 text-blue-100 transition-colors duration-200 dark:text-blue-200"
|
149 |
]}
|
150 |
></span>
|
151 |
<div class="flex flex-1 flex-col">
|
152 |
<span class="font-medium text-white transition-colors duration-200"
|
153 |
>{sensor.label}</span
|
154 |
>
|
155 |
+
<span class="text-xs text-blue-100 transition-colors duration-200 dark:text-blue-200">
|
156 |
{sensor.description}
|
157 |
</span>
|
158 |
</div>
|
159 |
{#if sensor.isDefault}
|
160 |
<Badge
|
161 |
variant="secondary"
|
162 |
+
class="ml-2 bg-blue-600 text-xs text-blue-100 group-data-highlighted:bg-blue-300 group-data-highlighted:text-blue-900 dark:bg-blue-700 dark:text-blue-100 dark:group-data-highlighted:bg-blue-400 dark:group-data-highlighted:text-blue-900"
|
163 |
>
|
164 |
Default
|
165 |
</Badge>
|
src/lib/components/interface/overlay/Overlay.svelte
CHANGED
@@ -4,31 +4,38 @@
|
|
4 |
import AddAIButton from "@/components/interface/overlay/AddAIButton.svelte";
|
5 |
import SettingsButton from "@/components/interface/overlay/SettingsButton.svelte";
|
6 |
import SettingsSheet from "@/components/interface/overlay/SettingsSheet.svelte";
|
|
|
7 |
|
8 |
interface Props {
|
|
|
9 |
addRobotDropdownMenuOpen?: boolean;
|
10 |
addSensorDropdownMenuOpen?: boolean;
|
11 |
addAIDropdownMenuOpen?: boolean;
|
12 |
settingsOpen?: boolean;
|
|
|
13 |
}
|
14 |
|
15 |
let {
|
|
|
16 |
addRobotDropdownMenuOpen = $bindable(false),
|
17 |
addSensorDropdownMenuOpen = $bindable(false),
|
18 |
addAIDropdownMenuOpen = $bindable(false),
|
19 |
-
settingsOpen = $bindable(false)
|
|
|
20 |
}: Props = $props();
|
|
|
|
|
21 |
</script>
|
22 |
|
23 |
<div class="select-none">
|
24 |
<!-- Button Bar Container -->
|
25 |
<div class="fixed top-4 left-4 z-50 flex gap-2 select-none">
|
26 |
<!-- Add Robot Button Group -->
|
27 |
-
<div class="flex items-center justify-center gap-2 overflow-hidden rounded-lg
|
28 |
<!-- Logo/Favicon -->
|
29 |
-
<div class="flex items-center justify-center">
|
30 |
<!-- From /favicon_1024.png -->
|
31 |
-
<img src="/favicon_1024.png" alt="Logo" draggable="false" class="h-10 w-10 invert" />
|
32 |
</div>
|
33 |
<!-- Add robot button and dropdown menu (Top Left) -->
|
34 |
<div class="flex items-center justify-center">
|
@@ -37,7 +44,7 @@
|
|
37 |
</div>
|
38 |
|
39 |
<!-- Add Sensor Button Group -->
|
40 |
-
<div class="flex items-center justify-center overflow-hidden rounded-lg
|
41 |
<!-- Add sensor button and dropdown menu -->
|
42 |
<div class="flex items-center justify-center">
|
43 |
<AddSensorButton bind:open={addSensorDropdownMenuOpen} />
|
@@ -45,7 +52,7 @@
|
|
45 |
</div>
|
46 |
|
47 |
<!-- Add AI Button Group -->
|
48 |
-
<div class="flex items-center justify-center overflow-hidden rounded-lg
|
49 |
<!-- Add AI button and dropdown menu -->
|
50 |
<div class="flex items-center justify-center">
|
51 |
<AddAIButton bind:open={addAIDropdownMenuOpen} />
|
@@ -53,7 +60,10 @@
|
|
53 |
</div>
|
54 |
</div>
|
55 |
|
56 |
-
<div class="fixed top-4 right-4 z-50">
|
|
|
|
|
|
|
57 |
<!-- Settings Button and Sheet (Top Right, Left Side) -->
|
58 |
<SettingsButton bind:open={settingsOpen} />
|
59 |
</div>
|
|
|
4 |
import AddAIButton from "@/components/interface/overlay/AddAIButton.svelte";
|
5 |
import SettingsButton from "@/components/interface/overlay/SettingsButton.svelte";
|
6 |
import SettingsSheet from "@/components/interface/overlay/SettingsSheet.svelte";
|
7 |
+
import WorkspaceIdButton from "@/components/interface/overlay/WorkspaceIdButton.svelte";
|
8 |
|
9 |
interface Props {
|
10 |
+
workspaceId: string;
|
11 |
addRobotDropdownMenuOpen?: boolean;
|
12 |
addSensorDropdownMenuOpen?: boolean;
|
13 |
addAIDropdownMenuOpen?: boolean;
|
14 |
settingsOpen?: boolean;
|
15 |
+
workspaceIdMenuOpen?: boolean;
|
16 |
}
|
17 |
|
18 |
let {
|
19 |
+
workspaceId,
|
20 |
addRobotDropdownMenuOpen = $bindable(false),
|
21 |
addSensorDropdownMenuOpen = $bindable(false),
|
22 |
addAIDropdownMenuOpen = $bindable(false),
|
23 |
+
settingsOpen = $bindable(false),
|
24 |
+
workspaceIdMenuOpen = $bindable(false)
|
25 |
}: Props = $props();
|
26 |
+
|
27 |
+
|
28 |
</script>
|
29 |
|
30 |
<div class="select-none">
|
31 |
<!-- Button Bar Container -->
|
32 |
<div class="fixed top-4 left-4 z-50 flex gap-2 select-none">
|
33 |
<!-- Add Robot Button Group -->
|
34 |
+
<div class="flex items-center justify-center gap-2 overflow-hidden rounded-lg ">
|
35 |
<!-- Logo/Favicon -->
|
36 |
+
<div class="flex items-center justify-center ">
|
37 |
<!-- From /favicon_1024.png -->
|
38 |
+
<img src="/favicon_1024.png" alt="Logo" draggable="false" class="h-10 w-10 filter dark:invert invert-0" />
|
39 |
</div>
|
40 |
<!-- Add robot button and dropdown menu (Top Left) -->
|
41 |
<div class="flex items-center justify-center">
|
|
|
44 |
</div>
|
45 |
|
46 |
<!-- Add Sensor Button Group -->
|
47 |
+
<div class="flex items-center justify-center overflow-hidden rounded-lg ">
|
48 |
<!-- Add sensor button and dropdown menu -->
|
49 |
<div class="flex items-center justify-center">
|
50 |
<AddSensorButton bind:open={addSensorDropdownMenuOpen} />
|
|
|
52 |
</div>
|
53 |
|
54 |
<!-- Add AI Button Group -->
|
55 |
+
<div class="flex items-center justify-center overflow-hidden rounded-lg ">
|
56 |
<!-- Add AI button and dropdown menu -->
|
57 |
<div class="flex items-center justify-center">
|
58 |
<AddAIButton bind:open={addAIDropdownMenuOpen} />
|
|
|
60 |
</div>
|
61 |
</div>
|
62 |
|
63 |
+
<div class="fixed top-4 right-4 z-50 flex gap-2">
|
64 |
+
<!-- Workspace ID Button -->
|
65 |
+
<WorkspaceIdButton {workspaceId} bind:open={workspaceIdMenuOpen} />
|
66 |
+
|
67 |
<!-- Settings Button and Sheet (Top Right, Left Side) -->
|
68 |
<SettingsButton bind:open={settingsOpen} />
|
69 |
</div>
|
src/lib/components/interface/overlay/SettingsButton.svelte
CHANGED
@@ -19,7 +19,7 @@
|
|
19 |
variant="default"
|
20 |
size="sm"
|
21 |
onclick={() => (open = !open)}
|
22 |
-
class="group w-32 border-0 bg-orange-
|
23 |
>
|
24 |
<span
|
25 |
class={[
|
|
|
19 |
variant="default"
|
20 |
size="sm"
|
21 |
onclick={() => (open = !open)}
|
22 |
+
class="group w-32 border-0 bg-orange-500 text-white transition-all duration-200 hover:bg-orange-400 dark:bg-orange-600 dark:hover:bg-orange-500"
|
23 |
>
|
24 |
<span
|
25 |
class={[
|
src/lib/components/interface/overlay/SettingsSheet.svelte
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
<script lang="ts">
|
2 |
import * as Sheet from "@/components/ui/sheet";
|
3 |
import { Separator } from "@/components/ui/separator";
|
|
|
4 |
import { Switch } from "@/components/ui/switch";
|
5 |
import { Label } from "@/components/ui/label";
|
6 |
import { Input } from "@/components/ui/input";
|
@@ -16,14 +17,12 @@
|
|
16 |
let { open = $bindable(false) }: Props = $props();
|
17 |
|
18 |
interface LocalSettings {
|
19 |
-
darkMode: boolean;
|
20 |
showFPS: boolean;
|
21 |
renderQuality: number;
|
22 |
animationSpeed: number;
|
23 |
}
|
24 |
|
25 |
let localSettings = $state<LocalSettings>({
|
26 |
-
darkMode: true,
|
27 |
showFPS: false,
|
28 |
renderQuality: 75,
|
29 |
animationSpeed: 1
|
@@ -89,16 +88,16 @@
|
|
89 |
<Sheet.Root bind:open>
|
90 |
<Sheet.Content
|
91 |
side="left"
|
92 |
-
class="w-80 gap-0 border-r border-slate-
|
93 |
>
|
94 |
<!-- Header -->
|
95 |
-
<Sheet.Header class="border-b border-slate-
|
96 |
<div class="flex items-center justify-between">
|
97 |
<div class="flex items-center gap-3">
|
98 |
-
<span class="icon-[mdi--cog] size-6 text-orange-400"></span>
|
99 |
<div>
|
100 |
-
<Sheet.Title class="text-xl font-semibold text-slate-100">Robot Settings</Sheet.Title>
|
101 |
-
<p class="mt-1 text-sm text-slate-400">Configure application preferences</p>
|
102 |
</div>
|
103 |
</div>
|
104 |
</div>
|
@@ -106,21 +105,21 @@
|
|
106 |
|
107 |
<!-- Content -->
|
108 |
<div
|
109 |
-
class="scrollbar-thin scrollbar-track-slate-700 scrollbar-thumb-slate-500 flex-1 overflow-y-auto px-4"
|
110 |
>
|
111 |
<div class="space-y-6 py-4">
|
112 |
<!-- Server Configuration -->
|
113 |
<div class="space-y-4">
|
114 |
<div class="mb-3 flex items-center gap-3">
|
115 |
-
<span class="icon-[mdi--server] size-5 text-blue-400"></span>
|
116 |
-
<h3 class="text-lg font-medium text-slate-100">Server Configuration</h3>
|
117 |
</div>
|
118 |
|
119 |
<div class="space-y-4">
|
120 |
<div class="space-y-3">
|
121 |
<div class="space-y-1">
|
122 |
-
<Label class="text-sm font-medium text-slate-200">Inference Server URL</Label>
|
123 |
-
<p class="text-xs text-slate-400">
|
124 |
URL for the remote AI inference server that runs ACT models and manages robot sessions
|
125 |
</p>
|
126 |
</div>
|
@@ -128,14 +127,14 @@
|
|
128 |
<Input
|
129 |
bind:value={settings.inferenceServerUrl}
|
130 |
placeholder="http://localhost:8001"
|
131 |
-
class="flex-1 bg-slate-800 border-slate-600 text-slate-100"
|
132 |
/>
|
133 |
<Button
|
134 |
variant="outline"
|
135 |
size="sm"
|
136 |
onclick={checkInferenceServerConnection}
|
137 |
disabled={isCheckingInferenceConnection}
|
138 |
-
class="border-slate-600 text-slate-200 hover:bg-slate-700"
|
139 |
>
|
140 |
{#if isCheckingInferenceConnection}
|
141 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-4"></span>
|
@@ -148,22 +147,22 @@
|
|
148 |
</div>
|
149 |
<div class="flex items-center gap-2">
|
150 |
{#if inferenceConnectionStatus === 'connected'}
|
151 |
-
<span class="icon-[mdi--check-circle] size-4 text-green-400"></span>
|
152 |
-
<span class="text-xs text-green-400">Connected to inference server</span>
|
153 |
{:else if inferenceConnectionStatus === 'disconnected'}
|
154 |
-
<span class="icon-[mdi--close-circle] size-4 text-red-400"></span>
|
155 |
-
<span class="text-xs text-red-400">Cannot connect to inference server</span>
|
156 |
{:else}
|
157 |
-
<span class="icon-[mdi--help-circle] size-4 text-slate-400"></span>
|
158 |
-
<span class="text-xs text-slate-400">Connection status unknown</span>
|
159 |
{/if}
|
160 |
</div>
|
161 |
</div>
|
162 |
|
163 |
<div class="space-y-3">
|
164 |
<div class="space-y-1">
|
165 |
-
<Label class="text-sm font-medium text-slate-200">Transport Server URL</Label>
|
166 |
-
<p class="text-xs text-slate-400">
|
167 |
URL for the transport server that manages communication rooms and routes video streams and robot data using consumer/producer system
|
168 |
</p>
|
169 |
</div>
|
@@ -171,14 +170,14 @@
|
|
171 |
<Input
|
172 |
bind:value={settings.transportServerUrl}
|
173 |
placeholder="http://localhost:8000"
|
174 |
-
class="flex-1 bg-slate-800 border-slate-600 text-slate-100"
|
175 |
/>
|
176 |
<Button
|
177 |
variant="outline"
|
178 |
size="sm"
|
179 |
onclick={checkTransportServerConnection}
|
180 |
disabled={isCheckingTransportConnection}
|
181 |
-
class="border-slate-600 text-slate-200 hover:bg-slate-700"
|
182 |
>
|
183 |
{#if isCheckingTransportConnection}
|
184 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-4"></span>
|
@@ -191,50 +190,53 @@
|
|
191 |
</div>
|
192 |
<div class="flex items-center gap-2">
|
193 |
{#if transportConnectionStatus === 'connected'}
|
194 |
-
<span class="icon-[mdi--check-circle] size-4 text-green-400"></span>
|
195 |
-
<span class="text-xs text-green-400">Connected to transport server</span>
|
196 |
{:else if transportConnectionStatus === 'disconnected'}
|
197 |
-
<span class="icon-[mdi--close-circle] size-4 text-red-400"></span>
|
198 |
-
<span class="text-xs text-red-400">Cannot connect to transport server</span>
|
199 |
{:else}
|
200 |
-
<span class="icon-[mdi--help-circle] size-4 text-slate-400"></span>
|
201 |
-
<span class="text-xs text-slate-400">Connection status unknown</span>
|
202 |
{/if}
|
203 |
</div>
|
204 |
</div>
|
205 |
</div>
|
206 |
</div>
|
207 |
|
208 |
-
<Separator class="bg-slate-600" />
|
209 |
|
210 |
<!-- Application Settings -->
|
211 |
<div class="space-y-4">
|
212 |
<div class="mb-3 flex items-center gap-3">
|
213 |
-
<span class="icon-[mdi--tune] size-5 text-orange-400"></span>
|
214 |
-
<h3 class="text-lg font-medium text-slate-100">Application Settings</h3>
|
215 |
</div>
|
216 |
|
217 |
-
|
218 |
<div class="flex items-center justify-between">
|
219 |
<div class="space-y-1">
|
220 |
-
<Label class="text-sm font-medium text-slate-200">Dark Mode</Label>
|
221 |
-
<p class="text-xs text-slate-400">Use dark theme for the interface</p>
|
222 |
</div>
|
223 |
-
<Switch
|
|
|
|
|
|
|
224 |
</div>
|
225 |
|
226 |
-
<div class="flex items-center justify-between">
|
227 |
<div class="space-y-1">
|
228 |
-
<Label class="text-sm font-medium text-slate-200">Show FPS</Label>
|
229 |
-
<p class="text-xs text-slate-400">Display frame rate counter</p>
|
230 |
</div>
|
231 |
<Switch bind:checked={localSettings.showFPS} />
|
232 |
</div>
|
233 |
|
234 |
<div class="space-y-3">
|
235 |
<div class="space-y-1">
|
236 |
-
<Label class="text-sm font-medium text-slate-200">Render Quality</Label>
|
237 |
-
<p class="text-xs text-slate-400">
|
238 |
Adjust 3D rendering quality ({localSettings.renderQuality}%)
|
239 |
</p>
|
240 |
</div>
|
@@ -244,7 +246,7 @@
|
|
244 |
max="100"
|
245 |
step="5"
|
246 |
bind:value={localSettings.renderQuality}
|
247 |
-
class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-600"
|
248 |
/>
|
249 |
<div class="flex justify-between text-xs text-slate-500">
|
250 |
<span>25%</span>
|
@@ -252,11 +254,10 @@
|
|
252 |
</div>
|
253 |
</div>
|
254 |
|
255 |
-
|
256 |
<div class="space-y-3">
|
257 |
<div class="space-y-1">
|
258 |
-
<Label class="text-sm font-medium text-slate-200">Animation Speed</Label>
|
259 |
-
<p class="text-xs text-slate-400">
|
260 |
Robot movement animation speed ({localSettings.animationSpeed}x)
|
261 |
</p>
|
262 |
</div>
|
@@ -266,29 +267,29 @@
|
|
266 |
max="3"
|
267 |
step="0.1"
|
268 |
bind:value={localSettings.animationSpeed}
|
269 |
-
class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-600"
|
270 |
/>
|
271 |
<div class="flex justify-between text-xs text-slate-500">
|
272 |
<span>0.1x</span>
|
273 |
<span>3x</span>
|
274 |
</div>
|
275 |
-
</div>
|
276 |
-
</div>
|
277 |
</div>
|
278 |
|
279 |
-
<Separator class="bg-slate-600" />
|
280 |
|
281 |
<!-- About Section -->
|
282 |
<div class="space-y-4">
|
283 |
<div class="mb-3 flex items-center gap-3">
|
284 |
-
<span class="icon-[mdi--information-outline] size-5 text-blue-400"></span>
|
285 |
-
<h3 class="text-lg font-medium text-slate-100">About</h3>
|
286 |
</div>
|
287 |
|
288 |
<div class="space-y-4">
|
289 |
<div class="space-y-2">
|
290 |
-
<h4 class="text-sm font-medium text-slate-200">Acknowledgements</h4>
|
291 |
-
<p class="text-xs leading-relaxed text-slate-400">
|
292 |
This application is built with amazing open-source technologies and communities.
|
293 |
</p>
|
294 |
</div>
|
@@ -299,12 +300,12 @@
|
|
299 |
href="https://github.com/huggingface/lerobot"
|
300 |
target="_blank"
|
301 |
rel="noopener noreferrer"
|
302 |
-
class="flex items-center gap-3 rounded-lg border border-slate-
|
303 |
>
|
304 |
-
<span class="icon-[mdi--robot] size-5 text-yellow-400"></span>
|
305 |
<div class="flex-1">
|
306 |
-
<div class="text-sm font-medium text-slate-200">Hugging Face LeRobot</div>
|
307 |
-
<p class="text-xs text-slate-400">Robotics AI framework and models</p>
|
308 |
</div>
|
309 |
</a>
|
310 |
|
@@ -313,12 +314,12 @@
|
|
313 |
href="https://threlte.xyz"
|
314 |
target="_blank"
|
315 |
rel="noopener noreferrer"
|
316 |
-
class="flex items-center gap-3 rounded-lg border border-slate-
|
317 |
>
|
318 |
-
<span class="icon-[mdi--cube-outline] size-5 text-orange-400"></span>
|
319 |
<div class="flex-1">
|
320 |
-
<div class="text-sm font-medium text-slate-200">Threlte</div>
|
321 |
-
<p class="text-xs text-slate-400">3D graphics library for Svelte</p>
|
322 |
</div>
|
323 |
</a>
|
324 |
|
@@ -327,12 +328,16 @@
|
|
327 |
href="https://bambot.org"
|
328 |
target="_blank"
|
329 |
rel="noopener noreferrer"
|
330 |
-
class="flex items-center gap-3 rounded-lg border border-slate-
|
331 |
>
|
332 |
-
<span class="icon-[mdi--memory] size-5 text-green-400"></span>
|
333 |
<div class="flex-1">
|
334 |
-
<div class="text-sm font-medium text-slate-200">bambot.org feetech.js</div>
|
335 |
-
<p class="text-xs text-slate-400">
|
|
|
|
|
|
|
|
|
336 |
</div>
|
337 |
</a>
|
338 |
|
@@ -341,12 +346,12 @@
|
|
341 |
href="https://github.com/brean/urdf-viewer"
|
342 |
target="_blank"
|
343 |
rel="noopener noreferrer"
|
344 |
-
class="flex items-center gap-3 rounded-lg border border-slate-
|
345 |
>
|
346 |
-
<span class="icon-[mdi--cube-outline] size-5 text-orange-400"></span>
|
347 |
<div class="flex-1">
|
348 |
-
<div class="text-sm font-medium text-slate-200">URDF Viewer</div>
|
349 |
-
<p class="text-xs text-slate-400">
|
350 |
Nice component for viewing URDF models with Threlte
|
351 |
</p>
|
352 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
import * as Sheet from "@/components/ui/sheet";
|
3 |
import { Separator } from "@/components/ui/separator";
|
4 |
+
import { setMode, mode } from "mode-watcher";
|
5 |
import { Switch } from "@/components/ui/switch";
|
6 |
import { Label } from "@/components/ui/label";
|
7 |
import { Input } from "@/components/ui/input";
|
|
|
17 |
let { open = $bindable(false) }: Props = $props();
|
18 |
|
19 |
interface LocalSettings {
|
|
|
20 |
showFPS: boolean;
|
21 |
renderQuality: number;
|
22 |
animationSpeed: number;
|
23 |
}
|
24 |
|
25 |
let localSettings = $state<LocalSettings>({
|
|
|
26 |
showFPS: false,
|
27 |
renderQuality: 75,
|
28 |
animationSpeed: 1
|
|
|
88 |
<Sheet.Root bind:open>
|
89 |
<Sheet.Content
|
90 |
side="left"
|
91 |
+
class="w-80 gap-0 border-r border-slate-300 bg-gradient-to-b from-slate-100 to-slate-200 p-0 text-slate-900 dark:border-slate-600 dark:bg-gradient-to-b dark:from-slate-700 dark:to-slate-800 dark:text-white sm:w-96"
|
92 |
>
|
93 |
<!-- Header -->
|
94 |
+
<Sheet.Header class="border-b border-slate-300 bg-slate-200/80 p-6 backdrop-blur-sm dark:border-slate-600 dark:bg-slate-700/80">
|
95 |
<div class="flex items-center justify-between">
|
96 |
<div class="flex items-center gap-3">
|
97 |
+
<span class="icon-[mdi--cog] size-6 text-orange-500 dark:text-orange-400"></span>
|
98 |
<div>
|
99 |
+
<Sheet.Title class="text-xl font-semibold text-slate-900 dark:text-slate-100">Robot Settings</Sheet.Title>
|
100 |
+
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">Configure application preferences</p>
|
101 |
</div>
|
102 |
</div>
|
103 |
</div>
|
|
|
105 |
|
106 |
<!-- Content -->
|
107 |
<div
|
108 |
+
class="scrollbar-thin scrollbar-track-slate-300 scrollbar-thumb-slate-400 dark:scrollbar-track-slate-700 dark:scrollbar-thumb-slate-500 flex-1 overflow-y-auto px-4"
|
109 |
>
|
110 |
<div class="space-y-6 py-4">
|
111 |
<!-- Server Configuration -->
|
112 |
<div class="space-y-4">
|
113 |
<div class="mb-3 flex items-center gap-3">
|
114 |
+
<span class="icon-[mdi--server] size-5 text-blue-500 dark:text-blue-400"></span>
|
115 |
+
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100">Server Configuration</h3>
|
116 |
</div>
|
117 |
|
118 |
<div class="space-y-4">
|
119 |
<div class="space-y-3">
|
120 |
<div class="space-y-1">
|
121 |
+
<Label class="text-sm font-medium text-slate-800 dark:text-slate-200">Inference Server URL</Label>
|
122 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
123 |
URL for the remote AI inference server that runs ACT models and manages robot sessions
|
124 |
</p>
|
125 |
</div>
|
|
|
127 |
<Input
|
128 |
bind:value={settings.inferenceServerUrl}
|
129 |
placeholder="http://localhost:8001"
|
130 |
+
class="flex-1 bg-slate-100 border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100"
|
131 |
/>
|
132 |
<Button
|
133 |
variant="outline"
|
134 |
size="sm"
|
135 |
onclick={checkInferenceServerConnection}
|
136 |
disabled={isCheckingInferenceConnection}
|
137 |
+
class="border-slate-300 text-slate-700 hover:bg-slate-200 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700"
|
138 |
>
|
139 |
{#if isCheckingInferenceConnection}
|
140 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-4"></span>
|
|
|
147 |
</div>
|
148 |
<div class="flex items-center gap-2">
|
149 |
{#if inferenceConnectionStatus === 'connected'}
|
150 |
+
<span class="icon-[mdi--check-circle] size-4 text-green-500 dark:text-green-400"></span>
|
151 |
+
<span class="text-xs text-green-600 dark:text-green-400">Connected to inference server</span>
|
152 |
{:else if inferenceConnectionStatus === 'disconnected'}
|
153 |
+
<span class="icon-[mdi--close-circle] size-4 text-red-500 dark:text-red-400"></span>
|
154 |
+
<span class="text-xs text-red-600 dark:text-red-400">Cannot connect to inference server</span>
|
155 |
{:else}
|
156 |
+
<span class="icon-[mdi--help-circle] size-4 text-slate-500 dark:text-slate-400"></span>
|
157 |
+
<span class="text-xs text-slate-600 dark:text-slate-400">Connection status unknown</span>
|
158 |
{/if}
|
159 |
</div>
|
160 |
</div>
|
161 |
|
162 |
<div class="space-y-3">
|
163 |
<div class="space-y-1">
|
164 |
+
<Label class="text-sm font-medium text-slate-800 dark:text-slate-200">Transport Server URL</Label>
|
165 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
166 |
URL for the transport server that manages communication rooms and routes video streams and robot data using consumer/producer system
|
167 |
</p>
|
168 |
</div>
|
|
|
170 |
<Input
|
171 |
bind:value={settings.transportServerUrl}
|
172 |
placeholder="http://localhost:8000"
|
173 |
+
class="flex-1 bg-slate-100 border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100"
|
174 |
/>
|
175 |
<Button
|
176 |
variant="outline"
|
177 |
size="sm"
|
178 |
onclick={checkTransportServerConnection}
|
179 |
disabled={isCheckingTransportConnection}
|
180 |
+
class="border-slate-300 text-slate-700 hover:bg-slate-200 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700"
|
181 |
>
|
182 |
{#if isCheckingTransportConnection}
|
183 |
<span class="icon-[mdi--loading] animate-spin mr-1 size-4"></span>
|
|
|
190 |
</div>
|
191 |
<div class="flex items-center gap-2">
|
192 |
{#if transportConnectionStatus === 'connected'}
|
193 |
+
<span class="icon-[mdi--check-circle] size-4 text-green-500 dark:text-green-400"></span>
|
194 |
+
<span class="text-xs text-green-600 dark:text-green-400">Connected to transport server</span>
|
195 |
{:else if transportConnectionStatus === 'disconnected'}
|
196 |
+
<span class="icon-[mdi--close-circle] size-4 text-red-500 dark:text-red-400"></span>
|
197 |
+
<span class="text-xs text-red-600 dark:text-red-400">Cannot connect to transport server</span>
|
198 |
{:else}
|
199 |
+
<span class="icon-[mdi--help-circle] size-4 text-slate-500 dark:text-slate-400"></span>
|
200 |
+
<span class="text-xs text-slate-600 dark:text-slate-400">Connection status unknown</span>
|
201 |
{/if}
|
202 |
</div>
|
203 |
</div>
|
204 |
</div>
|
205 |
</div>
|
206 |
|
207 |
+
<Separator class="bg-slate-300 dark:bg-slate-600" />
|
208 |
|
209 |
<!-- Application Settings -->
|
210 |
<div class="space-y-4">
|
211 |
<div class="mb-3 flex items-center gap-3">
|
212 |
+
<span class="icon-[mdi--tune] size-5 text-orange-500 dark:text-orange-400"></span>
|
213 |
+
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100">Application Settings</h3>
|
214 |
</div>
|
215 |
|
216 |
+
<div class="space-y-4">
|
217 |
<div class="flex items-center justify-between">
|
218 |
<div class="space-y-1">
|
219 |
+
<Label class="text-sm font-medium text-slate-800 dark:text-slate-200">Dark Mode</Label>
|
220 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">Use dark theme for the interface</p>
|
221 |
</div>
|
222 |
+
<Switch
|
223 |
+
checked={mode.current === 'dark'}
|
224 |
+
onCheckedChange={(checked) => setMode(checked ? 'dark' : 'light')}
|
225 |
+
/>
|
226 |
</div>
|
227 |
|
228 |
+
<!-- <div class="flex items-center justify-between">
|
229 |
<div class="space-y-1">
|
230 |
+
<Label class="text-sm font-medium text-slate-800 dark:text-slate-200">Show FPS</Label>
|
231 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">Display frame rate counter</p>
|
232 |
</div>
|
233 |
<Switch bind:checked={localSettings.showFPS} />
|
234 |
</div>
|
235 |
|
236 |
<div class="space-y-3">
|
237 |
<div class="space-y-1">
|
238 |
+
<Label class="text-sm font-medium text-slate-800 dark:text-slate-200">Render Quality</Label>
|
239 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
240 |
Adjust 3D rendering quality ({localSettings.renderQuality}%)
|
241 |
</p>
|
242 |
</div>
|
|
|
246 |
max="100"
|
247 |
step="5"
|
248 |
bind:value={localSettings.renderQuality}
|
249 |
+
class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-300 dark:bg-slate-600"
|
250 |
/>
|
251 |
<div class="flex justify-between text-xs text-slate-500">
|
252 |
<span>25%</span>
|
|
|
254 |
</div>
|
255 |
</div>
|
256 |
|
|
|
257 |
<div class="space-y-3">
|
258 |
<div class="space-y-1">
|
259 |
+
<Label class="text-sm font-medium text-slate-800 dark:text-slate-200">Animation Speed</Label>
|
260 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
261 |
Robot movement animation speed ({localSettings.animationSpeed}x)
|
262 |
</p>
|
263 |
</div>
|
|
|
267 |
max="3"
|
268 |
step="0.1"
|
269 |
bind:value={localSettings.animationSpeed}
|
270 |
+
class="slider h-2 w-full cursor-pointer appearance-none rounded-lg bg-slate-300 dark:bg-slate-600"
|
271 |
/>
|
272 |
<div class="flex justify-between text-xs text-slate-500">
|
273 |
<span>0.1x</span>
|
274 |
<span>3x</span>
|
275 |
</div>
|
276 |
+
</div> -->
|
277 |
+
</div>
|
278 |
</div>
|
279 |
|
280 |
+
<Separator class="bg-slate-300 dark:bg-slate-600" />
|
281 |
|
282 |
<!-- About Section -->
|
283 |
<div class="space-y-4">
|
284 |
<div class="mb-3 flex items-center gap-3">
|
285 |
+
<span class="icon-[mdi--information-outline] size-5 text-blue-500 dark:text-blue-400"></span>
|
286 |
+
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100">About</h3>
|
287 |
</div>
|
288 |
|
289 |
<div class="space-y-4">
|
290 |
<div class="space-y-2">
|
291 |
+
<h4 class="text-sm font-medium text-slate-800 dark:text-slate-200">Acknowledgements</h4>
|
292 |
+
<p class="text-xs leading-relaxed text-slate-600 dark:text-slate-400">
|
293 |
This application is built with amazing open-source technologies and communities.
|
294 |
</p>
|
295 |
</div>
|
|
|
300 |
href="https://github.com/huggingface/lerobot"
|
301 |
target="_blank"
|
302 |
rel="noopener noreferrer"
|
303 |
+
class="flex items-center gap-3 rounded-lg border border-slate-300 bg-slate-100/50 p-3 transition-colors duration-200 hover:bg-slate-200/50 dark:border-slate-600 dark:bg-slate-800/50 dark:hover:bg-slate-700/50"
|
304 |
>
|
305 |
+
<span class="icon-[mdi--robot] size-5 text-yellow-500 dark:text-yellow-400"></span>
|
306 |
<div class="flex-1">
|
307 |
+
<div class="text-sm font-medium text-slate-800 dark:text-slate-200">Hugging Face LeRobot</div>
|
308 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">Robotics AI framework and models</p>
|
309 |
</div>
|
310 |
</a>
|
311 |
|
|
|
314 |
href="https://threlte.xyz"
|
315 |
target="_blank"
|
316 |
rel="noopener noreferrer"
|
317 |
+
class="flex items-center gap-3 rounded-lg border border-slate-300 bg-slate-100/50 p-3 transition-colors duration-200 hover:bg-slate-200/50 dark:border-slate-600 dark:bg-slate-800/50 dark:hover:bg-slate-700/50"
|
318 |
>
|
319 |
+
<span class="icon-[mdi--cube-outline] size-5 text-orange-500 dark:text-orange-400"></span>
|
320 |
<div class="flex-1">
|
321 |
+
<div class="text-sm font-medium text-slate-800 dark:text-slate-200">Threlte</div>
|
322 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">3D graphics library for Svelte</p>
|
323 |
</div>
|
324 |
</a>
|
325 |
|
|
|
328 |
href="https://bambot.org"
|
329 |
target="_blank"
|
330 |
rel="noopener noreferrer"
|
331 |
+
class="flex items-center gap-3 rounded-lg border border-slate-300 bg-slate-100/50 p-3 transition-colors duration-200 hover:bg-slate-200/50 dark:border-slate-600 dark:bg-slate-800/50 dark:hover:bg-slate-700/50"
|
332 |
>
|
333 |
+
<span class="icon-[mdi--memory] size-5 text-green-500 dark:text-green-400"></span>
|
334 |
<div class="flex-1">
|
335 |
+
<div class="text-sm font-medium text-slate-800 dark:text-slate-200">bambot.org feetech.js</div>
|
336 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
337 |
+
Amazing project by Tim Qian (https://x.com/tim_qian).
|
338 |
+
Most of the USB control part (feetech.js) comes from this project.
|
339 |
+
Thanks to Tim for sharing his work!
|
340 |
+
</p>
|
341 |
</div>
|
342 |
</a>
|
343 |
|
|
|
346 |
href="https://github.com/brean/urdf-viewer"
|
347 |
target="_blank"
|
348 |
rel="noopener noreferrer"
|
349 |
+
class="flex items-center gap-3 rounded-lg border border-slate-300 bg-slate-100/50 p-3 transition-colors duration-200 hover:bg-slate-200/50 dark:border-slate-600 dark:bg-slate-800/50 dark:hover:bg-slate-700/50"
|
350 |
>
|
351 |
+
<span class="icon-[mdi--cube-outline] size-5 text-orange-500 dark:text-orange-400"></span>
|
352 |
<div class="flex-1">
|
353 |
+
<div class="text-sm font-medium text-slate-800 dark:text-slate-200">URDF Viewer</div>
|
354 |
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
355 |
Nice component for viewing URDF models with Threlte
|
356 |
</p>
|
357 |
</div>
|
src/lib/elements/compute/README.md
CHANGED
@@ -26,7 +26,7 @@ import { remoteComputeManager } from '$lib/elements/compute/';
|
|
26 |
// Create a new compute instance
|
27 |
const compute = remoteComputeManager.createCompute('my-compute', 'ACT Model');
|
28 |
|
29 |
-
// Create an
|
30 |
await remoteComputeManager.createSession(compute.id, {
|
31 |
sessionId: 'my-session',
|
32 |
policyPath: './checkpoints/act_so101_beyond',
|
@@ -73,7 +73,7 @@ The system integrates with the AI server backend (`backend/ai-server/`) which pr
|
|
73 |
### Modal Dialog
|
74 |
|
75 |
`AISessionConnectionModal.svelte` provides a comprehensive interface for:
|
76 |
-
- Creating new
|
77 |
- Managing existing sessions (start, stop, delete)
|
78 |
- Viewing session status and connection details
|
79 |
- Real-time session monitoring
|
@@ -99,7 +99,7 @@ The status system shows input/output connections:
|
|
99 |
// 1. Create a compute instance
|
100 |
const compute = remoteComputeManager.createCompute();
|
101 |
|
102 |
-
// 2. Configure and create
|
103 |
await remoteComputeManager.createSession(compute.id, {
|
104 |
sessionId: 'robot-control-01',
|
105 |
policyPath: './checkpoints/act_so101_beyond',
|
|
|
26 |
// Create a new compute instance
|
27 |
const compute = remoteComputeManager.createCompute('my-compute', 'ACT Model');
|
28 |
|
29 |
+
// Create an Inference Session
|
30 |
await remoteComputeManager.createSession(compute.id, {
|
31 |
sessionId: 'my-session',
|
32 |
policyPath: './checkpoints/act_so101_beyond',
|
|
|
73 |
### Modal Dialog
|
74 |
|
75 |
`AISessionConnectionModal.svelte` provides a comprehensive interface for:
|
76 |
+
- Creating new Inference Sessions with configurable parameters
|
77 |
- Managing existing sessions (start, stop, delete)
|
78 |
- Viewing session status and connection details
|
79 |
- Real-time session monitoring
|
|
|
99 |
// 1. Create a compute instance
|
100 |
const compute = remoteComputeManager.createCompute();
|
101 |
|
102 |
+
// 2. Configure and create Inference Session
|
103 |
await remoteComputeManager.createSession(compute.id, {
|
104 |
sessionId: 'robot-control-01',
|
105 |
policyPath: './checkpoints/act_so101_beyond',
|
src/lib/elements/compute/RemoteComputeManager.svelte.ts
CHANGED
@@ -136,7 +136,7 @@ export class RemoteComputeManager {
|
|
136 |
}
|
137 |
|
138 |
/**
|
139 |
-
* Create an
|
140 |
*/
|
141 |
async createSession(computeId: string, config: AISessionConfig): Promise<{ success: boolean; error?: string; data?: AISessionResponse }> {
|
142 |
const compute = this.getCompute(computeId);
|
|
|
136 |
}
|
137 |
|
138 |
/**
|
139 |
+
* Create an Inference Session
|
140 |
*/
|
141 |
async createSession(computeId: string, config: AISessionConfig): Promise<{ success: boolean; error?: string; data?: AISessionResponse }> {
|
142 |
const compute = this.getCompute(computeId);
|
src/lib/elements/robot/Robot.svelte.ts
CHANGED
@@ -257,7 +257,7 @@ export class Robot implements Positionable {
|
|
257 |
return this._setConsumer(config, false);
|
258 |
}
|
259 |
|
260 |
-
// Join existing room as consumer (for
|
261 |
async joinAsConsumer(config: RemoteDriverConfig): Promise<string> {
|
262 |
if (config.type !== 'remote') {
|
263 |
throw new Error('joinAsConsumer only supports remote drivers');
|
@@ -308,7 +308,7 @@ export class Robot implements Positionable {
|
|
308 |
return this._addProducer(config, false);
|
309 |
}
|
310 |
|
311 |
-
// Join existing room as producer (for
|
312 |
async joinAsProducer(config: RemoteDriverConfig): Promise<string> {
|
313 |
if (config.type !== 'remote') {
|
314 |
throw new Error('joinAsProducer only supports remote drivers');
|
|
|
257 |
return this._setConsumer(config, false);
|
258 |
}
|
259 |
|
260 |
+
// Join existing room as consumer (for Inference Session integration)
|
261 |
async joinAsConsumer(config: RemoteDriverConfig): Promise<string> {
|
262 |
if (config.type !== 'remote') {
|
263 |
throw new Error('joinAsConsumer only supports remote drivers');
|
|
|
308 |
return this._addProducer(config, false);
|
309 |
}
|
310 |
|
311 |
+
// Join existing room as producer (for Inference Session integration)
|
312 |
async joinAsProducer(config: RemoteDriverConfig): Promise<string> {
|
313 |
if (config.type !== 'remote') {
|
314 |
throw new Error('joinAsProducer only supports remote drivers');
|
src/lib/elements/robot/drivers/RemoteConsumer.ts
CHANGED
@@ -88,7 +88,7 @@ export class RemoteConsumer implements Consumer {
|
|
88 |
|
89 |
let roomData;
|
90 |
if (joinExistingRoom) {
|
91 |
-
// Join existing room (for
|
92 |
roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId };
|
93 |
console.log(`[RemoteConsumer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`);
|
94 |
} else {
|
|
|
88 |
|
89 |
let roomData;
|
90 |
if (joinExistingRoom) {
|
91 |
+
// Join existing room (for Inference Session integration)
|
92 |
roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId };
|
93 |
console.log(`[RemoteConsumer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`);
|
94 |
} else {
|
src/lib/elements/robot/drivers/RemoteProducer.ts
CHANGED
@@ -62,7 +62,7 @@ export class RemoteProducer implements Producer {
|
|
62 |
|
63 |
let roomData;
|
64 |
if (joinExistingRoom) {
|
65 |
-
// Join existing room (for
|
66 |
roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId };
|
67 |
console.log(`[RemoteProducer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`);
|
68 |
} else {
|
|
|
62 |
|
63 |
let roomData;
|
64 |
if (joinExistingRoom) {
|
65 |
+
// Join existing room (for Inference Session integration)
|
66 |
roomData = { workspaceId: this.workspaceId, roomId: this.config.robotId };
|
67 |
console.log(`[RemoteProducer] Joining existing room ${this.config.robotId} in workspace ${this.workspaceId}`);
|
68 |
} else {
|
src/lib/runes/settings.svelte.ts
CHANGED
@@ -9,7 +9,7 @@ export const settings: Settings = $state({
|
|
9 |
// inferenceServerUrl: 'http://localhost:8001',
|
10 |
// transportServerUrl: 'http://localhost:8000'
|
11 |
// inferenceServerUrl: 'https://blanchon-robothub-inferenceserver.hf.space/api',
|
12 |
-
// transportServerUrl: 'https://blanchon-robothub-
|
13 |
inferenceServerUrl: env.PUBLIC_INFERENCE_SERVER_URL ?? 'http://localhost:8001',
|
14 |
transportServerUrl: env.PUBLIC_TRANSPORT_SERVER_URL ?? 'http://localhost:8000'
|
15 |
});
|
|
|
9 |
// inferenceServerUrl: 'http://localhost:8001',
|
10 |
// transportServerUrl: 'http://localhost:8000'
|
11 |
// inferenceServerUrl: 'https://blanchon-robothub-inferenceserver.hf.space/api',
|
12 |
+
// transportServerUrl: 'https://blanchon-robothub-transport-server.hf.space/api'
|
13 |
inferenceServerUrl: env.PUBLIC_INFERENCE_SERVER_URL ?? 'http://localhost:8001',
|
14 |
transportServerUrl: env.PUBLIC_TRANSPORT_SERVER_URL ?? 'http://localhost:8000'
|
15 |
});
|
src/routes/+layout.svelte
CHANGED
@@ -1,37 +1,38 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import
|
3 |
import "../app.css";
|
4 |
import { Toaster } from "@/components/ui/sonner";
|
5 |
|
6 |
let { children } = $props();
|
7 |
</script>
|
8 |
|
9 |
-
<div class="fixed inset-0 -z-20 h-[100dvh] w-screen bg-[#192437]">
|
10 |
{@render children()}
|
11 |
</div>
|
12 |
|
|
|
|
|
13 |
<Toaster
|
14 |
richColors
|
15 |
position="bottom-right"
|
16 |
toastOptions={{
|
17 |
unstyled: true,
|
18 |
classes: {
|
19 |
-
toast: "rounded-lg shadow-xl p-4 flex items-start gap-3 min-w-[300px] max-w-[450px] border",
|
20 |
title: "font-medium text-sm leading-tight",
|
21 |
description: "text-sm mt-1 leading-relaxed opacity-90",
|
22 |
actionButton:
|
23 |
-
"bg-blue-
|
24 |
cancelButton:
|
25 |
-
"bg-slate-
|
26 |
closeButton:
|
27 |
-
"text-current opacity-70 hover:opacity-100 transition-opacity duration-200 p-1 rounded-md hover:bg-white/10",
|
28 |
-
success: "!bg-green-950 border-green-700
|
29 |
-
error: "!bg-red-950 border-red-700
|
30 |
-
warning: "!bg-yellow-950 border-yellow-700
|
31 |
-
info: "!bg-blue-950 border-blue-700
|
32 |
-
loading: "!bg-slate-900 border-slate-700
|
33 |
}
|
34 |
}}
|
35 |
/>
|
36 |
|
37 |
-
<Overlay />
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { ModeWatcher } from "mode-watcher";
|
3 |
import "../app.css";
|
4 |
import { Toaster } from "@/components/ui/sonner";
|
5 |
|
6 |
let { children } = $props();
|
7 |
</script>
|
8 |
|
9 |
+
<div class="fixed inset-0 -z-20 h-[100dvh] w-screen bg-slate-200 dark:bg-[#192437]">
|
10 |
{@render children()}
|
11 |
</div>
|
12 |
|
13 |
+
<ModeWatcher defaultMode="dark" track={false} />
|
14 |
+
|
15 |
<Toaster
|
16 |
richColors
|
17 |
position="bottom-right"
|
18 |
toastOptions={{
|
19 |
unstyled: true,
|
20 |
classes: {
|
21 |
+
toast: "rounded-lg shadow-xl p-4 flex items-start gap-3 min-w-[300px] max-w-[450px] border bg-white border-slate-200 text-slate-900 dark:bg-slate-900 dark:border-slate-700 dark:text-slate-100",
|
22 |
title: "font-medium text-sm leading-tight",
|
23 |
description: "text-sm mt-1 leading-relaxed opacity-90",
|
24 |
actionButton:
|
25 |
+
"bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-200 ml-auto dark:bg-blue-600 dark:hover:bg-blue-700",
|
26 |
cancelButton:
|
27 |
+
"bg-slate-200 hover:bg-slate-300 text-slate-700 px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-200 dark:bg-slate-700 dark:hover:bg-slate-600 dark:text-slate-300",
|
28 |
closeButton:
|
29 |
+
"text-current opacity-70 hover:opacity-100 transition-opacity duration-200 p-1 rounded-md hover:bg-slate-100 dark:hover:bg-white/10",
|
30 |
+
success: "!bg-green-50 !border-green-200 !text-green-800 dark:!bg-green-950 dark:!border-green-700 dark:!text-green-200",
|
31 |
+
error: "!bg-red-50 !border-red-200 !text-red-800 dark:!bg-red-950 dark:!border-red-700 dark:!text-red-200",
|
32 |
+
warning: "!bg-yellow-50 !border-yellow-200 !text-yellow-800 dark:!bg-yellow-950 dark:!border-yellow-700 dark:!text-yellow-200",
|
33 |
+
info: "!bg-blue-50 !border-blue-200 !text-blue-800 dark:!bg-blue-950 dark:!border-blue-700 dark:!text-blue-200",
|
34 |
+
loading: "!bg-slate-50 !border-slate-200 !text-slate-800 dark:!bg-slate-900 dark:!border-slate-700 dark:!text-slate-200"
|
35 |
}
|
36 |
}}
|
37 |
/>
|
38 |
|
|
src/routes/+page.svelte
CHANGED
@@ -4,12 +4,11 @@
|
|
4 |
import Videos from "@/components/3d/elements/video/Videos.svelte";
|
5 |
import { PerfMonitor } from "@threlte/extras";
|
6 |
import { T, Canvas } from "@threlte/core";
|
7 |
-
import Floor from
|
8 |
import { Gizmo, OrbitControls } from "@threlte/extras";
|
9 |
import { dev } from "$app/environment";
|
10 |
-
import { page } from "$app/stores";
|
11 |
-
import { goto } from "$app/navigation";
|
12 |
import { onMount } from "svelte";
|
|
|
13 |
|
14 |
let workspaceId = $state("");
|
15 |
|
@@ -27,55 +26,58 @@
|
|
27 |
</script>
|
28 |
|
29 |
{#if workspaceId}
|
30 |
-
<
|
31 |
-
<
|
32 |
-
<T.
|
33 |
-
<
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
|
|
46 |
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
position={[-2, 20, -5]}
|
58 |
intensity={1}
|
59 |
castShadow
|
60 |
shadow.mapSize.width={1024}
|
61 |
shadow.mapSize.height={1024}
|
62 |
-
/>
|
63 |
|
64 |
-
|
65 |
|
66 |
-
|
67 |
-
|
68 |
|
69 |
-
|
70 |
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
</Canvas>
|
77 |
{:else}
|
78 |
-
<div class="flex items-center justify-center
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
4 |
import Videos from "@/components/3d/elements/video/Videos.svelte";
|
5 |
import { PerfMonitor } from "@threlte/extras";
|
6 |
import { T, Canvas } from "@threlte/core";
|
7 |
+
import Floor from "@/components/3d/Floor.svelte";
|
8 |
import { Gizmo, OrbitControls } from "@threlte/extras";
|
9 |
import { dev } from "$app/environment";
|
|
|
|
|
10 |
import { onMount } from "svelte";
|
11 |
+
import Overlay from "@/components/interface/overlay/Overlay.svelte";
|
12 |
|
13 |
let workspaceId = $state("");
|
14 |
|
|
|
26 |
</script>
|
27 |
|
28 |
{#if workspaceId}
|
29 |
+
<Overlay {workspaceId} />
|
30 |
+
<Canvas>
|
31 |
+
<T.Scene>
|
32 |
+
<T.PerspectiveCamera position.x={-15} position.y={15} position.z={15} fov={20} makeDefault>
|
33 |
+
<OrbitControls
|
34 |
+
enableDamping
|
35 |
+
dampingFactor={0.05}
|
36 |
+
enableZoom
|
37 |
+
minDistance={15}
|
38 |
+
maxDistance={40}
|
39 |
+
minPolarAngle={Math.PI / 6}
|
40 |
+
maxPolarAngle={Math.PI / 2}
|
41 |
+
target={[0, 1, 0]}
|
42 |
+
>
|
43 |
+
<Gizmo />
|
44 |
+
</OrbitControls>
|
45 |
+
</T.PerspectiveCamera>
|
46 |
|
47 |
+
<!-- Lighting setup -->
|
48 |
+
<T.AmbientLight intensity={0.4} />
|
49 |
+
<T.DirectionalLight
|
50 |
+
position={[2, 20, 5]}
|
51 |
+
intensity={5}
|
52 |
+
castShadow
|
53 |
+
shadow.mapSize.width={1024}
|
54 |
+
shadow.mapSize.height={1024}
|
55 |
+
/>
|
56 |
+
<!-- <T.DirectionalLight
|
57 |
position={[-2, 20, -5]}
|
58 |
intensity={1}
|
59 |
castShadow
|
60 |
shadow.mapSize.width={1024}
|
61 |
shadow.mapSize.height={1024}
|
62 |
+
/> -->
|
63 |
|
64 |
+
<Floor />
|
65 |
|
66 |
+
<!-- Robot component now gets robots from robotManager -->
|
67 |
+
<Robots {workspaceId} />
|
68 |
|
69 |
+
<Videos {workspaceId} />
|
70 |
|
71 |
+
<Computes {workspaceId} />
|
72 |
+
</T.Scene>
|
73 |
+
{#if dev}
|
74 |
+
<PerfMonitor anchorX="right" anchorY="bottom" logsPerSecond={30} />
|
75 |
+
{/if}
|
76 |
+
</Canvas>
|
77 |
{:else}
|
78 |
+
<div class="flex h-screen items-center justify-center">
|
79 |
+
<div class="text-slate-900 dark:text-white">
|
80 |
+
Loading
|
81 |
+
</div>
|
82 |
+
</div>
|
83 |
+
{/if}
|
static-server.js
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
#!/usr/bin/env bun
|
2 |
|
3 |
// Simple static file server for Svelte build output
|
4 |
-
import {
|
5 |
|
6 |
const PORT = process.env.PORT || 3000;
|
7 |
const BUILD_DIR = "./build";
|
|
|
1 |
#!/usr/bin/env bun
|
2 |
|
3 |
// Simple static file server for Svelte build output
|
4 |
+
import { join } from "path";
|
5 |
|
6 |
const PORT = process.env.PORT || 3000;
|
7 |
const BUILD_DIR = "./build";
|