NERDDISCO commited on
Commit
8c5a6a3
·
1 Parent(s): bf33acd

chore: remove unused files

Browse files
examples/robot-control-web/App.tsx DELETED
@@ -1,19 +0,0 @@
1
- import { useState } from "react";
2
- import { Home } from "./pages/Home";
3
- import { ErrorBoundary } from "./components/ErrorBoundary";
4
- import type { RobotConnection } from "@lerobot/web";
5
-
6
- export function App() {
7
- const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
8
-
9
- return (
10
- <ErrorBoundary>
11
- <div className="min-h-screen bg-background">
12
- <Home
13
- connectedRobots={connectedRobots}
14
- onConnectedRobotsChange={setConnectedRobots}
15
- />
16
- </div>
17
- </ErrorBoundary>
18
- );
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/CalibrationModal.tsx DELETED
@@ -1,49 +0,0 @@
1
- import {
2
- Dialog,
3
- DialogContent,
4
- DialogDescription,
5
- DialogFooter,
6
- DialogHeader,
7
- DialogTitle,
8
- } from "./ui/dialog";
9
- import { Button } from "./ui/button";
10
-
11
- interface CalibrationModalProps {
12
- open: boolean;
13
- onOpenChange: (open: boolean) => void;
14
- deviceType: string;
15
- onContinue: () => void;
16
- }
17
-
18
- export function CalibrationModal({
19
- open,
20
- onOpenChange,
21
- deviceType,
22
- onContinue,
23
- }: CalibrationModalProps) {
24
- return (
25
- <Dialog open={open} onOpenChange={onOpenChange}>
26
- <DialogContent className="sm:max-w-md">
27
- <DialogHeader>
28
- <DialogTitle>📍 Set Homing Position</DialogTitle>
29
- <DialogDescription className="text-base py-4">
30
- Move the SO-100 {deviceType} to the <strong>MIDDLE</strong> of its
31
- range of motion and click OK when ready.
32
- <br />
33
- <br />
34
- The calibration will then automatically:
35
- <br />• Record homing offsets
36
- <br />• Record joint ranges (manual - you control when to stop)
37
- <br />• Save configuration file
38
- </DialogDescription>
39
- </DialogHeader>
40
-
41
- <DialogFooter>
42
- <Button onClick={onContinue} className="w-full">
43
- OK - Start Calibration
44
- </Button>
45
- </DialogFooter>
46
- </DialogContent>
47
- </Dialog>
48
- );
49
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/CalibrationPanel.tsx DELETED
@@ -1,408 +0,0 @@
1
- import { useState, useCallback, useMemo } from "react";
2
- import { Button } from "./ui/button.js";
3
- import {
4
- Card,
5
- CardContent,
6
- CardDescription,
7
- CardHeader,
8
- CardTitle,
9
- } from "./ui/card.js";
10
- import { Badge } from "./ui/badge.js";
11
- import {
12
- calibrate,
13
- releaseMotors,
14
- type WebCalibrationResults,
15
- type LiveCalibrationData,
16
- type CalibrationProcess,
17
- } from "@lerobot/web";
18
- import { CalibrationModal } from "./CalibrationModal.js";
19
- import type { RobotConnection } from "@lerobot/web";
20
-
21
- interface CalibrationPanelProps {
22
- robot: RobotConnection;
23
- onFinish: () => void;
24
- }
25
-
26
- export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
27
- // Simple state management
28
- const [isCalibrating, setIsCalibrating] = useState(false);
29
- const [calibrationResult, setCalibrationResult] =
30
- useState<WebCalibrationResults | null>(null);
31
- const [status, setStatus] = useState<string>("Ready to calibrate");
32
- const [modalOpen, setModalOpen] = useState(false);
33
- const [calibrationProcess, setCalibrationProcess] =
34
- useState<CalibrationProcess | null>(null);
35
- const [motorData, setMotorData] = useState<LiveCalibrationData>({});
36
- const [isPreparing, setIsPreparing] = useState(false);
37
-
38
- // Motor names for display
39
- const motorNames = useMemo(
40
- () => [
41
- "shoulder_pan",
42
- "shoulder_lift",
43
- "elbow_flex",
44
- "wrist_flex",
45
- "wrist_roll",
46
- "gripper",
47
- ],
48
- []
49
- );
50
-
51
- // Initialize motor data
52
- const initializeMotorData = useCallback(() => {
53
- const initialData: LiveCalibrationData = {};
54
- motorNames.forEach((name) => {
55
- initialData[name] = {
56
- current: 2047,
57
- min: 2047,
58
- max: 2047,
59
- range: 0,
60
- };
61
- });
62
- setMotorData(initialData);
63
- }, [motorNames]);
64
-
65
- // Release motor torque
66
- const releaseMotorTorque = useCallback(async () => {
67
- try {
68
- setIsPreparing(true);
69
- setStatus("🔓 Releasing motor torque - joints can now be moved freely");
70
-
71
- await releaseMotors(robot);
72
-
73
- setStatus("✅ Joints are now free to move - set your homing position");
74
- } catch (error) {
75
- console.warn("Failed to release motor torque:", error);
76
- setStatus("⚠️ Could not release motor torque - try moving joints gently");
77
- } finally {
78
- setIsPreparing(false);
79
- }
80
- }, [robot]);
81
-
82
- // Start calibration using new API
83
- const handleContinueCalibration = useCallback(async () => {
84
- setModalOpen(false);
85
-
86
- if (!robot.port || !robot.robotType) {
87
- return;
88
- }
89
-
90
- try {
91
- setStatus("🤖 Starting calibration process...");
92
- setIsCalibrating(true);
93
- initializeMotorData();
94
-
95
- // Use the unified config API for calibration
96
- const process = await calibrate({
97
- robot,
98
- onLiveUpdate: (data) => {
99
- setMotorData(data);
100
- setStatus(
101
- "📏 Recording joint ranges - move all joints through their full range"
102
- );
103
- },
104
- onProgress: (message) => {
105
- setStatus(message);
106
- },
107
- });
108
-
109
- setCalibrationProcess(process);
110
-
111
- // Add Enter key listener for stopping (matching Node.js UX)
112
- const handleKeyPress = (event: KeyboardEvent) => {
113
- if (event.key === "Enter") {
114
- process.stop();
115
- }
116
- };
117
- document.addEventListener("keydown", handleKeyPress);
118
-
119
- try {
120
- // Wait for calibration to complete
121
- const result = await process.result;
122
- setCalibrationResult(result);
123
-
124
- // App-level concern: Save results to storage
125
- const serialNumber =
126
- robot.serialNumber || robot.usbMetadata?.serialNumber || "unknown";
127
- await saveCalibrationResults(
128
- result,
129
- robot.robotType,
130
- robot.robotId || `${robot.robotType}_1`,
131
- serialNumber
132
- );
133
-
134
- setStatus(
135
- "✅ Calibration completed successfully! Configuration saved."
136
- );
137
- } finally {
138
- document.removeEventListener("keydown", handleKeyPress);
139
- setCalibrationProcess(null);
140
- setIsCalibrating(false);
141
- }
142
- } catch (error) {
143
- console.error("❌ Calibration failed:", error);
144
- setStatus(
145
- `❌ Calibration failed: ${
146
- error instanceof Error ? error.message : error
147
- }`
148
- );
149
- setIsCalibrating(false);
150
- setCalibrationProcess(null);
151
- }
152
- }, [robot, initializeMotorData]);
153
-
154
- // Stop calibration recording
155
- const handleStopRecording = useCallback(() => {
156
- if (calibrationProcess) {
157
- calibrationProcess.stop();
158
- }
159
- }, [calibrationProcess]);
160
-
161
- // App-level concern: Save calibration results
162
- const saveCalibrationResults = async (
163
- results: WebCalibrationResults,
164
- robotType: string,
165
- robotId: string,
166
- serialNumber: string
167
- ) => {
168
- try {
169
- // Save to unified storage (app-level functionality)
170
- const { saveCalibrationData } = await import("../lib/unified-storage.js");
171
-
172
- const fullCalibrationData = {
173
- ...results,
174
- device_type: robotType,
175
- device_id: robotId,
176
- calibrated_at: new Date().toISOString(),
177
- platform: "web",
178
- api: "Web Serial API",
179
- };
180
-
181
- const metadata = {
182
- timestamp: new Date().toISOString(),
183
- readCount: Object.keys(motorData).length > 0 ? 100 : 0, // Estimate
184
- };
185
-
186
- saveCalibrationData(serialNumber, fullCalibrationData, metadata);
187
- } catch (error) {
188
- console.warn("Failed to save calibration results:", error);
189
- }
190
- };
191
-
192
- // App-level concern: JSON export functionality
193
- const downloadConfigJSON = useCallback(() => {
194
- if (!calibrationResult) return;
195
-
196
- const jsonString = JSON.stringify(calibrationResult, null, 2);
197
- const blob = new Blob([jsonString], { type: "application/json" });
198
- const url = URL.createObjectURL(blob);
199
-
200
- const link = document.createElement("a");
201
- link.href = url;
202
- link.download = `${robot.robotId || robot.robotType}_calibration.json`;
203
- document.body.appendChild(link);
204
- link.click();
205
- document.body.removeChild(link);
206
- URL.revokeObjectURL(url);
207
- }, [calibrationResult, robot.robotId, robot.robotType]);
208
-
209
- return (
210
- <div className="space-y-4">
211
- {/* Calibration Status Card */}
212
- <Card>
213
- <CardHeader>
214
- <div className="flex items-center justify-between">
215
- <div>
216
- <CardTitle className="text-lg">
217
- 🛠️ Calibrating: {robot.robotId}
218
- </CardTitle>
219
- <CardDescription>
220
- {robot.robotType?.replace("_", " ")} • {robot.name}
221
- </CardDescription>
222
- </div>
223
- <Badge
224
- variant={
225
- isCalibrating
226
- ? "default"
227
- : calibrationResult
228
- ? "default"
229
- : "outline"
230
- }
231
- >
232
- {isCalibrating
233
- ? "Recording"
234
- : calibrationResult
235
- ? "Complete"
236
- : "Ready"}
237
- </Badge>
238
- </div>
239
- </CardHeader>
240
- <CardContent>
241
- <div className="space-y-4">
242
- <div className="p-3 bg-blue-50 rounded-lg">
243
- <p className="text-sm font-medium text-blue-900">Status:</p>
244
- <p className="text-sm text-blue-800">{status}</p>
245
- {isCalibrating && (
246
- <p className="text-xs text-blue-600 mt-1">
247
- Move joints through full range | Press "Finish Recording" or
248
- Enter key when done
249
- </p>
250
- )}
251
- </div>
252
-
253
- <div className="flex gap-2">
254
- {!isCalibrating && !calibrationResult && (
255
- <Button
256
- onClick={async () => {
257
- // ✅ Release motor torque FIRST - so user can move joints immediately
258
- await releaseMotorTorque();
259
- // THEN open modal - user can now follow instructions right away
260
- setModalOpen(true);
261
- }}
262
- disabled={isPreparing}
263
- >
264
- {isPreparing ? "Preparing..." : "Start Calibration"}
265
- </Button>
266
- )}
267
-
268
- {isCalibrating && calibrationProcess && (
269
- <Button onClick={handleStopRecording} variant="default">
270
- Finish Recording
271
- </Button>
272
- )}
273
-
274
- {calibrationResult && (
275
- <>
276
- <Button onClick={downloadConfigJSON} variant="outline">
277
- Download Config JSON
278
- </Button>
279
- <Button onClick={onFinish}>Done</Button>
280
- </>
281
- )}
282
- </div>
283
- </div>
284
- </CardContent>
285
- </Card>
286
-
287
- {/* Configuration JSON Display */}
288
- {calibrationResult && (
289
- <Card>
290
- <CardHeader>
291
- <CardTitle className="text-lg">
292
- 🎯 Calibration Configuration
293
- </CardTitle>
294
- <CardDescription>
295
- Copy this JSON or download it for your robot setup
296
- </CardDescription>
297
- </CardHeader>
298
- <CardContent>
299
- <div className="space-y-3">
300
- <pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
301
- <code>{JSON.stringify(calibrationResult, null, 2)}</code>
302
- </pre>
303
- <div className="flex gap-2">
304
- <Button onClick={downloadConfigJSON} variant="outline">
305
- 📄 Download JSON File
306
- </Button>
307
- <Button
308
- onClick={() => {
309
- navigator.clipboard.writeText(
310
- JSON.stringify(calibrationResult, null, 2)
311
- );
312
- }}
313
- variant="outline"
314
- >
315
- 📋 Copy to Clipboard
316
- </Button>
317
- </div>
318
- </div>
319
- </CardContent>
320
- </Card>
321
- )}
322
-
323
- {/* Live Position Recording Table */}
324
- <Card>
325
- <CardHeader>
326
- <CardTitle className="text-lg">Live Position Recording</CardTitle>
327
- <CardDescription>
328
- Real-time motor position feedback during calibration
329
- </CardDescription>
330
- </CardHeader>
331
- <CardContent>
332
- <div className="overflow-hidden rounded-lg border">
333
- <table className="w-full font-mono text-sm">
334
- <thead className="bg-gray-50">
335
- <tr>
336
- <th className="px-4 py-2 text-left font-medium text-gray-900">
337
- Motor Name
338
- </th>
339
- <th className="px-4 py-2 text-right font-medium text-gray-900">
340
- Current
341
- </th>
342
- <th className="px-4 py-2 text-right font-medium text-gray-900">
343
- Min
344
- </th>
345
- <th className="px-4 py-2 text-right font-medium text-gray-900">
346
- Max
347
- </th>
348
- <th className="px-4 py-2 text-right font-medium text-gray-900">
349
- Range
350
- </th>
351
- </tr>
352
- </thead>
353
- <tbody className="divide-y divide-gray-200">
354
- {motorNames.map((motorName) => {
355
- const motor = motorData[motorName] || {
356
- current: 2047,
357
- min: 2047,
358
- max: 2047,
359
- range: 0,
360
- };
361
-
362
- return (
363
- <tr key={motorName} className="hover:bg-gray-50">
364
- <td className="px-4 py-2 font-medium flex items-center gap-2">
365
- {motorName}
366
- {motor.range > 100 && (
367
- <span className="text-green-600 text-xs">✓</span>
368
- )}
369
- </td>
370
- <td className="px-4 py-2 text-right">{motor.current}</td>
371
- <td className="px-4 py-2 text-right">{motor.min}</td>
372
- <td className="px-4 py-2 text-right">{motor.max}</td>
373
- <td className="px-4 py-2 text-right font-medium">
374
- <span
375
- className={
376
- motor.range > 100
377
- ? "text-green-600"
378
- : "text-gray-500"
379
- }
380
- >
381
- {motor.range}
382
- </span>
383
- </td>
384
- </tr>
385
- );
386
- })}
387
- </tbody>
388
- </table>
389
- </div>
390
-
391
- {isCalibrating && (
392
- <div className="mt-3 text-center text-sm text-gray-600">
393
- Move joints through their full range of motion...
394
- </div>
395
- )}
396
- </CardContent>
397
- </Card>
398
-
399
- {/* Calibration Modal */}
400
- <CalibrationModal
401
- open={modalOpen}
402
- onOpenChange={setModalOpen}
403
- deviceType={robot.robotType || "robot"}
404
- onContinue={handleContinueCalibration}
405
- />
406
- </div>
407
- );
408
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ErrorBoundary.tsx DELETED
@@ -1,65 +0,0 @@
1
- import { Component, type ErrorInfo, type ReactNode } from "react";
2
- import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
3
- import { Button } from "./ui/button";
4
-
5
- interface Props {
6
- children: ReactNode;
7
- }
8
-
9
- interface State {
10
- hasError: boolean;
11
- error?: Error;
12
- }
13
-
14
- export class ErrorBoundary extends Component<Props, State> {
15
- constructor(props: Props) {
16
- super(props);
17
- this.state = { hasError: false };
18
- }
19
-
20
- static getDerivedStateFromError(error: Error): State {
21
- return { hasError: true, error };
22
- }
23
-
24
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
25
- console.error("ErrorBoundary caught an error:", error, errorInfo);
26
- }
27
-
28
- render() {
29
- if (this.state.hasError) {
30
- return (
31
- <div className="min-h-screen flex items-center justify-center p-8">
32
- <div className="max-w-md w-full">
33
- <Alert variant="destructive">
34
- <AlertTitle>Something went wrong</AlertTitle>
35
- <AlertDescription>
36
- The application encountered an error. Please try refreshing the
37
- page or contact support if the problem persists.
38
- </AlertDescription>
39
- </Alert>
40
- <div className="mt-4 flex gap-2">
41
- <Button onClick={() => window.location.reload()}>
42
- Refresh Page
43
- </Button>
44
- <Button
45
- variant="outline"
46
- onClick={() =>
47
- this.setState({ hasError: false, error: undefined })
48
- }
49
- >
50
- Try Again
51
- </Button>
52
- </div>
53
- {process.env.NODE_ENV === "development" && this.state.error && (
54
- <div className="mt-4 p-4 bg-gray-100 rounded-md text-xs">
55
- <pre>{this.state.error.stack}</pre>
56
- </div>
57
- )}
58
- </div>
59
- </div>
60
- );
61
- }
62
-
63
- return this.props.children;
64
- }
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/PortManager.tsx DELETED
@@ -1,485 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { Button } from "./ui/button";
3
- import {
4
- Card,
5
- CardContent,
6
- CardDescription,
7
- CardHeader,
8
- CardTitle,
9
- } from "./ui/card";
10
- import { Alert, AlertDescription } from "./ui/alert";
11
- import { Badge } from "./ui/badge";
12
- import { findPort, isWebSerialSupported } from "@lerobot/web";
13
- import type { RobotConnection } from "@lerobot/web";
14
-
15
- interface PortManagerProps {
16
- connectedRobots: RobotConnection[];
17
- onConnectedRobotsChange: (robots: RobotConnection[]) => void;
18
- onCalibrate?: (port: any) => void; // Let library handle port type
19
- onTeleoperate?: (robot: RobotConnection) => void;
20
- }
21
-
22
- export function PortManager({
23
- connectedRobots,
24
- onConnectedRobotsChange,
25
- onCalibrate,
26
- onTeleoperate,
27
- }: PortManagerProps) {
28
- const [isFindingPorts, setIsFindingPorts] = useState(false);
29
- const [findPortsLog, setFindPortsLog] = useState<string[]>([]);
30
- const [error, setError] = useState<string | null>(null);
31
-
32
- // Load saved robots on mount by calling findPort with saved data
33
- useEffect(() => {
34
- loadSavedRobots();
35
- }, []);
36
-
37
- const loadSavedRobots = async () => {
38
- try {
39
- console.log("🔄 Loading saved robots from localStorage...");
40
-
41
- // Load saved robot configs for auto-connect mode
42
- const robotConfigs: any[] = [];
43
- const { getUnifiedRobotData } = await import("../lib/unified-storage");
44
-
45
- // Check localStorage for saved robot data
46
- for (let i = 0; i < localStorage.length; i++) {
47
- const key = localStorage.key(i);
48
- if (key && key.startsWith("lerobotjs-")) {
49
- const serialNumber = key.replace("lerobotjs-", "");
50
- const robotData = getUnifiedRobotData(serialNumber);
51
-
52
- if (robotData) {
53
- console.log(
54
- `✅ Found saved robot: ${robotData.device_info.robotId}`
55
- );
56
-
57
- // Create robot config for auto-connect mode
58
- robotConfigs.push({
59
- robotType: robotData.device_info.robotType,
60
- robotId: robotData.device_info.robotId,
61
- serialNumber: serialNumber,
62
- });
63
- }
64
- }
65
- }
66
-
67
- if (robotConfigs.length > 0) {
68
- console.log(
69
- `🔄 Auto-connecting to ${robotConfigs.length} saved robots...`
70
- );
71
-
72
- // Use auto-connect mode - NO DIALOG will be shown!
73
- const findPortProcess = await findPort({
74
- robotConfigs,
75
- onMessage: (message) => {
76
- console.log(`Auto-connect: ${message}`);
77
- },
78
- });
79
-
80
- const reconnectedRobots = await findPortProcess.result;
81
- console.log(
82
- `✅ Auto-connected to ${
83
- reconnectedRobots.filter((r) => r.isConnected).length
84
- }/${robotConfigs.length} saved robots`
85
- );
86
-
87
- onConnectedRobotsChange(reconnectedRobots);
88
- } else {
89
- console.log("No saved robots found in localStorage");
90
- }
91
- } catch (error) {
92
- console.error("Failed to load saved robots:", error);
93
- }
94
- };
95
-
96
- const handleFindPorts = async () => {
97
- if (!isWebSerialSupported()) {
98
- setError("Web Serial API is not supported in this browser");
99
- return;
100
- }
101
-
102
- try {
103
- setIsFindingPorts(true);
104
- setFindPortsLog([]);
105
- setError(null);
106
-
107
- // Use clean library API - library handles everything!
108
- const findPortProcess = await findPort({
109
- onMessage: (message) => {
110
- setFindPortsLog((prev) => [...prev, message]);
111
- },
112
- });
113
-
114
- const robotConnections = await findPortProcess.result;
115
-
116
- // Add new robots to the list (avoid duplicates)
117
- const newRobots = robotConnections.filter(
118
- (newRobot) =>
119
- !connectedRobots.some(
120
- (existing) => existing.serialNumber === newRobot.serialNumber
121
- )
122
- );
123
-
124
- onConnectedRobotsChange([...connectedRobots, ...newRobots]);
125
- setFindPortsLog((prev) => [
126
- ...prev,
127
- `✅ Found ${newRobots.length} new robots`,
128
- ]);
129
- } catch (error) {
130
- if (
131
- error instanceof Error &&
132
- (error.message.includes("cancelled") ||
133
- error.name === "NotAllowedError")
134
- ) {
135
- console.log("Port discovery cancelled by user");
136
- return;
137
- }
138
- setError(error instanceof Error ? error.message : "Failed to find ports");
139
- } finally {
140
- setIsFindingPorts(false);
141
- }
142
- };
143
-
144
- const handleDisconnect = (index: number) => {
145
- const updatedRobots = connectedRobots.filter((_, i) => i !== index);
146
- onConnectedRobotsChange(updatedRobots);
147
- };
148
-
149
- const handleCalibrate = (robot: RobotConnection) => {
150
- if (!robot.robotType || !robot.robotId) {
151
- setError("Please configure robot type and ID first");
152
- return;
153
- }
154
- if (onCalibrate) {
155
- onCalibrate(robot.port);
156
- }
157
- };
158
-
159
- const handleTeleoperate = (robot: RobotConnection) => {
160
- if (!robot.robotType || !robot.robotId) {
161
- setError("Please configure robot type and ID first");
162
- return;
163
- }
164
-
165
- if (!robot.isConnected || !robot.port) {
166
- setError(
167
- "Robot is not connected. Please use 'Find & Connect Robots' first."
168
- );
169
- return;
170
- }
171
-
172
- // Robot is connected, proceed with teleoperation
173
- if (onTeleoperate) {
174
- onTeleoperate(robot);
175
- }
176
- };
177
-
178
- return (
179
- <Card>
180
- <CardHeader>
181
- <CardTitle>🔌 Robot Connection Manager</CardTitle>
182
- <CardDescription>
183
- Find and connect to your robot devices
184
- </CardDescription>
185
- </CardHeader>
186
- <CardContent>
187
- <div className="space-y-6">
188
- {/* Error Display */}
189
- {error && (
190
- <Alert variant="destructive">
191
- <AlertDescription>{error}</AlertDescription>
192
- </Alert>
193
- )}
194
-
195
- {/* Find Ports Button */}
196
- <Button
197
- onClick={handleFindPorts}
198
- disabled={isFindingPorts || !isWebSerialSupported()}
199
- className="w-full"
200
- >
201
- {isFindingPorts ? "Finding Robots..." : "🔍 Find & Connect Robots"}
202
- </Button>
203
-
204
- {/* Find Ports Log */}
205
- {findPortsLog.length > 0 && (
206
- <div className="bg-gray-50 p-3 rounded-md text-sm space-y-1 max-h-32 overflow-y-auto">
207
- {findPortsLog.map((log, index) => (
208
- <div key={index} className="text-gray-700">
209
- {log}
210
- </div>
211
- ))}
212
- </div>
213
- )}
214
-
215
- {/* Connected Robots */}
216
- <div>
217
- <h4 className="font-semibold mb-3">
218
- Connected Robots ({connectedRobots.length})
219
- </h4>
220
-
221
- {connectedRobots.length === 0 ? (
222
- <div className="text-center py-8 text-gray-500">
223
- <div className="text-2xl mb-2">🤖</div>
224
- <p>No robots found</p>
225
- <p className="text-xs">
226
- Click "Find & Connect Robots" to discover devices
227
- </p>
228
- </div>
229
- ) : (
230
- <div className="space-y-4">
231
- {connectedRobots.map((robot, index) => (
232
- <RobotCard
233
- key={robot.serialNumber || index}
234
- robot={robot}
235
- onDisconnect={() => handleDisconnect(index)}
236
- onCalibrate={() => handleCalibrate(robot)}
237
- onTeleoperate={() => handleTeleoperate(robot)}
238
- />
239
- ))}
240
- </div>
241
- )}
242
- </div>
243
- </div>
244
- </CardContent>
245
- </Card>
246
- );
247
- }
248
-
249
- interface RobotCardProps {
250
- robot: RobotConnection;
251
- onDisconnect: () => void;
252
- onCalibrate: () => void;
253
- onTeleoperate: () => void;
254
- }
255
-
256
- function RobotCard({
257
- robot,
258
- onDisconnect,
259
- onCalibrate,
260
- onTeleoperate,
261
- }: RobotCardProps) {
262
- const [calibrationStatus, setCalibrationStatus] = useState<{
263
- timestamp: string;
264
- readCount: number;
265
- } | null>(null);
266
- const [isEditing, setIsEditing] = useState(false);
267
- const [editRobotType, setEditRobotType] = useState<
268
- "so100_follower" | "so100_leader"
269
- >(robot.robotType || "so100_follower");
270
- const [editRobotId, setEditRobotId] = useState(robot.robotId || "");
271
-
272
- const isConfigured = robot.robotType && robot.robotId;
273
-
274
- // Check calibration status using unified storage
275
- useEffect(() => {
276
- const checkCalibrationStatus = async () => {
277
- if (!robot.serialNumber) return;
278
-
279
- try {
280
- const { getCalibrationStatus } = await import("../lib/unified-storage");
281
- const status = getCalibrationStatus(robot.serialNumber);
282
- setCalibrationStatus(status);
283
- } catch (error) {
284
- console.warn("Failed to check calibration status:", error);
285
- }
286
- };
287
-
288
- checkCalibrationStatus();
289
- }, [robot.serialNumber]);
290
-
291
- const handleSaveConfig = async () => {
292
- if (!editRobotId.trim() || !robot.serialNumber) return;
293
-
294
- try {
295
- const { saveRobotConfig } = await import("../lib/unified-storage");
296
- saveRobotConfig(
297
- robot.serialNumber,
298
- editRobotType,
299
- editRobotId.trim(),
300
- robot.usbMetadata
301
- );
302
-
303
- // Update the robot object (this should trigger a re-render)
304
- robot.robotType = editRobotType;
305
- robot.robotId = editRobotId.trim();
306
-
307
- setIsEditing(false);
308
- console.log("✅ Robot configuration saved");
309
- } catch (error) {
310
- console.error("Failed to save robot configuration:", error);
311
- }
312
- };
313
-
314
- const handleCancelEdit = () => {
315
- setEditRobotType(robot.robotType || "so100_follower");
316
- setEditRobotId(robot.robotId || "");
317
- setIsEditing(false);
318
- };
319
-
320
- return (
321
- <div className="border rounded-lg p-4 space-y-3">
322
- {/* Header */}
323
- <div className="flex items-center justify-between">
324
- <div className="flex items-center space-x-2">
325
- <div className="flex flex-col">
326
- <span className="font-medium">
327
- {robot.robotId || robot.name || "Unnamed Robot"}
328
- </span>
329
- <span className="text-xs text-gray-500">
330
- {robot.robotType?.replace("_", " ") || "Not configured"}
331
- </span>
332
- {robot.serialNumber && (
333
- <span className="text-xs text-gray-400 font-mono">
334
- {robot.serialNumber.length > 20
335
- ? robot.serialNumber.substring(0, 20) + "..."
336
- : robot.serialNumber}
337
- </span>
338
- )}
339
- </div>
340
- <div className="flex flex-col gap-1">
341
- <Badge variant={robot.isConnected ? "default" : "outline"}>
342
- {robot.isConnected ? "Connected" : "Available"}
343
- </Badge>
344
- {calibrationStatus && (
345
- <Badge variant="default" className="bg-green-100 text-green-800">
346
- ✅ Calibrated
347
- </Badge>
348
- )}
349
- </div>
350
- </div>
351
- <Button variant="destructive" size="sm" onClick={onDisconnect}>
352
- Remove
353
- </Button>
354
- </div>
355
-
356
- {/* Robot Configuration Display (when not editing) */}
357
- {!isEditing && isConfigured && (
358
- <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
359
- <div className="flex items-center space-x-3">
360
- <div>
361
- <div className="font-medium text-sm">{robot.robotId}</div>
362
- <div className="text-xs text-gray-600">
363
- {robot.robotType?.replace("_", " ")}
364
- </div>
365
- </div>
366
- </div>
367
- <Button
368
- variant="outline"
369
- size="sm"
370
- onClick={() => setIsEditing(true)}
371
- >
372
- Edit
373
- </Button>
374
- </div>
375
- )}
376
-
377
- {/* Configuration Prompt for unconfigured robots */}
378
- {!isEditing && !isConfigured && (
379
- <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
380
- <div className="text-sm text-blue-800">
381
- Robot needs configuration before use
382
- </div>
383
- <Button
384
- variant="outline"
385
- size="sm"
386
- onClick={() => setIsEditing(true)}
387
- >
388
- Configure
389
- </Button>
390
- </div>
391
- )}
392
-
393
- {/* Robot Configuration Form (when editing) */}
394
- {isEditing && (
395
- <div className="space-y-3 p-3 bg-gray-50 rounded-lg">
396
- <div className="grid grid-cols-2 gap-3">
397
- <div>
398
- <label className="text-sm font-medium block mb-1">
399
- Robot Type
400
- </label>
401
- <select
402
- value={editRobotType}
403
- onChange={(e) =>
404
- setEditRobotType(
405
- e.target.value as "so100_follower" | "so100_leader"
406
- )
407
- }
408
- className="w-full px-2 py-1 border rounded text-sm"
409
- >
410
- <option value="so100_follower">SO-100 Follower</option>
411
- <option value="so100_leader">SO-100 Leader</option>
412
- </select>
413
- </div>
414
- <div>
415
- <label className="text-sm font-medium block mb-1">Robot ID</label>
416
- <input
417
- type="text"
418
- value={editRobotId}
419
- onChange={(e) => setEditRobotId(e.target.value)}
420
- placeholder="e.g., my_robot"
421
- className="w-full px-2 py-1 border rounded text-sm"
422
- />
423
- </div>
424
- </div>
425
-
426
- <div className="flex gap-2">
427
- <Button
428
- size="sm"
429
- onClick={handleSaveConfig}
430
- disabled={!editRobotId.trim()}
431
- >
432
- Save
433
- </Button>
434
- <Button size="sm" variant="outline" onClick={handleCancelEdit}>
435
- Cancel
436
- </Button>
437
- </div>
438
- </div>
439
- )}
440
-
441
- {/* Calibration Status */}
442
- {isConfigured && !isEditing && (
443
- <div className="text-sm text-gray-600">
444
- {calibrationStatus ? (
445
- <span>
446
- Last calibrated:{" "}
447
- {new Date(calibrationStatus.timestamp).toLocaleDateString()}
448
- <span className="text-xs ml-1">
449
- ({calibrationStatus.readCount} readings)
450
- </span>
451
- </span>
452
- ) : (
453
- <span>Not calibrated yet</span>
454
- )}
455
- </div>
456
- )}
457
-
458
- {/* Actions */}
459
- {isConfigured && !isEditing && (
460
- <div className="flex gap-2">
461
- <Button
462
- size="sm"
463
- variant={calibrationStatus ? "outline" : "default"}
464
- onClick={onCalibrate}
465
- >
466
- {calibrationStatus ? "📏 Re-calibrate" : "📏 Calibrate"}
467
- </Button>
468
- <Button
469
- size="sm"
470
- variant="outline"
471
- onClick={onTeleoperate}
472
- disabled={!robot.isConnected}
473
- title={
474
- !robot.isConnected
475
- ? "Use 'Find & Connect Robots' first"
476
- : undefined
477
- }
478
- >
479
- 🎮 Teleoperate
480
- </Button>
481
- </div>
482
- )}
483
- </div>
484
- );
485
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/TeleoperationPanel.tsx DELETED
@@ -1,587 +0,0 @@
1
- import { useState, useEffect, useRef, useCallback } from "react";
2
- import { Button } from "./ui/button";
3
- import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
4
- import { Badge } from "./ui/badge";
5
- import { Alert, AlertDescription } from "./ui/alert";
6
- import {
7
- teleoperate,
8
- type TeleoperationProcess,
9
- type TeleoperationState,
10
- type TeleoperateConfig,
11
- } from "@lerobot/web";
12
- import { getUnifiedRobotData } from "../lib/unified-storage";
13
- import type { RobotConnection } from "@lerobot/web";
14
- import { SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
15
-
16
- interface TeleoperationPanelProps {
17
- robot: RobotConnection;
18
- onClose: () => void;
19
- }
20
-
21
- export function TeleoperationPanel({
22
- robot,
23
- onClose,
24
- }: TeleoperationPanelProps) {
25
- const [teleoperationState, setTeleoperationState] =
26
- useState<TeleoperationState>({
27
- isActive: false,
28
- motorConfigs: [],
29
- lastUpdate: 0,
30
- keyStates: {},
31
- });
32
- const [error, setError] = useState<string | null>(null);
33
- const [, setIsInitialized] = useState(false);
34
-
35
- // Separate refs for keyboard and direct teleoperators
36
- const keyboardProcessRef = useRef<TeleoperationProcess | null>(null);
37
- const directProcessRef = useRef<TeleoperationProcess | null>(null);
38
-
39
- // Initialize both teleoperation processes
40
- useEffect(() => {
41
- const initializeTeleoperation = async () => {
42
- if (!robot || !robot.robotType) {
43
- setError("No robot configuration available");
44
- return;
45
- }
46
-
47
- try {
48
- // Load calibration data from demo storage (app concern)
49
- let calibrationData;
50
- if (robot.serialNumber) {
51
- const data = getUnifiedRobotData(robot.serialNumber);
52
- calibrationData = data?.calibration;
53
- if (calibrationData) {
54
- console.log("✅ Loaded calibration data for", robot.serialNumber);
55
- }
56
- }
57
-
58
- // Create keyboard teleoperation process
59
- const keyboardConfig: TeleoperateConfig = {
60
- robot: robot,
61
- teleop: {
62
- type: "keyboard",
63
- },
64
- calibrationData,
65
- onStateUpdate: (state: TeleoperationState) => {
66
- setTeleoperationState(state);
67
- },
68
- };
69
- const keyboardProcess = await teleoperate(keyboardConfig);
70
-
71
- // Create direct teleoperation process
72
- const directConfig: TeleoperateConfig = {
73
- robot: robot,
74
- teleop: {
75
- type: "direct",
76
- },
77
- calibrationData,
78
- };
79
- const directProcess = await teleoperate(directConfig);
80
-
81
- keyboardProcessRef.current = keyboardProcess;
82
- directProcessRef.current = directProcess;
83
- setTeleoperationState(keyboardProcess.getState());
84
- setIsInitialized(true);
85
- setError(null);
86
-
87
- console.log("✅ Initialized both keyboard and direct teleoperators");
88
- } catch (error) {
89
- const errorMessage =
90
- error instanceof Error
91
- ? error.message
92
- : "Failed to initialize teleoperation";
93
- setError(errorMessage);
94
- console.error("❌ Failed to initialize teleoperation:", error);
95
- }
96
- };
97
-
98
- initializeTeleoperation();
99
-
100
- return () => {
101
- // Cleanup on unmount
102
- const cleanup = async () => {
103
- try {
104
- if (keyboardProcessRef.current) {
105
- await keyboardProcessRef.current.disconnect();
106
- keyboardProcessRef.current = null;
107
- }
108
- if (directProcessRef.current) {
109
- await directProcessRef.current.disconnect();
110
- directProcessRef.current = null;
111
- }
112
- console.log("🧹 Teleoperation cleanup completed");
113
- } catch (error) {
114
- console.warn("Error during teleoperation cleanup:", error);
115
- }
116
- };
117
- cleanup();
118
- };
119
- }, [robot]);
120
-
121
- // Keyboard event handlers
122
- const handleKeyDown = useCallback(
123
- (event: KeyboardEvent) => {
124
- if (!teleoperationState.isActive || !keyboardProcessRef.current) return;
125
-
126
- const key = event.key;
127
- event.preventDefault();
128
- keyboardProcessRef.current.updateKeyState(key, true);
129
- },
130
- [teleoperationState.isActive]
131
- );
132
-
133
- const handleKeyUp = useCallback(
134
- (event: KeyboardEvent) => {
135
- if (!teleoperationState.isActive || !keyboardProcessRef.current) return;
136
-
137
- const key = event.key;
138
- event.preventDefault();
139
- keyboardProcessRef.current.updateKeyState(key, false);
140
- },
141
- [teleoperationState.isActive]
142
- );
143
-
144
- // Register keyboard events
145
- useEffect(() => {
146
- if (teleoperationState.isActive) {
147
- window.addEventListener("keydown", handleKeyDown);
148
- window.addEventListener("keyup", handleKeyUp);
149
-
150
- return () => {
151
- window.removeEventListener("keydown", handleKeyDown);
152
- window.removeEventListener("keyup", handleKeyUp);
153
- };
154
- }
155
- }, [teleoperationState.isActive, handleKeyDown, handleKeyUp]);
156
-
157
- const handleStart = () => {
158
- if (!keyboardProcessRef.current || !directProcessRef.current) {
159
- setError("Teleoperation not initialized");
160
- return;
161
- }
162
-
163
- try {
164
- keyboardProcessRef.current.start();
165
- directProcessRef.current.start();
166
- console.log("🎮 Both keyboard and direct teleoperation started");
167
- } catch (error) {
168
- const errorMessage =
169
- error instanceof Error
170
- ? error.message
171
- : "Failed to start teleoperation";
172
- setError(errorMessage);
173
- }
174
- };
175
-
176
- const handleStop = async () => {
177
- try {
178
- if (keyboardProcessRef.current) {
179
- keyboardProcessRef.current.stop();
180
- }
181
- if (directProcessRef.current) {
182
- directProcessRef.current.stop();
183
- }
184
- console.log("🛑 Both keyboard and direct teleoperation stopped");
185
- } catch (error) {
186
- console.warn("Error during teleoperation stop:", error);
187
- }
188
- };
189
-
190
- const handleClose = async () => {
191
- try {
192
- if (keyboardProcessRef.current) {
193
- keyboardProcessRef.current.stop();
194
- await keyboardProcessRef.current.disconnect();
195
- }
196
- if (directProcessRef.current) {
197
- directProcessRef.current.stop();
198
- await directProcessRef.current.disconnect();
199
- }
200
- console.log("🔌 Properly disconnected from robot");
201
- } catch (error) {
202
- console.warn("Error during teleoperation cleanup:", error);
203
- }
204
- onClose();
205
- };
206
-
207
- const simulateKeyPress = (key: string) => {
208
- if (!keyboardProcessRef.current) return;
209
- keyboardProcessRef.current.updateKeyState(key, true);
210
- };
211
-
212
- const simulateKeyRelease = (key: string) => {
213
- if (!keyboardProcessRef.current) return;
214
- keyboardProcessRef.current.updateKeyState(key, false);
215
- };
216
-
217
- // Unified motor control: Both sliders AND keyboard use the same teleoperator
218
- // This ensures the UI always shows the correct motor positions
219
- const moveMotorToPosition = async (motorIndex: number, position: number) => {
220
- if (!keyboardProcessRef.current) return;
221
-
222
- try {
223
- const motorName = teleoperationState.motorConfigs[motorIndex]?.name;
224
- if (motorName) {
225
- const keyboardTeleoperator = keyboardProcessRef.current
226
- .teleoperator as any;
227
- await keyboardTeleoperator.moveMotor(motorName, position);
228
- }
229
- } catch (error) {
230
- console.warn(
231
- `Failed to move motor ${motorIndex + 1} to position ${position}:`,
232
- error
233
- );
234
- }
235
- };
236
-
237
- const isConnected = robot?.isConnected || false;
238
- const isActive = teleoperationState.isActive;
239
- const motorConfigs = teleoperationState.motorConfigs;
240
- const keyStates = teleoperationState.keyStates;
241
-
242
- // Virtual keyboard component
243
- const VirtualKeyboard = () => {
244
- const isKeyPressed = (key: string) => {
245
- return keyStates?.[key]?.pressed || false;
246
- };
247
-
248
- const KeyButton = ({
249
- keyCode,
250
- children,
251
- className = "",
252
- size = "default" as "default" | "sm" | "lg" | "icon",
253
- }: {
254
- keyCode: string;
255
- children: React.ReactNode;
256
- className?: string;
257
- size?: "default" | "sm" | "lg" | "icon";
258
- }) => {
259
- const control =
260
- SO100_KEYBOARD_CONTROLS[
261
- keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
262
- ];
263
- const pressed = isKeyPressed(keyCode);
264
-
265
- const handleMouseDown = (e: React.MouseEvent) => {
266
- e.preventDefault();
267
- if (!isActive) return;
268
- simulateKeyPress(keyCode);
269
- };
270
-
271
- const handleMouseUp = (e: React.MouseEvent) => {
272
- e.preventDefault();
273
- if (!isActive) return;
274
- simulateKeyRelease(keyCode);
275
- };
276
-
277
- return (
278
- <Button
279
- variant={pressed ? "default" : "outline"}
280
- size={size}
281
- className={`
282
- ${className}
283
- ${
284
- pressed
285
- ? "bg-blue-600 text-white shadow-inner"
286
- : "hover:bg-gray-100"
287
- }
288
- transition-all duration-75 font-mono text-xs
289
- ${!isActive ? "opacity-50 cursor-not-allowed" : ""}
290
- `}
291
- disabled={!isActive}
292
- onMouseDown={handleMouseDown}
293
- onMouseUp={handleMouseUp}
294
- onMouseLeave={handleMouseUp}
295
- title={control?.description || keyCode}
296
- >
297
- {children}
298
- </Button>
299
- );
300
- };
301
-
302
- return (
303
- <div className="space-y-4">
304
- {/* Arrow Keys */}
305
- <div className="text-center">
306
- <h4 className="text-xs font-semibold mb-2 text-gray-600">Shoulder</h4>
307
- <div className="flex flex-col items-center gap-1">
308
- <KeyButton keyCode="ArrowUp" size="sm">
309
-
310
- </KeyButton>
311
- <div className="flex gap-1">
312
- <KeyButton keyCode="ArrowLeft" size="sm">
313
-
314
- </KeyButton>
315
- <KeyButton keyCode="ArrowDown" size="sm">
316
-
317
- </KeyButton>
318
- <KeyButton keyCode="ArrowRight" size="sm">
319
-
320
- </KeyButton>
321
- </div>
322
- </div>
323
- </div>
324
-
325
- {/* WASD Keys */}
326
- <div className="text-center">
327
- <h4 className="text-xs font-semibold mb-2 text-gray-600">
328
- Elbow/Wrist
329
- </h4>
330
- <div className="flex flex-col items-center gap-1">
331
- <KeyButton keyCode="w" size="sm">
332
- W
333
- </KeyButton>
334
- <div className="flex gap-1">
335
- <KeyButton keyCode="a" size="sm">
336
- A
337
- </KeyButton>
338
- <KeyButton keyCode="s" size="sm">
339
- S
340
- </KeyButton>
341
- <KeyButton keyCode="d" size="sm">
342
- D
343
- </KeyButton>
344
- </div>
345
- </div>
346
- </div>
347
-
348
- {/* Q/E and Gripper */}
349
- <div className="flex justify-center gap-2">
350
- <div className="text-center">
351
- <h4 className="text-xs font-semibold mb-2 text-gray-600">Roll</h4>
352
- <div className="flex gap-1">
353
- <KeyButton keyCode="q" size="sm">
354
- Q
355
- </KeyButton>
356
- <KeyButton keyCode="e" size="sm">
357
- E
358
- </KeyButton>
359
- </div>
360
- </div>
361
- <div className="text-center">
362
- <h4 className="text-xs font-semibold mb-2 text-gray-600">
363
- Gripper
364
- </h4>
365
- <div className="flex gap-1">
366
- <KeyButton keyCode="o" size="sm">
367
- O
368
- </KeyButton>
369
- <KeyButton keyCode="c" size="sm">
370
- C
371
- </KeyButton>
372
- </div>
373
- </div>
374
- </div>
375
-
376
- {/* Emergency Stop */}
377
- <div className="text-center border-t pt-2">
378
- <KeyButton
379
- keyCode="Escape"
380
- className="bg-red-100 border-red-300 hover:bg-red-200 text-red-800 text-xs"
381
- >
382
- ESC
383
- </KeyButton>
384
- </div>
385
- </div>
386
- );
387
- };
388
-
389
- return (
390
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
391
- <div className="container mx-auto px-6 py-8">
392
- {/* Header */}
393
- <div className="flex justify-between items-center mb-6">
394
- <div>
395
- <h1 className="text-3xl font-bold text-gray-900">
396
- 🎮 Robot Teleoperation
397
- </h1>
398
- <p className="text-gray-600">
399
- {robot.robotId || robot.name} - {robot.serialNumber}
400
- </p>
401
- </div>
402
- <Button variant="outline" onClick={handleClose}>
403
- ← Back to Dashboard
404
- </Button>
405
- </div>
406
-
407
- {/* Error Alert */}
408
- {error && (
409
- <Alert variant="destructive" className="mb-6">
410
- <AlertDescription>{error}</AlertDescription>
411
- </Alert>
412
- )}
413
-
414
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
415
- {/* Status Panel */}
416
- <Card>
417
- <CardHeader>
418
- <CardTitle className="flex items-center gap-2">
419
- Status
420
- <Badge variant={isConnected ? "default" : "destructive"}>
421
- {isConnected ? "Connected" : "Disconnected"}
422
- </Badge>
423
- </CardTitle>
424
- </CardHeader>
425
- <CardContent className="space-y-4">
426
- <div className="flex items-center justify-between">
427
- <span className="text-sm text-gray-600">Teleoperation</span>
428
- <Badge variant={isActive ? "default" : "secondary"}>
429
- {isActive ? "Active" : "Stopped"}
430
- </Badge>
431
- </div>
432
-
433
- <div className="flex items-center justify-between">
434
- <span className="text-sm text-gray-600">Active Keys</span>
435
- <Badge variant="outline">
436
- {
437
- Object.values(keyStates || {}).filter(
438
- (state) => state.pressed
439
- ).length
440
- }
441
- </Badge>
442
- </div>
443
-
444
- <div className="space-y-2">
445
- {isActive ? (
446
- <Button
447
- onClick={handleStop}
448
- variant="destructive"
449
- className="w-full"
450
- >
451
- ⏹️ Stop Teleoperation
452
- </Button>
453
- ) : (
454
- <Button
455
- onClick={handleStart}
456
- disabled={!isConnected}
457
- className="w-full"
458
- >
459
- ▶️ Start Teleoperation
460
- </Button>
461
- )}
462
- </div>
463
- </CardContent>
464
- </Card>
465
-
466
- {/* Virtual Keyboard */}
467
- <Card>
468
- <CardHeader>
469
- <CardTitle>Virtual Keyboard</CardTitle>
470
- </CardHeader>
471
- <CardContent>
472
- <VirtualKeyboard />
473
- </CardContent>
474
- </Card>
475
-
476
- {/* Motor Status */}
477
- <Card>
478
- <CardHeader>
479
- <CardTitle>Motor Positions</CardTitle>
480
- </CardHeader>
481
- <CardContent className="space-y-3">
482
- {motorConfigs.map((motor, index) => {
483
- return (
484
- <div key={motor.name} className="space-y-1">
485
- <div className="flex justify-between items-center">
486
- <span className="text-sm font-medium">
487
- {motor.name.replace("_", " ")}
488
- </span>
489
- <span className="text-xs text-gray-500">
490
- {motor.currentPosition}
491
- </span>
492
- </div>
493
- <input
494
- type="range"
495
- min={motor.minPosition}
496
- max={motor.maxPosition}
497
- value={motor.currentPosition}
498
- disabled={!isActive}
499
- className={`w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200 slider-thumb ${
500
- !isActive ? "opacity-50 cursor-not-allowed" : ""
501
- }`}
502
- style={{
503
- background: isActive
504
- ? `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${
505
- ((motor.currentPosition - motor.minPosition) /
506
- (motor.maxPosition - motor.minPosition)) *
507
- 100
508
- }%, #e5e7eb ${
509
- ((motor.currentPosition - motor.minPosition) /
510
- (motor.maxPosition - motor.minPosition)) *
511
- 100
512
- }%, #e5e7eb 100%)`
513
- : "#e5e7eb",
514
- }}
515
- onChange={async (e) => {
516
- if (!isActive) return;
517
- const newPosition = parseInt(e.target.value);
518
- try {
519
- await moveMotorToPosition(index, newPosition);
520
- } catch (error) {
521
- console.warn(
522
- "Failed to move motor via slider:",
523
- error
524
- );
525
- }
526
- }}
527
- />
528
- <div className="flex justify-between text-xs text-gray-400">
529
- <span>{motor.minPosition}</span>
530
- <span>{motor.maxPosition}</span>
531
- </div>
532
- </div>
533
- );
534
- })}
535
- </CardContent>
536
- </Card>
537
- </div>
538
-
539
- {/* Help Card */}
540
- <Card className="mt-6">
541
- <CardHeader>
542
- <CardTitle>Control Instructions</CardTitle>
543
- </CardHeader>
544
- <CardContent>
545
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
546
- <div>
547
- <h4 className="font-semibold mb-2">Arrow Keys</h4>
548
- <ul className="space-y-1 text-gray-600">
549
- <li>↑ ↓ Shoulder lift</li>
550
- <li>← → Shoulder pan</li>
551
- </ul>
552
- </div>
553
- <div>
554
- <h4 className="font-semibold mb-2">WASD Keys</h4>
555
- <ul className="space-y-1 text-gray-600">
556
- <li>W S Elbow flex</li>
557
- <li>A D Wrist flex</li>
558
- </ul>
559
- </div>
560
- <div>
561
- <h4 className="font-semibold mb-2">Other Keys</h4>
562
- <ul className="space-y-1 text-gray-600">
563
- <li>Q E Wrist roll</li>
564
- <li>O Open gripper</li>
565
- <li>C Close gripper</li>
566
- </ul>
567
- </div>
568
- <div>
569
- <h4 className="font-semibold mb-2 text-red-700">Emergency</h4>
570
- <ul className="space-y-1 text-red-600">
571
- <li>ESC Emergency stop</li>
572
- </ul>
573
- </div>
574
- </div>
575
- <div className="mt-4 p-3 bg-blue-50 rounded-lg">
576
- <p className="text-sm text-blue-800">
577
- 💡 <strong>Pro tip:</strong> Use your physical keyboard for
578
- faster control, or click the virtual keys below. Hold keys down
579
- for continuous movement.
580
- </p>
581
- </div>
582
- </CardContent>
583
- </Card>
584
- </div>
585
- </div>
586
- );
587
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ui/alert.tsx DELETED
@@ -1,58 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import { cn } from "../../lib/utils";
4
-
5
- const alertVariants = cva(
6
- "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
7
- {
8
- variants: {
9
- variant: {
10
- default: "bg-background text-foreground",
11
- destructive:
12
- "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13
- },
14
- },
15
- defaultVariants: {
16
- variant: "default",
17
- },
18
- }
19
- );
20
-
21
- const Alert = React.forwardRef<
22
- HTMLDivElement,
23
- React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
24
- >(({ className, variant, ...props }, ref) => (
25
- <div
26
- ref={ref}
27
- role="alert"
28
- className={cn(alertVariants({ variant }), className)}
29
- {...props}
30
- />
31
- ));
32
- Alert.displayName = "Alert";
33
-
34
- const AlertTitle = React.forwardRef<
35
- HTMLParagraphElement,
36
- React.HTMLAttributes<HTMLHeadingElement>
37
- >(({ className, ...props }, ref) => (
38
- <h5
39
- ref={ref}
40
- className={cn("mb-1 font-medium leading-none tracking-tight", className)}
41
- {...props}
42
- />
43
- ));
44
- AlertTitle.displayName = "AlertTitle";
45
-
46
- const AlertDescription = React.forwardRef<
47
- HTMLParagraphElement,
48
- React.HTMLAttributes<HTMLParagraphElement>
49
- >(({ className, ...props }, ref) => (
50
- <div
51
- ref={ref}
52
- className={cn("text-sm [&_p]:leading-relaxed", className)}
53
- {...props}
54
- />
55
- ));
56
- AlertDescription.displayName = "AlertDescription";
57
-
58
- export { Alert, AlertTitle, AlertDescription };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ui/badge.tsx DELETED
@@ -1,35 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import { cn } from "../../lib/utils";
4
-
5
- const badgeVariants = cva(
6
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7
- {
8
- variants: {
9
- variant: {
10
- default:
11
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
- secondary:
13
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
- destructive:
15
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
- outline: "text-foreground",
17
- },
18
- },
19
- defaultVariants: {
20
- variant: "default",
21
- },
22
- }
23
- );
24
-
25
- export interface BadgeProps
26
- extends React.HTMLAttributes<HTMLDivElement>,
27
- VariantProps<typeof badgeVariants> {}
28
-
29
- function Badge({ className, variant, ...props }: BadgeProps) {
30
- return (
31
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
32
- );
33
- }
34
-
35
- export { Badge, badgeVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ui/button.tsx DELETED
@@ -1,53 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
- import { cn } from "../../lib/utils";
4
-
5
- const buttonVariants = cva(
6
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7
- {
8
- variants: {
9
- variant: {
10
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
- destructive:
12
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
- outline:
14
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15
- secondary:
16
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
17
- ghost: "hover:bg-accent hover:text-accent-foreground",
18
- link: "text-primary underline-offset-4 hover:underline",
19
- },
20
- size: {
21
- default: "h-10 px-4 py-2",
22
- sm: "h-9 rounded-md px-3",
23
- lg: "h-11 rounded-md px-8",
24
- icon: "h-10 w-10",
25
- },
26
- },
27
- defaultVariants: {
28
- variant: "default",
29
- size: "default",
30
- },
31
- }
32
- );
33
-
34
- export interface ButtonProps
35
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
36
- VariantProps<typeof buttonVariants> {
37
- asChild?: boolean;
38
- }
39
-
40
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
41
- ({ className, variant, size, asChild = false, ...props }, ref) => {
42
- return (
43
- <button
44
- className={cn(buttonVariants({ variant, size, className }))}
45
- ref={ref}
46
- {...props}
47
- />
48
- );
49
- }
50
- );
51
- Button.displayName = "Button";
52
-
53
- export { Button, buttonVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ui/card.tsx DELETED
@@ -1,85 +0,0 @@
1
- import * as React from "react";
2
- import { cn } from "../../lib/utils";
3
-
4
- const Card = React.forwardRef<
5
- HTMLDivElement,
6
- React.HTMLAttributes<HTMLDivElement>
7
- >(({ className, ...props }, ref) => (
8
- <div
9
- ref={ref}
10
- className={cn(
11
- "rounded-lg border bg-card text-card-foreground shadow-sm",
12
- className
13
- )}
14
- {...props}
15
- />
16
- ));
17
- Card.displayName = "Card";
18
-
19
- const CardHeader = React.forwardRef<
20
- HTMLDivElement,
21
- React.HTMLAttributes<HTMLDivElement>
22
- >(({ className, ...props }, ref) => (
23
- <div
24
- ref={ref}
25
- className={cn("flex flex-col space-y-1.5 p-6", className)}
26
- {...props}
27
- />
28
- ));
29
- CardHeader.displayName = "CardHeader";
30
-
31
- const CardTitle = React.forwardRef<
32
- HTMLParagraphElement,
33
- React.HTMLAttributes<HTMLHeadingElement>
34
- >(({ className, ...props }, ref) => (
35
- <h3
36
- ref={ref}
37
- className={cn(
38
- "text-2xl font-semibold leading-none tracking-tight",
39
- className
40
- )}
41
- {...props}
42
- />
43
- ));
44
- CardTitle.displayName = "CardTitle";
45
-
46
- const CardDescription = React.forwardRef<
47
- HTMLParagraphElement,
48
- React.HTMLAttributes<HTMLParagraphElement>
49
- >(({ className, ...props }, ref) => (
50
- <p
51
- ref={ref}
52
- className={cn("text-sm text-muted-foreground", className)}
53
- {...props}
54
- />
55
- ));
56
- CardDescription.displayName = "CardDescription";
57
-
58
- const CardContent = React.forwardRef<
59
- HTMLDivElement,
60
- React.HTMLAttributes<HTMLDivElement>
61
- >(({ className, ...props }, ref) => (
62
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
63
- ));
64
- CardContent.displayName = "CardContent";
65
-
66
- const CardFooter = React.forwardRef<
67
- HTMLDivElement,
68
- React.HTMLAttributes<HTMLDivElement>
69
- >(({ className, ...props }, ref) => (
70
- <div
71
- ref={ref}
72
- className={cn("flex items-center p-6 pt-0", className)}
73
- {...props}
74
- />
75
- ));
76
- CardFooter.displayName = "CardFooter";
77
-
78
- export {
79
- Card,
80
- CardHeader,
81
- CardFooter,
82
- CardTitle,
83
- CardDescription,
84
- CardContent,
85
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ui/dialog.tsx DELETED
@@ -1,120 +0,0 @@
1
- import * as React from "react";
2
- import * as DialogPrimitive from "@radix-ui/react-dialog";
3
- import { X } from "lucide-react";
4
-
5
- import { cn } from "../../lib/utils";
6
-
7
- const Dialog = DialogPrimitive.Root;
8
-
9
- const DialogTrigger = DialogPrimitive.Trigger;
10
-
11
- const DialogPortal = DialogPrimitive.Portal;
12
-
13
- const DialogClose = DialogPrimitive.Close;
14
-
15
- const DialogOverlay = React.forwardRef<
16
- React.ElementRef<typeof DialogPrimitive.Overlay>,
17
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18
- >(({ className, ...props }, ref) => (
19
- <DialogPrimitive.Overlay
20
- ref={ref}
21
- className={cn(
22
- "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
23
- className
24
- )}
25
- {...props}
26
- />
27
- ));
28
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29
-
30
- const DialogContent = React.forwardRef<
31
- React.ElementRef<typeof DialogPrimitive.Content>,
32
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
33
- >(({ className, children, ...props }, ref) => (
34
- <DialogPortal>
35
- <DialogOverlay />
36
- <DialogPrimitive.Content
37
- ref={ref}
38
- className={cn(
39
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
- className
41
- )}
42
- {...props}
43
- >
44
- {children}
45
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
46
- <X className="h-4 w-4" />
47
- <span className="sr-only">Close</span>
48
- </DialogPrimitive.Close>
49
- </DialogPrimitive.Content>
50
- </DialogPortal>
51
- ));
52
- DialogContent.displayName = DialogPrimitive.Content.displayName;
53
-
54
- const DialogHeader = ({
55
- className,
56
- ...props
57
- }: React.HTMLAttributes<HTMLDivElement>) => (
58
- <div
59
- className={cn(
60
- "flex flex-col space-y-1.5 text-center sm:text-left",
61
- className
62
- )}
63
- {...props}
64
- />
65
- );
66
- DialogHeader.displayName = "DialogHeader";
67
-
68
- const DialogFooter = ({
69
- className,
70
- ...props
71
- }: React.HTMLAttributes<HTMLDivElement>) => (
72
- <div
73
- className={cn(
74
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
75
- className
76
- )}
77
- {...props}
78
- />
79
- );
80
- DialogFooter.displayName = "DialogFooter";
81
-
82
- const DialogTitle = React.forwardRef<
83
- React.ElementRef<typeof DialogPrimitive.Title>,
84
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
85
- >(({ className, ...props }, ref) => (
86
- <DialogPrimitive.Title
87
- ref={ref}
88
- className={cn(
89
- "text-lg font-semibold leading-none tracking-tight",
90
- className
91
- )}
92
- {...props}
93
- />
94
- ));
95
- DialogTitle.displayName = DialogPrimitive.Title.displayName;
96
-
97
- const DialogDescription = React.forwardRef<
98
- React.ElementRef<typeof DialogPrimitive.Description>,
99
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
100
- >(({ className, ...props }, ref) => (
101
- <DialogPrimitive.Description
102
- ref={ref}
103
- className={cn("text-sm text-muted-foreground", className)}
104
- {...props}
105
- />
106
- ));
107
- DialogDescription.displayName = DialogPrimitive.Description.displayName;
108
-
109
- export {
110
- Dialog,
111
- DialogPortal,
112
- DialogOverlay,
113
- DialogClose,
114
- DialogContent,
115
- DialogDescription,
116
- DialogFooter,
117
- DialogHeader,
118
- DialogTitle,
119
- DialogTrigger,
120
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/components/ui/progress.tsx DELETED
@@ -1,26 +0,0 @@
1
- import * as React from "react";
2
- import * as ProgressPrimitive from "@radix-ui/react-progress";
3
-
4
- import { cn } from "../../lib/utils";
5
-
6
- const Progress = React.forwardRef<
7
- React.ElementRef<typeof ProgressPrimitive.Root>,
8
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
9
- >(({ className, value, ...props }, ref) => (
10
- <ProgressPrimitive.Root
11
- ref={ref}
12
- className={cn(
13
- "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
14
- className
15
- )}
16
- {...props}
17
- >
18
- <ProgressPrimitive.Indicator
19
- className="h-full w-full flex-1 bg-primary transition-all"
20
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
21
- />
22
- </ProgressPrimitive.Root>
23
- ));
24
- Progress.displayName = ProgressPrimitive.Root.displayName;
25
-
26
- export { Progress };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/index.css DELETED
@@ -1,12 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- @layer base {
6
- * {
7
- @apply border-border;
8
- }
9
- body {
10
- @apply bg-background text-foreground;
11
- }
12
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/lib/unified-storage.ts DELETED
@@ -1,325 +0,0 @@
1
- // Unified storage system for robot data
2
- // Consolidates robot config, calibration data, and metadata under one key per device
3
-
4
- export interface UnifiedRobotData {
5
- device_info: {
6
- serialNumber: string;
7
- robotType: "so100_follower" | "so100_leader";
8
- robotId: string;
9
- usbMetadata?: any;
10
- lastUpdated: string;
11
- };
12
- calibration?: {
13
- // Motor calibration data (from lerobot_calibration_* keys)
14
- shoulder_pan?: {
15
- id: number;
16
- drive_mode: number;
17
- homing_offset: number;
18
- range_min: number;
19
- range_max: number;
20
- };
21
- shoulder_lift?: {
22
- id: number;
23
- drive_mode: number;
24
- homing_offset: number;
25
- range_min: number;
26
- range_max: number;
27
- };
28
- elbow_flex?: {
29
- id: number;
30
- drive_mode: number;
31
- homing_offset: number;
32
- range_min: number;
33
- range_max: number;
34
- };
35
- wrist_flex?: {
36
- id: number;
37
- drive_mode: number;
38
- homing_offset: number;
39
- range_min: number;
40
- range_max: number;
41
- };
42
- wrist_roll?: {
43
- id: number;
44
- drive_mode: number;
45
- homing_offset: number;
46
- range_min: number;
47
- range_max: number;
48
- };
49
- gripper?: {
50
- id: number;
51
- drive_mode: number;
52
- homing_offset: number;
53
- range_min: number;
54
- range_max: number;
55
- };
56
-
57
- // Calibration metadata (from lerobot-calibration-* keys)
58
- metadata: {
59
- timestamp: string;
60
- readCount: number;
61
- platform: string;
62
- api: string;
63
- device_type: string;
64
- device_id: string;
65
- calibrated_at: string;
66
- };
67
- };
68
- }
69
-
70
- /**
71
- * Get unified storage key for a robot by serial number
72
- */
73
- export function getUnifiedKey(serialNumber: string): string {
74
- return `lerobotjs-${serialNumber}`;
75
- }
76
-
77
- /**
78
- * Migrate data from old storage keys to unified format
79
- * Safely combines data from three sources:
80
- * 1. lerobot-robot-{serialNumber} - robot config
81
- * 2. lerobot-calibration-{serialNumber} - calibration metadata
82
- * 3. lerobot_calibration_{robotType}_{robotId} - actual calibration data
83
- */
84
- export function migrateToUnifiedStorage(
85
- serialNumber: string
86
- ): UnifiedRobotData | null {
87
- try {
88
- const unifiedKey = getUnifiedKey(serialNumber);
89
-
90
- // Check if already migrated
91
- const existing = localStorage.getItem(unifiedKey);
92
- if (existing) {
93
- console.log(`✅ Data already unified for ${serialNumber}`);
94
- return JSON.parse(existing);
95
- }
96
-
97
- console.log(`🔄 Migrating data for serial number: ${serialNumber}`);
98
-
99
- // 1. Get robot configuration
100
- const robotConfigKey = `lerobot-robot-${serialNumber}`;
101
- const robotConfigRaw = localStorage.getItem(robotConfigKey);
102
-
103
- if (!robotConfigRaw) {
104
- return null;
105
- }
106
-
107
- const robotConfig = JSON.parse(robotConfigRaw);
108
- console.log(`📋 Found robot config:`, robotConfig);
109
-
110
- // 2. Get calibration metadata
111
- const calibrationMetaKey = `lerobot-calibration-${serialNumber}`;
112
- const calibrationMetaRaw = localStorage.getItem(calibrationMetaKey);
113
- const calibrationMeta = calibrationMetaRaw
114
- ? JSON.parse(calibrationMetaRaw)
115
- : null;
116
- console.log(`📊 Found calibration metadata:`, calibrationMeta);
117
-
118
- // 3. Get actual calibration data (using robotType and robotId from config)
119
- const calibrationDataKey = `lerobot_calibration_${robotConfig.robotType}_${robotConfig.robotId}`;
120
- const calibrationDataRaw = localStorage.getItem(calibrationDataKey);
121
- const calibrationData = calibrationDataRaw
122
- ? JSON.parse(calibrationDataRaw)
123
- : null;
124
- console.log(`🔧 Found calibration data:`, calibrationData);
125
-
126
- // 4. Build unified structure
127
- const unifiedData: UnifiedRobotData = {
128
- device_info: {
129
- serialNumber: robotConfig.serialNumber || serialNumber,
130
- robotType: robotConfig.robotType,
131
- robotId: robotConfig.robotId,
132
- lastUpdated: robotConfig.lastUpdated || new Date().toISOString(),
133
- },
134
- };
135
-
136
- // Add calibration if available
137
- if (calibrationData && calibrationMeta) {
138
- const motors: any = {};
139
-
140
- // Copy motor data (excluding metadata fields)
141
- Object.keys(calibrationData).forEach((key) => {
142
- if (
143
- ![
144
- "device_type",
145
- "device_id",
146
- "calibrated_at",
147
- "platform",
148
- "api",
149
- ].includes(key)
150
- ) {
151
- motors[key] = calibrationData[key];
152
- }
153
- });
154
-
155
- unifiedData.calibration = {
156
- ...motors,
157
- metadata: {
158
- timestamp: calibrationMeta.timestamp || calibrationData.calibrated_at,
159
- readCount: calibrationMeta.readCount || 0,
160
- platform: calibrationData.platform || "web",
161
- api: calibrationData.api || "Web Serial API",
162
- device_type: calibrationData.device_type || robotConfig.robotType,
163
- device_id: calibrationData.device_id || robotConfig.robotId,
164
- calibrated_at:
165
- calibrationData.calibrated_at || calibrationMeta.timestamp,
166
- },
167
- };
168
- }
169
-
170
- // 5. Save unified data
171
- localStorage.setItem(unifiedKey, JSON.stringify(unifiedData));
172
- console.log(`✅ Successfully unified data for ${serialNumber}`);
173
- console.log(`📦 Unified data:`, unifiedData);
174
-
175
- // 6. Clean up old keys (optional - keep for now for safety)
176
- // localStorage.removeItem(robotConfigKey);
177
- // localStorage.removeItem(calibrationMetaKey);
178
- // localStorage.removeItem(calibrationDataKey);
179
-
180
- return unifiedData;
181
- } catch (error) {
182
- console.error(`❌ Failed to migrate data for ${serialNumber}:`, error);
183
- return null;
184
- }
185
- }
186
-
187
- /**
188
- * Get unified robot data
189
- */
190
- export function getUnifiedRobotData(
191
- serialNumber: string
192
- ): UnifiedRobotData | null {
193
- const unifiedKey = getUnifiedKey(serialNumber);
194
-
195
- // Try to get existing unified data
196
- const existing = localStorage.getItem(unifiedKey);
197
- if (existing) {
198
- try {
199
- return JSON.parse(existing);
200
- } catch (error) {
201
- console.warn(`Failed to parse unified data for ${serialNumber}:`, error);
202
- }
203
- }
204
-
205
- return null;
206
- }
207
-
208
- /**
209
- * Save robot configuration to unified storage
210
- */
211
- export function saveRobotConfig(
212
- serialNumber: string,
213
- robotType: "so100_follower" | "so100_leader",
214
- robotId: string,
215
- usbMetadata?: any
216
- ): void {
217
- const unifiedKey = getUnifiedKey(serialNumber);
218
- const existing =
219
- getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
220
-
221
- existing.device_info = {
222
- serialNumber,
223
- robotType,
224
- robotId,
225
- usbMetadata,
226
- lastUpdated: new Date().toISOString(),
227
- };
228
-
229
- localStorage.setItem(unifiedKey, JSON.stringify(existing));
230
- console.log(`💾 Saved robot config for ${serialNumber}`);
231
- }
232
-
233
- /**
234
- * Save calibration data to unified storage
235
- */
236
- export function saveCalibrationData(
237
- serialNumber: string,
238
- calibrationData: any,
239
- metadata: { timestamp: string; readCount: number }
240
- ): void {
241
- const unifiedKey = getUnifiedKey(serialNumber);
242
- const existing =
243
- getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
244
-
245
- // Ensure device_info exists
246
- if (!existing.device_info) {
247
- console.warn(
248
- `No device info found for ${serialNumber}, cannot save calibration`
249
- );
250
- return;
251
- }
252
-
253
- // Extract motor data (exclude metadata fields)
254
- const motors: any = {};
255
- Object.keys(calibrationData).forEach((key) => {
256
- if (
257
- ![
258
- "device_type",
259
- "device_id",
260
- "calibrated_at",
261
- "platform",
262
- "api",
263
- ].includes(key)
264
- ) {
265
- motors[key] = calibrationData[key];
266
- }
267
- });
268
-
269
- existing.calibration = {
270
- ...motors,
271
- metadata: {
272
- timestamp: metadata.timestamp,
273
- readCount: metadata.readCount,
274
- platform: calibrationData.platform || "web",
275
- api: calibrationData.api || "Web Serial API",
276
- device_type:
277
- calibrationData.device_type || existing.device_info.robotType,
278
- device_id: calibrationData.device_id || existing.device_info.robotId,
279
- calibrated_at: calibrationData.calibrated_at || metadata.timestamp,
280
- },
281
- };
282
-
283
- localStorage.setItem(unifiedKey, JSON.stringify(existing));
284
- console.log(`🔧 Saved calibration data for ${serialNumber}`);
285
- }
286
-
287
- /**
288
- * Check if robot is calibrated
289
- */
290
- export function isRobotCalibrated(serialNumber: string): boolean {
291
- const data = getUnifiedRobotData(serialNumber);
292
- return !!data?.calibration?.metadata?.timestamp;
293
- }
294
-
295
- /**
296
- * Get calibration status for dashboard
297
- */
298
- export function getCalibrationStatus(
299
- serialNumber: string
300
- ): { timestamp: string; readCount: number } | null {
301
- const data = getUnifiedRobotData(serialNumber);
302
- if (data?.calibration?.metadata) {
303
- return {
304
- timestamp: data.calibration.metadata.timestamp,
305
- readCount: data.calibration.metadata.readCount,
306
- };
307
- }
308
- return null;
309
- }
310
-
311
- /**
312
- * Get robot configuration
313
- */
314
- export function getRobotConfig(
315
- serialNumber: string
316
- ): { robotType: string; robotId: string } | null {
317
- const data = getUnifiedRobotData(serialNumber);
318
- if (data?.device_info) {
319
- return {
320
- robotType: data.device_info.robotType,
321
- robotId: data.device_info.robotId,
322
- };
323
- }
324
- return null;
325
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/robot-control-web/lib/utils.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type ClassValue, clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
 
 
 
 
 
 
 
examples/robot-control-web/main.tsx DELETED
@@ -1,5 +0,0 @@
1
- import ReactDOM from "react-dom/client";
2
- import { App } from "./App";
3
- import "./index.css";
4
-
5
- ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
 
 
 
 
 
 
examples/robot-control-web/pages/Home.tsx DELETED
@@ -1,99 +0,0 @@
1
- import { useState } from "react";
2
- import { Button } from "../components/ui/button";
3
- import { Alert, AlertDescription } from "../components/ui/alert";
4
- import { PortManager } from "../components/PortManager";
5
- import { CalibrationPanel } from "../components/CalibrationPanel";
6
- import { TeleoperationPanel } from "../components/TeleoperationPanel";
7
- import { isWebSerialSupported } from "@lerobot/web";
8
- import type { RobotConnection } from "@lerobot/web";
9
-
10
- interface HomeProps {
11
- connectedRobots: RobotConnection[];
12
- onConnectedRobotsChange: (robots: RobotConnection[]) => void;
13
- }
14
-
15
- export function Home({ connectedRobots, onConnectedRobotsChange }: HomeProps) {
16
- const [calibratingRobot, setCalibratingRobot] =
17
- useState<RobotConnection | null>(null);
18
- const [teleoperatingRobot, setTeleoperatingRobot] =
19
- useState<RobotConnection | null>(null);
20
- const isSupported = isWebSerialSupported();
21
-
22
- const handleCalibrate = (port: SerialPort) => {
23
- // Find the robot from connectedRobots
24
- const robot = connectedRobots.find((r) => r.port === port);
25
- if (robot) {
26
- setCalibratingRobot(robot);
27
- }
28
- };
29
-
30
- const handleTeleoperate = (robot: RobotConnection) => {
31
- setTeleoperatingRobot(robot);
32
- };
33
-
34
- const handleFinishCalibration = () => {
35
- setCalibratingRobot(null);
36
- };
37
-
38
- const handleFinishTeleoperation = () => {
39
- setTeleoperatingRobot(null);
40
- };
41
-
42
- return (
43
- <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
44
- <div className="container mx-auto px-6 py-12">
45
- {/* Header */}
46
- <div className="text-center mb-12">
47
- <h1 className="text-4xl font-bold text-gray-900 mb-4">
48
- 🤖 LeRobot.js
49
- </h1>
50
- <p className="text-xl text-gray-600 mb-8">
51
- Robotics for the web and node
52
- </p>
53
-
54
- {!isSupported && (
55
- <Alert variant="destructive" className="max-w-2xl mx-auto mb-8">
56
- <AlertDescription>
57
- Web Serial API is not supported in this browser. Please use
58
- Chrome, Edge, or another Chromium-based browser to use this
59
- demo.
60
- </AlertDescription>
61
- </Alert>
62
- )}
63
- </div>
64
-
65
- {/* Main Content */}
66
- {calibratingRobot ? (
67
- <div className="max-w-6xl mx-auto">
68
- <div className="mb-4">
69
- <Button
70
- variant="outline"
71
- onClick={() => setCalibratingRobot(null)}
72
- >
73
- ← Back to Dashboard
74
- </Button>
75
- </div>
76
- <CalibrationPanel
77
- robot={calibratingRobot}
78
- onFinish={handleFinishCalibration}
79
- />
80
- </div>
81
- ) : teleoperatingRobot ? (
82
- <TeleoperationPanel
83
- robot={teleoperatingRobot}
84
- onClose={handleFinishTeleoperation}
85
- />
86
- ) : (
87
- <div className="max-w-6xl mx-auto">
88
- <PortManager
89
- onCalibrate={handleCalibrate}
90
- onTeleoperate={handleTeleoperate}
91
- connectedRobots={connectedRobots}
92
- onConnectedRobotsChange={onConnectedRobotsChange}
93
- />
94
- </div>
95
- )}
96
- </div>
97
- </div>
98
- );
99
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html DELETED
@@ -1,81 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>🤖 lerobot.js - React Demo</title>
7
- <meta
8
- name="description"
9
- content="State-of-the-art AI for real-world robotics in JavaScript/TypeScript - Interactive React Demo"
10
- />
11
- <style>
12
- :root {
13
- --background: 0 0% 100%;
14
- --foreground: 240 10% 3.9%;
15
- --card: 0 0% 100%;
16
- --card-foreground: 240 10% 3.9%;
17
- --popover: 0 0% 100%;
18
- --popover-foreground: 240 10% 3.9%;
19
- --primary: 240 9% 17%;
20
- --primary-foreground: 0 0% 98%;
21
- --secondary: 240 4.8% 95.9%;
22
- --secondary-foreground: 240 5.9% 10%;
23
- --muted: 240 4.8% 95.9%;
24
- --muted-foreground: 240 3.8% 46.1%;
25
- --accent: 240 4.8% 95.9%;
26
- --accent-foreground: 240 5.9% 10%;
27
- --destructive: 0 84.2% 60.2%;
28
- --destructive-foreground: 0 0% 98%;
29
- --border: 240 5.9% 90%;
30
- --input: 240 5.9% 90%;
31
- --ring: 240 10% 3.9%;
32
- --chart-1: 12 76% 61%;
33
- --chart-2: 173 58% 39%;
34
- --chart-3: 197 37% 24%;
35
- --chart-4: 43 74% 66%;
36
- --chart-5: 27 87% 67%;
37
- --radius: 0.5rem;
38
- }
39
-
40
- .dark {
41
- --background: 240 10% 3.9%;
42
- --foreground: 0 0% 98%;
43
- --card: 240 10% 3.9%;
44
- --card-foreground: 0 0% 98%;
45
- --popover: 240 10% 3.9%;
46
- --popover-foreground: 0 0% 98%;
47
- --primary: 0 0% 98%;
48
- --primary-foreground: 240 5.9% 10%;
49
- --secondary: 240 3.7% 15.9%;
50
- --secondary-foreground: 0 0% 98%;
51
- --muted: 240 3.7% 15.9%;
52
- --muted-foreground: 240 5% 64.9%;
53
- --accent: 240 3.7% 15.9%;
54
- --accent-foreground: 0 0% 98%;
55
- --destructive: 0 62.8% 30.6%;
56
- --destructive-foreground: 0 0% 98%;
57
- --border: 240 3.7% 15.9%;
58
- --input: 240 3.7% 15.9%;
59
- --ring: 240 4.9% 83.9%;
60
- --chart-1: 220 70% 50%;
61
- --chart-2: 160 60% 45%;
62
- --chart-3: 30 80% 55%;
63
- --chart-4: 280 65% 60%;
64
- --chart-5: 340 75% 55%;
65
- }
66
-
67
- * {
68
- border-color: hsl(var(--border));
69
- }
70
-
71
- body {
72
- background-color: hsl(var(--background));
73
- color: hsl(var(--foreground));
74
- }
75
- </style>
76
- </head>
77
- <body>
78
- <div id="root"></div>
79
- <script type="module" src="/examples/robot-control-web/main.tsx"></script>
80
- </body>
81
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
postcss.config.mjs DELETED
@@ -1,6 +0,0 @@
1
- import tailwindcss from "tailwindcss";
2
- import autoprefixer from "autoprefixer";
3
-
4
- export default {
5
- plugins: [tailwindcss, autoprefixer],
6
- };
 
 
 
 
 
 
 
src/web_interface.css DELETED
@@ -1,238 +0,0 @@
1
- /* lerobot.js Web Interface Styles */
2
-
3
- * {
4
- margin: 0;
5
- padding: 0;
6
- box-sizing: border-box;
7
- }
8
-
9
- body {
10
- font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
11
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
- min-height: 100vh;
13
- display: flex;
14
- align-items: center;
15
- justify-content: center;
16
- color: #333;
17
- }
18
-
19
- .lerobot-app {
20
- background: white;
21
- border-radius: 12px;
22
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
23
- max-width: 800px;
24
- width: 90%;
25
- padding: 2rem;
26
- margin: 2rem;
27
- }
28
-
29
- .lerobot-header {
30
- text-align: center;
31
- margin-bottom: 2rem;
32
- padding-bottom: 1rem;
33
- border-bottom: 1px solid #eee;
34
- }
35
-
36
- .lerobot-header h1 {
37
- font-size: 2.5rem;
38
- color: #2c3e50;
39
- margin-bottom: 0.5rem;
40
- }
41
-
42
- .lerobot-header p {
43
- color: #7f8c8d;
44
- font-size: 1.1rem;
45
- }
46
-
47
- .lerobot-main {
48
- display: grid;
49
- gap: 2rem;
50
- }
51
-
52
- .tool-section {
53
- background: #f8f9fa;
54
- padding: 1.5rem;
55
- border-radius: 8px;
56
- border-left: 4px solid #3498db;
57
- }
58
-
59
- .tool-section h2 {
60
- color: #2c3e50;
61
- margin-bottom: 0.5rem;
62
- font-size: 1.4rem;
63
- }
64
-
65
- .tool-section p {
66
- color: #7f8c8d;
67
- margin-bottom: 1rem;
68
- }
69
-
70
- .button-group {
71
- display: flex;
72
- gap: 0.5rem;
73
- margin-bottom: 1rem;
74
- flex-wrap: wrap;
75
- }
76
-
77
- .info-box {
78
- background: #e8f4fd;
79
- border: 1px solid #3498db;
80
- border-radius: 6px;
81
- padding: 12px 16px;
82
- margin-bottom: 1rem;
83
- display: flex;
84
- align-items: center;
85
- gap: 8px;
86
- color: #2c3e50;
87
- }
88
-
89
- .info-icon {
90
- font-size: 1.1rem;
91
- flex-shrink: 0;
92
- }
93
-
94
- .info-text {
95
- font-size: 0.95rem;
96
- line-height: 1.4;
97
- }
98
-
99
- .primary-btn,
100
- .secondary-btn {
101
- border: none;
102
- padding: 12px 24px;
103
- border-radius: 6px;
104
- font-size: 1rem;
105
- font-weight: 600;
106
- cursor: pointer;
107
- transition: all 0.3s ease;
108
- }
109
-
110
- .primary-btn {
111
- background: linear-gradient(135deg, #3498db, #2980b9);
112
- color: white;
113
- }
114
-
115
- .secondary-btn {
116
- background: linear-gradient(135deg, #95a5a6, #7f8c8d);
117
- color: white;
118
- }
119
-
120
- .primary-btn:hover:not(:disabled) {
121
- transform: translateY(-2px);
122
- box-shadow: 0 6px 12px rgba(52, 152, 219, 0.3);
123
- }
124
-
125
- .secondary-btn:hover:not(:disabled) {
126
- transform: translateY(-2px);
127
- box-shadow: 0 6px 12px rgba(149, 165, 166, 0.3);
128
- }
129
-
130
- .primary-btn:disabled,
131
- .secondary-btn:disabled {
132
- opacity: 0.6;
133
- cursor: not-allowed;
134
- transform: none;
135
- }
136
-
137
- .results-area {
138
- background: #2c3e50;
139
- color: #ecf0f1;
140
- padding: 1rem;
141
- border-radius: 6px;
142
- font-family: "Fira Code", "Courier New", monospace;
143
- font-size: 0.9rem;
144
- line-height: 1.4;
145
- max-height: 300px;
146
- overflow-y: auto;
147
- min-height: 100px;
148
- }
149
-
150
- .results-area:empty::before {
151
- content: "Results will appear here...";
152
- color: #7f8c8d;
153
- font-style: italic;
154
- }
155
-
156
- .results-area .log {
157
- margin: 0.25rem 0;
158
- color: #bdc3c7;
159
- }
160
-
161
- .results-area .status {
162
- margin: 0.25rem 0;
163
- color: #3498db;
164
- font-weight: 600;
165
- }
166
-
167
- .results-area .error {
168
- margin: 0.25rem 0;
169
- color: #e74c3c;
170
- font-weight: 600;
171
- }
172
-
173
- .results-area .success {
174
- margin: 0.25rem 0;
175
- color: #27ae60;
176
- font-weight: 600;
177
- }
178
-
179
- .info-section {
180
- background: #fff3cd;
181
- padding: 1.5rem;
182
- border-radius: 8px;
183
- border-left: 4px solid #ffc107;
184
- }
185
-
186
- .info-section h3 {
187
- color: #856404;
188
- margin-bottom: 0.5rem;
189
- font-size: 1.2rem;
190
- }
191
-
192
- .info-section p {
193
- color: #856404;
194
- margin-bottom: 0.5rem;
195
- }
196
-
197
- .info-section ul {
198
- color: #856404;
199
- margin-left: 1.5rem;
200
- }
201
-
202
- .info-section li {
203
- margin-bottom: 0.25rem;
204
- }
205
-
206
- .info-section a {
207
- color: #0056b3;
208
- text-decoration: none;
209
- }
210
-
211
- .info-section a:hover {
212
- text-decoration: underline;
213
- }
214
-
215
- .info-section code {
216
- background: #f5f5f5;
217
- padding: 2px 6px;
218
- border-radius: 3px;
219
- font-family: "Fira Code", "Courier New", monospace;
220
- font-size: 0.9em;
221
- color: #333;
222
- }
223
-
224
- /* Responsive design */
225
- @media (max-width: 600px) {
226
- .lerobot-app {
227
- margin: 1rem;
228
- padding: 1.5rem;
229
- }
230
-
231
- .lerobot-header h1 {
232
- font-size: 2rem;
233
- }
234
-
235
- .lerobot-header p {
236
- font-size: 1rem;
237
- }
238
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tailwind.config.js DELETED
@@ -1,59 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- export default {
3
- content: [
4
- "./index.html",
5
- "./examples/robot-control-web/**/*.{js,ts,jsx,tsx}",
6
- ],
7
- theme: {
8
- extend: {
9
- borderRadius: {
10
- lg: "var(--radius)",
11
- md: "calc(var(--radius) - 2px)",
12
- sm: "calc(var(--radius) - 4px)",
13
- },
14
- colors: {
15
- background: "hsl(var(--background))",
16
- foreground: "hsl(var(--foreground))",
17
- card: {
18
- DEFAULT: "hsl(var(--card))",
19
- foreground: "hsl(var(--foreground))",
20
- },
21
- popover: {
22
- DEFAULT: "hsl(var(--popover))",
23
- foreground: "hsl(var(--popover-foreground))",
24
- },
25
- primary: {
26
- DEFAULT: "hsl(var(--primary))",
27
- foreground: "hsl(var(--primary-foreground))",
28
- },
29
- secondary: {
30
- DEFAULT: "hsl(var(--secondary))",
31
- foreground: "hsl(var(--secondary-foreground))",
32
- },
33
- muted: {
34
- DEFAULT: "hsl(var(--muted))",
35
- foreground: "hsl(var(--muted-foreground))",
36
- },
37
- accent: {
38
- DEFAULT: "hsl(var(--accent))",
39
- foreground: "hsl(var(--accent-foreground))",
40
- },
41
- destructive: {
42
- DEFAULT: "hsl(var(--destructive))",
43
- foreground: "hsl(var(--destructive-foreground))",
44
- },
45
- border: "hsl(var(--border))",
46
- input: "hsl(var(--input))",
47
- ring: "hsl(var(--ring))",
48
- chart: {
49
- 1: "hsl(var(--chart-1))",
50
- 2: "hsl(var(--chart-2))",
51
- 3: "hsl(var(--chart-3))",
52
- 4: "hsl(var(--chart-4))",
53
- 5: "hsl(var(--chart-5))",
54
- },
55
- },
56
- },
57
- },
58
- plugins: [],
59
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tsconfig.json DELETED
@@ -1,28 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "useDefineForClassFields": true,
5
- "module": "ESNext",
6
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
- "skipLibCheck": true,
8
-
9
- /* React */
10
- "jsx": "react-jsx",
11
-
12
- /* Bundler mode */
13
- "moduleResolution": "bundler",
14
- "allowImportingTsExtensions": true,
15
- "verbatimModuleSyntax": true,
16
- "moduleDetection": "force",
17
- "noEmit": true,
18
-
19
- /* Linting */
20
- "strict": true,
21
- "noUnusedLocals": true,
22
- "noUnusedParameters": true,
23
- "erasableSyntaxOnly": true,
24
- "noFallthroughCasesInSwitch": true,
25
- "noUncheckedSideEffectImports": true
26
- },
27
- "include": ["src", "examples/robot-control-web"]
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vite.config.ts DELETED
@@ -1,83 +0,0 @@
1
- import { defineConfig } from "vite";
2
- import react from "@vitejs/plugin-react";
3
- import { resolve } from "path";
4
- import { existsSync } from "fs";
5
-
6
- export default defineConfig(({ mode }) => {
7
- // Check if we're in a workspace environment (has packages/web/src)
8
- const isWorkspace = existsSync(resolve(__dirname, "./packages/web/src"));
9
-
10
- const baseConfig = {
11
- plugins: [],
12
- resolve: {
13
- alias: {
14
- "@": resolve(__dirname, "./src"),
15
- // Only add workspace alias if in workspace environment
16
- ...(isWorkspace && {
17
- "@lerobot/web": resolve(__dirname, "./packages/web/src"),
18
- }),
19
- },
20
- },
21
- };
22
-
23
- if (mode === "demo") {
24
- // React demo mode - includes React, Tailwind, shadcn/ui
25
- return {
26
- ...baseConfig,
27
- plugins: [react()],
28
- css: {
29
- postcss: "./postcss.config.mjs",
30
- },
31
- build: {
32
- outDir: "dist/demo",
33
- rollupOptions: {
34
- input: {
35
- main: resolve(__dirname, "index.html"),
36
- },
37
- },
38
- },
39
- };
40
- }
41
-
42
- if (mode === "vanilla") {
43
- // Vanilla mode - current implementation without React
44
- return {
45
- ...baseConfig,
46
- build: {
47
- outDir: "dist/vanilla",
48
- rollupOptions: {
49
- input: {
50
- main: resolve(__dirname, "vanilla.html"),
51
- },
52
- },
53
- },
54
- };
55
- }
56
-
57
- if (mode === "lib") {
58
- // Library mode - core library without any demo UI
59
- return {
60
- ...baseConfig,
61
- build: {
62
- lib: {
63
- entry: resolve(__dirname, "src/main.ts"),
64
- name: "LeRobot",
65
- fileName: "lerobot",
66
- },
67
- rollupOptions: {
68
- external: ["serialport", "react", "react-dom"],
69
- output: {
70
- globals: {
71
- serialport: "SerialPort",
72
- react: "React",
73
- "react-dom": "ReactDOM",
74
- },
75
- },
76
- },
77
- },
78
- };
79
- }
80
-
81
- // Default mode (fallback to demo)
82
- return defineConfig({ mode: "demo" });
83
- });