blanchon commited on
Commit
8173aa6
·
1 Parent(s): c3f65a8
Files changed (35) hide show
  1. Dockerfile +2 -2
  2. bun.lock +1 -1
  3. external/RobotHub-InferenceServer +1 -1
  4. package.json +1 -1
  5. src/lib/components/3d/Floor.svelte +7 -1
  6. src/lib/components/3d/elements/compute/Computes.svelte +1 -1
  7. src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte +54 -54
  8. src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte +44 -44
  9. src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte +43 -43
  10. src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte +6 -6
  11. src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte +1 -1
  12. src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte +1 -1
  13. src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte +1 -1
  14. src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte +1 -1
  15. src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte +1 -1
  16. src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte +54 -54
  17. src/lib/components/3d/elements/robot/modal/ManualControlSheet.svelte +21 -21
  18. src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte +49 -49
  19. src/lib/components/3d/elements/video/modal/VideoInputConnectionModal.svelte +55 -55
  20. src/lib/components/3d/elements/video/modal/VideoOutputConnectionModal.svelte +128 -117
  21. src/lib/components/interface/overlay/AddAIButton.svelte +10 -9
  22. src/lib/components/interface/overlay/AddRobotButton.svelte +11 -10
  23. src/lib/components/interface/overlay/AddSensorButton.svelte +11 -10
  24. src/lib/components/interface/overlay/Overlay.svelte +17 -7
  25. src/lib/components/interface/overlay/SettingsButton.svelte +1 -1
  26. src/lib/components/interface/overlay/SettingsSheet.svelte +75 -70
  27. src/lib/elements/compute/README.md +3 -3
  28. src/lib/elements/compute/RemoteComputeManager.svelte.ts +1 -1
  29. src/lib/elements/robot/Robot.svelte.ts +2 -2
  30. src/lib/elements/robot/drivers/RemoteConsumer.ts +1 -1
  31. src/lib/elements/robot/drivers/RemoteProducer.ts +1 -1
  32. src/lib/runes/settings.svelte.ts +1 -1
  33. src/routes/+layout.svelte +13 -12
  34. src/routes/+page.svelte +46 -44
  35. 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-transportserver.hf.space/api
62
  ENV PUBLIC_TRANSPORT_SERVER_URL=${PUBLIC_TRANSPORT_SERVER_URL}
63
 
64
- ARG PUBLIC_INFERENCE_SERVER_URL=https://blanchon-robothub-inference-server.hf.space/api
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.7",
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 119f2e226ee9dd33a2ab59894e7c1be24cb5dbde
 
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.7",
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
- <!-- AI Session Creation Modal -->
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(`AI session created: ${sessionId}`);
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('AI session started');
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('AI session stopped');
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('AI session deleted');
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-500/30 bg-purple-900/20 p-3"
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-600 text-xs">
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-500/30 bg-purple-900/20 p-3">
176
  <div class="grid grid-cols-2 gap-2 text-xs">
177
  <div>
178
- <span class="text-purple-300 font-medium">Session ID:</span>
179
- <span class="text-purple-100 block">{compute.sessionId}</span>
180
  </div>
181
  <div>
182
- <span class="text-purple-300 font-medium">Status:</span>
183
- <span class="text-purple-100 block">{compute.statusInfo.emoji} {compute.statusInfo.statusText}</span>
184
  </div>
185
  <div>
186
- <span class="text-purple-300 font-medium">Policy:</span>
187
- <span class="text-purple-100 block">{compute.sessionConfig?.policyPath}</span>
188
  </div>
189
  <div>
190
- <span class="text-purple-300 font-medium">Cameras:</span>
191
- <span class="text-purple-100 block">{compute.sessionConfig?.cameraNames.join(', ')}</span>
192
  </div>
193
  </div>
194
  </div>
195
 
196
  <!-- Connection Details -->
197
- <div class="rounded-lg border border-green-500/30 bg-green-900/20 p-3">
198
- <div class="text-sm font-medium text-green-300 mb-2">📡 Inference Server Connections</div>
199
  <div class="space-y-1 text-xs">
200
  <div>
201
- <span class="text-green-400">Workspace:</span>
202
- <span class="text-green-200 font-mono ml-2">{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-400">📹 {camera}:</span>
207
- <span class="text-green-200 font-mono ml-2">{roomId}</span>
208
  </div>
209
  {/each}
210
  <div>
211
- <span class="text-green-400">📥 Joint Input:</span>
212
- <span class="text-green-200 font-mono ml-2">{compute.sessionData.joint_input_room_id}</span>
213
  </div>
214
  <div>
215
- <span class="text-green-400">📤 Joint Output:</span>
216
- <span class="text-green-200 font-mono ml-2">{compute.sessionData.joint_output_room_id}</span>
217
  </div>
218
  </div>
219
  </div>
@@ -226,7 +226,7 @@
226
  size="sm"
227
  onclick={handleStartSession}
228
  disabled={isConnecting}
229
- class="bg-green-600 hover:bg-green-700 text-xs disabled:opacity-50"
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 AI 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-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-800 border-slate-600 text-slate-100 opacity-60 cursor-not-allowed"
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-300 text-sm">
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-600 hover:bg-purple-700 disabled:opacity-50"
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 AI Session
367
  {/if}
368
  </Button>
369
  </div>
@@ -372,9 +372,9 @@
372
  {/if}
373
 
374
  <!-- Quick Info -->
375
- <div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
376
  <span class="icon-[mdi--information] mr-1 size-3"></span>
377
- AI 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>
 
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 AI session available. Create a session first.');
29
  return;
30
  }
31
 
@@ -36,10 +36,10 @@
36
 
37
  isConnecting = true;
38
  try {
39
- // Get the joint input room ID from the AI session
40
  const jointInputRoomId = compute.sessionData?.joint_input_room_id;
41
  if (!jointInputRoomId) {
42
- throw new Error('No joint input room found in AI session');
43
  }
44
 
45
  // Find the selected robot
@@ -53,7 +53,7 @@
53
 
54
  connectedRobotId = selectedRobotId;
55
 
56
- toast.success('Robot input connected to AI session', {
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
- <!-- AI Session Status -->
115
  <div
116
- class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
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">AI Session</span>
121
  </div>
122
  {#if compute.hasSession}
123
- <Badge variant="default" class="bg-purple-600 text-xs">
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
- AI Session Required
137
  </Card.Title>
138
  </Card.Header>
139
- <Card.Content class="text-sm text-yellow-300">
140
- You need to create an AI 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-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-600 text-xs">
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-500/30 bg-amber-900/20 p-3">
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-600 hover:bg-amber-700 text-xs disabled:opacity-50"
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 → AI 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-800/50">
253
- <span class="text-blue-300 font-medium">Joint Input Room:</span>
254
- <span class="text-blue-200 font-mono">{compute.sessionData?.joint_input_room_id}</span>
255
  </div>
256
- <div class="text-slate-400 text-xs">
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 AI session as a producer.
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-700 bg-slate-800/30 p-2 text-xs 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>
 
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 AI session available. Create a session first.');
29
  return;
30
  }
31
 
@@ -36,10 +36,10 @@
36
 
37
  isConnecting = true;
38
  try {
39
- // Get the joint output room ID from the AI session
40
  const jointOutputRoomId = compute.sessionData?.joint_output_room_id;
41
  if (!jointOutputRoomId) {
42
- throw new Error('No joint output room found in AI session');
43
  }
44
 
45
  // Find the selected robot
@@ -53,7 +53,7 @@
53
 
54
  connectedRobotId = selectedRobotId;
55
 
56
- toast.success('Robot output connected to AI session', {
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
- <!-- AI Session Status -->
112
  <div
113
- class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
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">AI Session</span>
118
  </div>
119
  {#if compute.hasSession}
120
- <Badge variant="default" class="bg-purple-600 text-xs">
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
- AI Session Required
134
  </Card.Title>
135
  </Card.Header>
136
- <Card.Content class="text-sm text-yellow-300">
137
- You need to create an AI 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-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-600 text-xs">
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-500/30 bg-blue-900/20 p-3">
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-600 hover:bg-blue-700 text-xs disabled:opacity-50"
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: AI 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-800/50">
250
- <span class="text-orange-300 font-medium">Joint Output Room:</span>
251
- <span class="text-orange-200 font-mono">{compute.sessionData?.joint_output_room_id}</span>
252
  </div>
253
- <div class="text-slate-400 text-xs">
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-700 bg-slate-800/30 p-2 text-xs 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>
 
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 AI session available. Create a session first.');
36
  return;
37
  }
38
 
@@ -65,7 +65,7 @@
65
  // Start streaming
66
  await videoProducer.startCamera();
67
 
68
- toast.success(`Camera connected to AI session`, {
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
- <!-- AI 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">AI Session</span>
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
- AI Session Required
150
  </Card.Title>
151
  </Card.Header>
152
  <Card.Content class="text-sm text-yellow-300">
153
- You need to create an AI session before connecting video inputs.
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 AI sessions.
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 AI sessions.
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 AI sessions.
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 AI sessions.
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 AI sessions.
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-400 text-sm">
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-200 text-sm">
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-600 text-xs">
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-500/30 bg-blue-900/20 p-3">
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-600 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
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-500/30 bg-purple-900/20 p-3">
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-500/50 bg-green-500/5 p-3">
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-700 border border-slate-600 rounded text-xs text-slate-100 disabled:opacity-50"
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-600 hover:bg-green-700 disabled:opacity-50"
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-600 hover:bg-green-700 disabled:opacity-50"
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-600 bg-slate-800/50 p-2">
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-200 truncate">
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-600 hover:bg-purple-700 shrink-0 disabled:opacity-50"
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-400 text-xs">
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-600 bg-gradient-to-b from-slate-700 to-slate-800 p-0 text-white sm:w-96"
23
  >
24
  <!-- Header -->
25
- <Sheet.Header class="border-b border-slate-600 bg-slate-700/80 p-6 backdrop-blur-sm">
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-700 scrollbar-thumb-slate-500 flex-1 overflow-y-auto px-4">
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-600 text-xs">
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-500 italic">No joints available</p>
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-600 bg-slate-800/50 p-3">
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-400 text-sm">
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-200 text-sm">
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-600 text-xs">
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-600 text-sm text-white hover:bg-green-700"
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-500/50 bg-green-500/5 p-3">
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-700 border border-slate-600 rounded text-xs text-slate-100 disabled:opacity-50"
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-600 bg-slate-800/50 p-2">
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-200 truncate">
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-600 hover:bg-orange-700 shrink-0"
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-700/50 p-2">
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-400 text-xs">
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-400 text-sm">
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-600 text-xs">
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-500/30 bg-green-900/20 p-3">
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-500/30 bg-blue-900/20 p-3">
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-600 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
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-500/30 bg-purple-900/20 p-3">
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-500/50 bg-green-500/5 p-3">
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-700 border border-slate-600 rounded text-xs text-slate-100 disabled:opacity-50"
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-600 hover:bg-green-700 disabled:opacity-50"
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-600 hover:bg-green-700 disabled:opacity-50"
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-600 bg-slate-800/50 p-2">
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-200 truncate">
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-600 hover:bg-purple-700 shrink-0 disabled:opacity-50"
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-400 text-xs">
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-blue-400"></span>
134
  Video Output - {video?.name || 'No Video Selected'}
135
  </Dialog.Title>
136
- <Dialog.Description class="text-sm text-slate-400">
137
- Broadcast your local camera to remote viewers in rooms
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">Broadcasting Error</Alert.Title>
148
- <Alert.Description class="text-red-400 text-sm">
149
  {error}
150
  </Alert.Description>
151
  </Alert.Root>
152
  {/if}
 
153
  <!-- Current Status Overview -->
154
- <Card.Root class="border-blue-500/30 bg-blue-900/20">
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-blue-400"></span>
159
- <span class="text-sm font-medium text-blue-300">Current Video Output</span>
160
  </div>
161
  {#if video?.hasOutput}
162
- <Badge variant="default" class="bg-blue-600 text-xs">
163
- Broadcasting
164
  </Badge>
165
  {:else}
166
- <Badge variant="secondary" class="text-xs text-slate-400">Not Broadcasting</Badge>
167
  {/if}
168
  </div>
169
  {#if video?.hasOutput}
170
- <div class="mt-2 text-xs text-blue-400/70">
171
  {#if video.output.roomId}
172
- Room: {video.output.roomId}
173
  {:else}
174
- Broadcasting to server
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-blue-500/30 bg-blue-500/5">
214
  <Card.Header>
215
- <Card.Title class="flex items-center gap-2 text-base text-blue-200">
216
- <span class="icon-[mdi--broadcast] size-4"></span>
217
- Broadcasting
218
  </Card.Title>
219
  </Card.Header>
220
  <Card.Content>
221
- <div class="rounded-lg border border-blue-500/30 bg-blue-900/20 p-3">
222
  <div class="flex items-center justify-between">
223
  <div>
224
- <p class="text-sm font-medium text-blue-300">
225
- Streaming to Server
226
  </p>
227
  {#if video.output.roomId}
228
- <p class="text-xs text-blue-400/70">
229
- Room ID: {video.output.roomId}
230
  </p>
231
  {/if}
232
- <p class="text-xs text-blue-400/70">
233
- Source: Local Camera
234
- </p>
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--stop] mr-1 size-3"></span>
251
  Stop
252
  </Button>
253
  </div>
@@ -256,17 +221,70 @@
256
  </Card.Root>
257
  {/if}
258
 
259
- <!-- Remote Broadcasting -->
260
- <Card.Root class="border-orange-500/30 bg-orange-500/5">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-orange-200">
265
- <span class="icon-[mdi--broadcast] size-4"></span>
266
- Remote Broadcasting (Rooms)
267
  </Card.Title>
268
- <Card.Description class="text-xs text-orange-300/70">
269
- Broadcast your camera feed to remote viewers in rooms
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-orange-300 hover:text-orange-200 hover:bg-orange-500/20"
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
- <!-- Broadcasting State -->
292
- <div class="rounded-lg border border-orange-500/30 bg-orange-900/20 p-3">
293
  <div class="flex items-center justify-between">
294
  <div>
295
- <p class="text-sm font-medium text-orange-300">Broadcasting Active</p>
296
- <p class="text-xs text-orange-400/70">Sending video to remote viewers</p>
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--stop] mr-1 size-3"></span>
306
- {isConnecting ? 'Stopping...' : 'Stop'}
307
  </Button>
308
  </div>
309
  </div>
310
- {:else if video?.canOutput}
311
  <!-- Create New Room -->
312
- <div class="rounded border-2 border-dashed border-green-500/50 bg-green-500/5 p-3">
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 camera feed
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-700 border border-slate-600 rounded text-xs text-slate-100 disabled:opacity-50"
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-600 hover:bg-green-700 disabled:opacity-50"
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-600 hover:bg-green-700 disabled:opacity-50"
343
  >
344
- Create & Start
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-orange-300">Join Existing Room:</span>
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-600 bg-slate-800/50 p-2">
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-200 truncate">
370
  {room.id}
371
  </p>
372
- <div class="flex gap-3 text-xs text-slate-400">
373
- <span>{room.participants?.producer ? '🔴 Occupied' : '🟢 Available'}</span>
374
- <span>👥 {room.participants?.consumers?.length || 0} viewers</span>
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-orange-600 hover:bg-orange-700 shrink-0 disabled:opacity-50"
384
  >
385
- <span class="icon-[mdi--broadcast] mr-1 size-3"></span>
386
  Join as Output
387
  </Button>
388
  {:else}
@@ -392,7 +410,7 @@
392
  disabled
393
  class="text-xs opacity-50 shrink-0"
394
  >
395
- Occupied
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 broadcast to join a different room
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 Broadcasting</Alert.Title>
425
- <Alert.Description class="text-slate-400 text-xs">
426
- <strong>Requirements:</strong> Local camera input only • <strong>Remote streams:</strong> Cannot be re-broadcasted • Only one output per room
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-600 text-white transition-all duration-200 hover:bg-purple-500"
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-500/30 bg-purple-600 px-2 text-white transition-all duration-200 hover:bg-purple-500",
101
- open && "bg-purple-700 shadow-inner"
 
102
  )}
103
  >
104
  <span
@@ -113,12 +114,12 @@
113
  </DropdownMenu.Trigger>
114
 
115
  <DropdownMenu.Content
116
- class="w-56 border-purple-500/30 bg-purple-600 backdrop-blur-sm"
117
  align="center"
118
  >
119
  <DropdownMenu.Group>
120
  <DropdownMenu.GroupHeading
121
- class="text-xs font-semibold tracking-wider text-purple-200 uppercase"
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-600 text-white transition-all duration-200",
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-200 transition-colors duration-200"
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-200 transition-colors duration-200">
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-600 text-white transition-all duration-200 hover:bg-emerald-500"
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-500/30 bg-emerald-600 px-2 text-white transition-all duration-200 hover:bg-emerald-500",
79
- open && "bg-emerald-700 shadow-inner"
 
80
  )}
81
  >
82
  <span
@@ -91,12 +92,12 @@
91
  </DropdownMenu.Trigger>
92
 
93
  <DropdownMenu.Content
94
- class="w-56 border-emerald-500/30 bg-emerald-600 backdrop-blur-sm"
95
  align="center"
96
  >
97
  <DropdownMenu.Group>
98
  <DropdownMenu.GroupHeading
99
- class="text-xs font-semibold tracking-wider text-emerald-200 uppercase"
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-600 text-white transition-all duration-200",
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-200 transition-colors duration-200"
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-200 transition-colors duration-200">
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-700 text-xs text-emerald-100 group-data-highlighted:bg-emerald-400 group-data-highlighted:text-emerald-900"
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-600 text-white transition-all duration-200 hover:bg-blue-500"
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-500/30 bg-blue-600 px-2 text-white transition-all duration-200 hover:bg-blue-500",
110
- open && "bg-blue-700 shadow-inner"
 
111
  )}
112
  >
113
  <span
@@ -122,12 +123,12 @@
122
  </DropdownMenu.Trigger>
123
 
124
  <DropdownMenu.Content
125
- class="w-56 border-blue-500/30 bg-blue-600 backdrop-blur-sm"
126
  align="center"
127
  >
128
  <DropdownMenu.Group>
129
  <DropdownMenu.GroupHeading
130
- class="text-xs font-semibold tracking-wider text-blue-200 uppercase"
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-600 text-white transition-all duration-200",
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-200 transition-colors duration-200"
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-200 transition-colors duration-200">
155
  {sensor.description}
156
  </span>
157
  </div>
158
  {#if sensor.isDefault}
159
  <Badge
160
  variant="secondary"
161
- class="ml-2 bg-blue-700 text-xs text-blue-100 group-data-highlighted:bg-blue-400 group-data-highlighted:text-blue-900"
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 shadow-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 shadow-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 shadow-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-600 text-white transition-all duration-200 hover:bg-orange-500"
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-600 bg-gradient-to-b from-slate-700 to-slate-800 p-0 text-white sm:w-96"
93
  >
94
  <!-- Header -->
95
- <Sheet.Header class="border-b border-slate-600 bg-slate-700/80 p-6 backdrop-blur-sm">
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
- <!-- <div class="space-y-4">
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 bind:checked={localSettings.darkMode} />
 
 
 
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-600 bg-slate-800/50 p-3 transition-colors duration-200 hover:bg-slate-700/50"
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-600 bg-slate-800/50 p-3 transition-colors duration-200 hover:bg-slate-700/50"
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-600 bg-slate-800/50 p-3 transition-colors duration-200 hover:bg-slate-700/50"
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">Servo motor control library</p>
 
 
 
 
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-600 bg-slate-800/50 p-3 transition-colors duration-200 hover:bg-slate-700/50"
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 AI session
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 AI sessions with configurable parameters
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 AI session
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 AI session
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 AI session integration)
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 AI session integration)
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 AI 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 {
 
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 AI 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 {
 
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-transportserver.hf.space/api'
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 Overlay from "@/components/interface/overlay/Overlay.svelte";
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-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-200 ml-auto",
24
  cancelButton:
25
- "bg-slate-700 hover:bg-slate-600 text-slate-300 px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-200",
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 !text-green-200",
29
- error: "!bg-red-950 border-red-700 !text-red-200",
30
- warning: "!bg-yellow-950 border-yellow-700 !text-yellow-200",
31
- info: "!bg-blue-950 border-blue-700 !text-blue-200",
32
- loading: "!bg-slate-900 border-slate-700 !text-slate-200"
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 '@/components/3d/Floor.svelte'
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
- <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={1}
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 items-center justify-center h-screen">
79
- <div class="text-white">Loading workspace...</div>
80
- </div>
81
- {/if}
 
 
 
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 { resolve, join } from "path";
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";