Spaces:
Running
Running
feat(example): improved everything to make it release rdy
Browse files- examples/cyberpunk-standalone/src/App.tsx +3 -3
- examples/cyberpunk-standalone/src/components/calibration-view.tsx +138 -56
- examples/cyberpunk-standalone/src/components/device-dashboard.tsx +216 -162
- examples/cyberpunk-standalone/src/components/docs-section.tsx +127 -55
- examples/cyberpunk-standalone/src/components/footer.tsx +3 -3
- examples/cyberpunk-standalone/src/components/hardware-support-section.tsx +14 -27
- examples/cyberpunk-standalone/src/components/header.tsx +3 -3
- examples/cyberpunk-standalone/src/components/roadmap-section.tsx +61 -35
- examples/cyberpunk-standalone/src/components/setup-cards.tsx +46 -30
examples/cyberpunk-standalone/src/App.tsx
CHANGED
@@ -267,7 +267,7 @@ function App() {
|
|
267 |
case "dashboard":
|
268 |
default:
|
269 |
return (
|
270 |
-
<div className="space-y-
|
271 |
<DeviceDashboard
|
272 |
robots={robots}
|
273 |
onCalibrate={handleCalibrate}
|
@@ -280,7 +280,7 @@ function App() {
|
|
280 |
/>
|
281 |
<div>
|
282 |
<div className="mb-6">
|
283 |
-
<h2 className="text-
|
284 |
install
|
285 |
</h2>
|
286 |
<p className="text-sm text-muted-foreground font-mono">
|
@@ -356,7 +356,7 @@ function App() {
|
|
356 |
};
|
357 |
|
358 |
return (
|
359 |
-
<div className="flex flex-col min-h-screen font-sans">
|
360 |
<Header />
|
361 |
<main className="flex-grow container mx-auto py-12 px-4 md:px-6">
|
362 |
<PageHeader />
|
|
|
267 |
case "dashboard":
|
268 |
default:
|
269 |
return (
|
270 |
+
<div className="space-y-20">
|
271 |
<DeviceDashboard
|
272 |
robots={robots}
|
273 |
onCalibrate={handleCalibrate}
|
|
|
280 |
/>
|
281 |
<div>
|
282 |
<div className="mb-6">
|
283 |
+
<h2 className="text-3xl font-bold font-mono tracking-wider mb-2 uppercase">
|
284 |
install
|
285 |
</h2>
|
286 |
<p className="text-sm text-muted-foreground font-mono">
|
|
|
356 |
};
|
357 |
|
358 |
return (
|
359 |
+
<div className="flex flex-col min-h-screen font-sans bg-gray-200 dark:bg-background">
|
360 |
<Header />
|
361 |
<main className="flex-grow container mx-auto py-12 px-4 md:px-6">
|
362 |
<PageHeader />
|
examples/cyberpunk-standalone/src/components/calibration-view.tsx
CHANGED
@@ -3,6 +3,16 @@ import { useState, useMemo, useEffect, useCallback } from "react";
|
|
3 |
import { Download } from "lucide-react";
|
4 |
import { Button } from "@/components/ui/button";
|
5 |
import { Card } from "@/components/ui/card";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
import { useToast } from "@/hooks/use-toast";
|
7 |
import {
|
8 |
calibrate,
|
@@ -28,6 +38,7 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
|
|
28 |
const [liveData, setLiveData] = useState<LiveCalibrationData | null>(null);
|
29 |
const [isCalibrating, setIsCalibrating] = useState(false);
|
30 |
const [isPreparing, setIsPreparing] = useState(false);
|
|
|
31 |
const [calibrationProcess, setCalibrationProcess] =
|
32 |
useState<CalibrationProcess | null>(null);
|
33 |
const [calibrationResults, setCalibrationResults] =
|
@@ -86,12 +97,38 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
|
|
86 |
|
87 |
const handleStart = async () => {
|
88 |
try {
|
89 |
-
|
90 |
-
setStatus("🤖
|
91 |
|
92 |
// Release motors first
|
93 |
await releaseMotorTorque();
|
94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
// Start calibration process
|
96 |
const process = await calibrate({
|
97 |
robot,
|
@@ -162,6 +199,12 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
|
|
162 |
}
|
163 |
};
|
164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
165 |
const handleFinish = async () => {
|
166 |
if (calibrationProcess) {
|
167 |
try {
|
@@ -224,67 +267,106 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
|
|
224 |
);
|
225 |
|
226 |
return (
|
227 |
-
|
228 |
-
<
|
229 |
-
<div className="flex items-center
|
230 |
-
<div className="
|
231 |
-
|
232 |
-
<
|
233 |
-
|
234 |
-
|
235 |
-
|
|
|
|
|
|
|
|
|
236 |
</div>
|
237 |
-
|
238 |
-
|
239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
240 |
<Button
|
241 |
-
onClick={
|
|
|
242 |
size="lg"
|
243 |
-
disabled={
|
244 |
>
|
245 |
-
|
246 |
</Button>
|
247 |
-
|
248 |
-
<Button onClick={handleFinish} variant="destructive" size="lg">
|
249 |
-
Finish Recording
|
250 |
-
</Button>
|
251 |
-
)}
|
252 |
-
<Button
|
253 |
-
onClick={downloadJson}
|
254 |
-
variant="outline"
|
255 |
-
size="lg"
|
256 |
-
disabled={!calibrationResults}
|
257 |
-
>
|
258 |
-
<Download className="w-4 h-4 mr-2" /> Download JSON
|
259 |
-
</Button>
|
260 |
-
</div>
|
261 |
-
</div>
|
262 |
-
<div className="pt-6 p-6">
|
263 |
-
<div className="flex items-center gap-4 py-2 px-4 text-sm font-sans text-muted-foreground">
|
264 |
-
<div className="w-40">Motor Name</div>
|
265 |
-
<div className="flex-1">Visual Range</div>
|
266 |
-
<div className="w-16 text-right">Current</div>
|
267 |
-
<div className="w-16 text-right">Min</div>
|
268 |
-
<div className="w-16 text-right">Max</div>
|
269 |
-
<div className="w-16 text-right">Range</div>
|
270 |
</div>
|
271 |
-
<div className="
|
272 |
-
|
273 |
-
<
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
282 |
}
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
</div>
|
287 |
-
</
|
288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
);
|
290 |
}
|
|
|
3 |
import { Download } from "lucide-react";
|
4 |
import { Button } from "@/components/ui/button";
|
5 |
import { Card } from "@/components/ui/card";
|
6 |
+
import {
|
7 |
+
AlertDialog,
|
8 |
+
AlertDialogAction,
|
9 |
+
AlertDialogCancel,
|
10 |
+
AlertDialogContent,
|
11 |
+
AlertDialogDescription,
|
12 |
+
AlertDialogFooter,
|
13 |
+
AlertDialogHeader,
|
14 |
+
AlertDialogTitle,
|
15 |
+
} from "@/components/ui/alert-dialog";
|
16 |
import { useToast } from "@/hooks/use-toast";
|
17 |
import {
|
18 |
calibrate,
|
|
|
38 |
const [liveData, setLiveData] = useState<LiveCalibrationData | null>(null);
|
39 |
const [isCalibrating, setIsCalibrating] = useState(false);
|
40 |
const [isPreparing, setIsPreparing] = useState(false);
|
41 |
+
const [showHomingDialog, setShowHomingDialog] = useState(false);
|
42 |
const [calibrationProcess, setCalibrationProcess] =
|
43 |
useState<CalibrationProcess | null>(null);
|
44 |
const [calibrationResults, setCalibrationResults] =
|
|
|
97 |
|
98 |
const handleStart = async () => {
|
99 |
try {
|
100 |
+
setIsPreparing(true);
|
101 |
+
setStatus("🤖 Preparing for calibration...");
|
102 |
|
103 |
// Release motors first
|
104 |
await releaseMotorTorque();
|
105 |
|
106 |
+
// Show dialog asking user to put robot in homing position
|
107 |
+
setShowHomingDialog(true);
|
108 |
+
} catch (error) {
|
109 |
+
console.error("Failed to prepare for calibration:", error);
|
110 |
+
setStatus(
|
111 |
+
`❌ Preparation failed: ${
|
112 |
+
error instanceof Error ? error.message : error
|
113 |
+
}`
|
114 |
+
);
|
115 |
+
toast({
|
116 |
+
title: "Preparation Failed",
|
117 |
+
description:
|
118 |
+
error instanceof Error ? error.message : "An unknown error occurred",
|
119 |
+
variant: "destructive",
|
120 |
+
});
|
121 |
+
setIsPreparing(false);
|
122 |
+
}
|
123 |
+
};
|
124 |
+
|
125 |
+
const handleStartCalibration = async () => {
|
126 |
+
try {
|
127 |
+
setShowHomingDialog(false);
|
128 |
+
setIsCalibrating(true);
|
129 |
+
setIsPreparing(false);
|
130 |
+
setStatus("🤖 Starting calibration process...");
|
131 |
+
|
132 |
// Start calibration process
|
133 |
const process = await calibrate({
|
134 |
robot,
|
|
|
199 |
}
|
200 |
};
|
201 |
|
202 |
+
const handleCancelCalibration = () => {
|
203 |
+
setShowHomingDialog(false);
|
204 |
+
setIsPreparing(false);
|
205 |
+
setStatus("Ready to calibrate.");
|
206 |
+
};
|
207 |
+
|
208 |
const handleFinish = async () => {
|
209 |
if (calibrationProcess) {
|
210 |
try {
|
|
|
267 |
);
|
268 |
|
269 |
return (
|
270 |
+
<>
|
271 |
+
<Card className="border-0 rounded-none">
|
272 |
+
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
273 |
+
<div className="flex items-center gap-4">
|
274 |
+
<div className="w-1 h-8 bg-primary"></div>
|
275 |
+
<div>
|
276 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
277 |
+
motor calibration
|
278 |
+
</h3>
|
279 |
+
<p className="text-sm text-muted-foreground font-mono">
|
280 |
+
{status}
|
281 |
+
</p>
|
282 |
+
</div>
|
283 |
</div>
|
284 |
+
<div className="flex gap-4">
|
285 |
+
{!isCalibrating ? (
|
286 |
+
<Button
|
287 |
+
onClick={handleStart}
|
288 |
+
size="lg"
|
289 |
+
disabled={isPreparing || !robot.isConnected}
|
290 |
+
>
|
291 |
+
{isPreparing
|
292 |
+
? "Preparing..."
|
293 |
+
: calibrationResults
|
294 |
+
? "Re-calibrate"
|
295 |
+
: "Start Calibration"}
|
296 |
+
</Button>
|
297 |
+
) : (
|
298 |
+
<Button onClick={handleFinish} variant="destructive" size="lg">
|
299 |
+
Finish Recording
|
300 |
+
</Button>
|
301 |
+
)}
|
302 |
<Button
|
303 |
+
onClick={downloadJson}
|
304 |
+
variant="outline"
|
305 |
size="lg"
|
306 |
+
disabled={!calibrationResults}
|
307 |
>
|
308 |
+
<Download className="w-4 h-4 mr-2" /> Download JSON
|
309 |
</Button>
|
310 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
</div>
|
312 |
+
<div className="pt-6 p-6">
|
313 |
+
<div className="flex items-center gap-4 py-2 px-4 text-sm font-sans text-muted-foreground">
|
314 |
+
<div className="w-40">Motor Name</div>
|
315 |
+
<div className="flex-1">Visual Range</div>
|
316 |
+
<div className="w-16 text-right">Current</div>
|
317 |
+
<div className="w-16 text-right">Min</div>
|
318 |
+
<div className="w-16 text-right">Max</div>
|
319 |
+
<div className="w-16 text-right">Range</div>
|
320 |
+
</div>
|
321 |
+
<div className="border-t border-white/10">
|
322 |
+
{motorData.map(([name, data]) => (
|
323 |
+
<MotorCalibrationVisual
|
324 |
+
key={name as string}
|
325 |
+
name={name as string}
|
326 |
+
data={
|
327 |
+
data as {
|
328 |
+
current: number;
|
329 |
+
min: number;
|
330 |
+
max: number;
|
331 |
+
range: number;
|
332 |
+
}
|
333 |
}
|
334 |
+
/>
|
335 |
+
))}
|
336 |
+
</div>
|
337 |
</div>
|
338 |
+
</Card>
|
339 |
+
|
340 |
+
<AlertDialog open={showHomingDialog} onOpenChange={setShowHomingDialog}>
|
341 |
+
<AlertDialogContent>
|
342 |
+
<AlertDialogHeader>
|
343 |
+
<AlertDialogTitle>Position Robot for Calibration</AlertDialogTitle>
|
344 |
+
<AlertDialogDescription>
|
345 |
+
The motors have been released and are now free to move. Please
|
346 |
+
position the robot in its homing position:
|
347 |
+
<br />
|
348 |
+
<br />
|
349 |
+
• Move all joints to their center/neutral position
|
350 |
+
<br />
|
351 |
+
• Ensure the robot is in a comfortable, balanced pose
|
352 |
+
<br />
|
353 |
+
• Make sure all motors can move freely through their full range
|
354 |
+
<br />
|
355 |
+
<br />
|
356 |
+
Once the robot is positioned correctly, click "Start Calibration"
|
357 |
+
to begin recording joint ranges.
|
358 |
+
</AlertDialogDescription>
|
359 |
+
</AlertDialogHeader>
|
360 |
+
<AlertDialogFooter>
|
361 |
+
<AlertDialogCancel onClick={handleCancelCalibration}>
|
362 |
+
Cancel
|
363 |
+
</AlertDialogCancel>
|
364 |
+
<AlertDialogAction onClick={handleStartCalibration}>
|
365 |
+
Start Calibration
|
366 |
+
</AlertDialogAction>
|
367 |
+
</AlertDialogFooter>
|
368 |
+
</AlertDialogContent>
|
369 |
+
</AlertDialog>
|
370 |
+
</>
|
371 |
);
|
372 |
}
|
examples/cyberpunk-standalone/src/components/device-dashboard.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
"use client";
|
2 |
|
|
|
3 |
import {
|
4 |
Settings,
|
5 |
Gamepad2,
|
@@ -11,8 +12,19 @@ import {
|
|
11 |
import { Button } from "@/components/ui/button";
|
12 |
import { Card, CardFooter } from "@/components/ui/card";
|
13 |
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
import { cn } from "@/lib/utils";
|
15 |
import HudCorners from "@/components/hud-corners";
|
|
|
16 |
import type { RobotConnection } from "@/types/robot";
|
17 |
|
18 |
interface DeviceDashboardProps {
|
@@ -36,183 +48,225 @@ export function DeviceDashboard({
|
|
36 |
isConnecting,
|
37 |
onScrollToHardware,
|
38 |
}: DeviceDashboardProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
return (
|
40 |
-
|
41 |
-
<
|
42 |
-
<div className="flex items-center
|
43 |
-
<div className="
|
44 |
-
|
45 |
-
<
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
<
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
|
|
59 |
</div>
|
60 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
</div>
|
62 |
-
{robots.length > 0 && (
|
63 |
-
<Button
|
64 |
-
onClick={onFindNew}
|
65 |
-
disabled={isConnecting}
|
66 |
-
size="lg"
|
67 |
-
className="font-mono uppercase"
|
68 |
-
>
|
69 |
-
<Plus className="w-4 h-4 mr-2" />
|
70 |
-
add unit
|
71 |
-
</Button>
|
72 |
-
)}
|
73 |
-
</div>
|
74 |
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
112 |
</div>
|
113 |
-
</
|
114 |
-
</
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
</h4>
|
127 |
-
<div className="flex items-center gap-2 mt-1">
|
128 |
-
<span className="text-xs text-muted-foreground font-mono">
|
129 |
-
{robot.serialNumber}
|
130 |
-
</span>
|
131 |
-
<span className="text-xs text-muted-foreground">
|
132 |
-
•
|
133 |
-
</span>
|
134 |
-
<span className="text-xs font-mono uppercase text-muted-foreground">
|
135 |
-
{robot.robotType}
|
136 |
-
</span>
|
137 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
</div>
|
139 |
-
<Badge
|
140 |
-
variant="outline"
|
141 |
-
className={cn(
|
142 |
-
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
143 |
-
robot.isConnected && "animate-pulse-slow"
|
144 |
-
)}
|
145 |
-
>
|
146 |
-
{robot.isConnected ? "ONLINE" : "OFFLINE"}
|
147 |
-
</Badge>
|
148 |
</div>
|
149 |
-
</div>
|
150 |
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
|
|
159 |
</div>
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
</div>
|
168 |
</div>
|
169 |
</div>
|
170 |
-
</div>
|
171 |
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
217 |
);
|
218 |
}
|
|
|
1 |
"use client";
|
2 |
|
3 |
+
import { useState } from "react";
|
4 |
import {
|
5 |
Settings,
|
6 |
Gamepad2,
|
|
|
12 |
import { Button } from "@/components/ui/button";
|
13 |
import { Card, CardFooter } from "@/components/ui/card";
|
14 |
import { Badge } from "@/components/ui/badge";
|
15 |
+
import {
|
16 |
+
AlertDialog,
|
17 |
+
AlertDialogAction,
|
18 |
+
AlertDialogCancel,
|
19 |
+
AlertDialogContent,
|
20 |
+
AlertDialogDescription,
|
21 |
+
AlertDialogFooter,
|
22 |
+
AlertDialogHeader,
|
23 |
+
AlertDialogTitle,
|
24 |
+
} from "@/components/ui/alert-dialog";
|
25 |
import { cn } from "@/lib/utils";
|
26 |
import HudCorners from "@/components/hud-corners";
|
27 |
+
import { getUnifiedRobotData } from "@/lib/unified-storage";
|
28 |
import type { RobotConnection } from "@/types/robot";
|
29 |
|
30 |
interface DeviceDashboardProps {
|
|
|
48 |
isConnecting,
|
49 |
onScrollToHardware,
|
50 |
}: DeviceDashboardProps) {
|
51 |
+
const [robotToRemove, setRobotToRemove] = useState<RobotConnection | null>(
|
52 |
+
null
|
53 |
+
);
|
54 |
+
|
55 |
+
const handleRemoveClick = (robot: RobotConnection) => {
|
56 |
+
setRobotToRemove(robot);
|
57 |
+
};
|
58 |
+
|
59 |
+
const handleConfirmRemove = () => {
|
60 |
+
if (robotToRemove) {
|
61 |
+
onRemove(
|
62 |
+
robotToRemove.robotId || robotToRemove.serialNumber || "unknown"
|
63 |
+
);
|
64 |
+
setRobotToRemove(null);
|
65 |
+
}
|
66 |
+
};
|
67 |
+
|
68 |
+
const handleCancelRemove = () => {
|
69 |
+
setRobotToRemove(null);
|
70 |
+
};
|
71 |
return (
|
72 |
+
<>
|
73 |
+
<Card className="border-0 rounded-none">
|
74 |
+
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
75 |
+
<div className="flex items-center gap-4">
|
76 |
+
<div className="w-1 h-8 bg-primary"></div>
|
77 |
+
<div>
|
78 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
79 |
+
device registry
|
80 |
+
</h3>
|
81 |
+
<div className="flex items-center gap-2 mt-1">
|
82 |
+
<span className="text-xs text-muted-foreground font-mono">
|
83 |
+
currently supports SO-100{" "}
|
84 |
+
</span>
|
85 |
+
<button
|
86 |
+
onClick={onScrollToHardware}
|
87 |
+
className="text-xs text-primary hover:text-accent transition-colors underline font-mono flex items-center gap-1"
|
88 |
+
>
|
89 |
+
<ExternalLink className="w-3 h-3" />
|
90 |
+
add more devices
|
91 |
+
</button>
|
92 |
+
</div>
|
93 |
</div>
|
94 |
</div>
|
95 |
+
{robots.length > 0 && (
|
96 |
+
<Button
|
97 |
+
onClick={onFindNew}
|
98 |
+
disabled={isConnecting}
|
99 |
+
size="lg"
|
100 |
+
className="font-mono uppercase"
|
101 |
+
>
|
102 |
+
<Plus className="w-4 h-4 mr-2" />
|
103 |
+
add unit
|
104 |
+
</Button>
|
105 |
+
)}
|
106 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
|
108 |
+
<div className="pt-6 p-6">
|
109 |
+
{robots.length === 0 ? (
|
110 |
+
<div className="relative">
|
111 |
+
<HudCorners className="p-16">
|
112 |
+
<div className="text-center font-mono">
|
113 |
+
<div className="mb-6">
|
114 |
+
{isConnecting ? (
|
115 |
+
<>
|
116 |
+
<div className="w-16 h-16 mx-auto mb-4 border-2 border-primary/50 rounded-lg flex items-center justify-center animate-pulse">
|
117 |
+
<Plus className="w-8 h-8 text-primary animate-spin" />
|
118 |
+
</div>
|
119 |
+
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">
|
120 |
+
scanning for units
|
121 |
+
</h4>
|
122 |
+
<p className="text-sm text-muted-foreground mb-8">
|
123 |
+
searching for available devices...
|
124 |
+
</p>
|
125 |
+
</>
|
126 |
+
) : (
|
127 |
+
<>
|
128 |
+
<div className="w-16 h-16 mx-auto mb-4 border-2 border-dashed border-primary/50 rounded-lg flex items-center justify-center">
|
129 |
+
<Plus className="w-8 h-8 text-primary/50" />
|
130 |
+
</div>
|
131 |
+
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">
|
132 |
+
no units detected
|
133 |
+
</h4>
|
134 |
|
135 |
+
<Button
|
136 |
+
onClick={onFindNew}
|
137 |
+
size="lg"
|
138 |
+
className="font-mono uppercase"
|
139 |
+
>
|
140 |
+
<Plus className="w-4 h-4 mr-2" />
|
141 |
+
add unit
|
142 |
+
</Button>
|
143 |
+
</>
|
144 |
+
)}
|
145 |
+
</div>
|
146 |
</div>
|
147 |
+
</HudCorners>
|
148 |
+
</div>
|
149 |
+
) : (
|
150 |
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
151 |
+
{robots.map((robot) => (
|
152 |
+
<HudCorners key={robot.robotId}>
|
153 |
+
<Card className="flex flex-col h-full">
|
154 |
+
<div className="p-4 border-b border-white/10">
|
155 |
+
<div className="flex items-center justify-between">
|
156 |
+
<div className="flex-1">
|
157 |
+
<h4 className="text-2xl font-bold text-primary font-mono tracking-wider">
|
158 |
+
{robot.name}
|
159 |
+
</h4>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
</div>
|
161 |
+
<Badge
|
162 |
+
variant="outline"
|
163 |
+
className={cn(
|
164 |
+
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
165 |
+
robot.isConnected && "animate-pulse-slow"
|
166 |
+
)}
|
167 |
+
>
|
168 |
+
{robot.isConnected ? "ONLINE" : "OFFLINE"}
|
169 |
+
</Badge>
|
170 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
</div>
|
|
|
172 |
|
173 |
+
<div className="flex-grow p-4">
|
174 |
+
<div className="grid grid-cols-2 gap-4 text-xs font-mono">
|
175 |
+
<div>
|
176 |
+
<span className="text-muted-foreground/40 uppercase">
|
177 |
+
serial number
|
178 |
+
</span>
|
179 |
+
<div className="text-muted-foreground uppercase/70">
|
180 |
+
{robot.serialNumber}
|
181 |
+
</div>
|
182 |
</div>
|
183 |
+
<div>
|
184 |
+
<span className="text-muted-foreground/40 uppercase">
|
185 |
+
type
|
186 |
+
</span>
|
187 |
+
<div className="text-muted-foreground uppercase/70">
|
188 |
+
{robot.robotType}
|
189 |
+
</div>
|
190 |
</div>
|
191 |
</div>
|
192 |
</div>
|
|
|
193 |
|
194 |
+
<CardFooter className="p-4 border-t border-white/10 flex flex-wrap gap-2">
|
195 |
+
<Button
|
196 |
+
variant="outline"
|
197 |
+
size="sm"
|
198 |
+
onClick={() => onEdit(robot)}
|
199 |
+
className="font-mono text-xs uppercase px-2"
|
200 |
+
>
|
201 |
+
<Pencil className="w-3 h-3 mr-0.5" /> edit
|
202 |
+
</Button>
|
203 |
+
<Button
|
204 |
+
variant="outline"
|
205 |
+
size="sm"
|
206 |
+
onClick={() => onCalibrate(robot)}
|
207 |
+
className="font-mono text-xs uppercase px-2"
|
208 |
+
>
|
209 |
+
<Settings className="w-3 h-3 mr-0.5" />
|
210 |
+
{robot.serialNumber &&
|
211 |
+
getUnifiedRobotData(robot.serialNumber)?.calibration
|
212 |
+
? "re-calibrate"
|
213 |
+
: "calibrate"}
|
214 |
+
</Button>
|
215 |
+
<Button
|
216 |
+
variant="outline"
|
217 |
+
size="sm"
|
218 |
+
onClick={() => onTeleoperate(robot)}
|
219 |
+
className="font-mono text-xs uppercase px-2"
|
220 |
+
>
|
221 |
+
<Gamepad2 className="w-3 h-3 mr-0.5" /> control
|
222 |
+
</Button>
|
223 |
+
<Button
|
224 |
+
variant="destructive"
|
225 |
+
size="sm"
|
226 |
+
onClick={() => handleRemoveClick(robot)}
|
227 |
+
className="font-mono text-xs uppercase px-2"
|
228 |
+
>
|
229 |
+
<Trash2 className="w-3 h-3 mr-0.5" /> remove
|
230 |
+
</Button>
|
231 |
+
</CardFooter>
|
232 |
+
</Card>
|
233 |
+
</HudCorners>
|
234 |
+
))}
|
235 |
+
</div>
|
236 |
+
)}
|
237 |
+
</div>
|
238 |
+
</Card>
|
239 |
+
|
240 |
+
<AlertDialog
|
241 |
+
open={!!robotToRemove}
|
242 |
+
onOpenChange={() => setRobotToRemove(null)}
|
243 |
+
>
|
244 |
+
<AlertDialogContent>
|
245 |
+
<AlertDialogHeader>
|
246 |
+
<AlertDialogTitle>Remove Robot</AlertDialogTitle>
|
247 |
+
<AlertDialogDescription>
|
248 |
+
Are you sure you want to remove{" "}
|
249 |
+
<strong>{robotToRemove?.name || robotToRemove?.robotId}</strong>{" "}
|
250 |
+
from the device registry?
|
251 |
+
<br />
|
252 |
+
<br />
|
253 |
+
This will permanently delete all stored calibration data and
|
254 |
+
settings for this robot. This action cannot be undone.
|
255 |
+
</AlertDialogDescription>
|
256 |
+
</AlertDialogHeader>
|
257 |
+
<AlertDialogFooter>
|
258 |
+
<AlertDialogCancel onClick={handleCancelRemove}>
|
259 |
+
Cancel
|
260 |
+
</AlertDialogCancel>
|
261 |
+
<AlertDialogAction
|
262 |
+
onClick={handleConfirmRemove}
|
263 |
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
264 |
+
>
|
265 |
+
Remove Robot
|
266 |
+
</AlertDialogAction>
|
267 |
+
</AlertDialogFooter>
|
268 |
+
</AlertDialogContent>
|
269 |
+
</AlertDialog>
|
270 |
+
</>
|
271 |
);
|
272 |
}
|
examples/cyberpunk-standalone/src/components/docs-section.tsx
CHANGED
@@ -30,7 +30,7 @@ const CodeBlock = ({
|
|
30 |
typeof children === "string" ? children : children?.toString() || "";
|
31 |
|
32 |
return (
|
33 |
-
<div className="bg-
|
34 |
<SyntaxHighlighter
|
35 |
language={language}
|
36 |
style={oneDark}
|
@@ -40,6 +40,8 @@ const CodeBlock = ({
|
|
40 |
fontSize: "0.875rem",
|
41 |
background: "transparent",
|
42 |
backgroundColor: "transparent",
|
|
|
|
|
43 |
}}
|
44 |
wrapLines={true}
|
45 |
wrapLongLines={true}
|
@@ -68,16 +70,36 @@ export function DocsSection() {
|
|
68 |
return (
|
69 |
<div className="font-mono">
|
70 |
<div className="mb-6">
|
71 |
-
<h2 className="text-
|
72 |
<Book className="w-6 h-6" />
|
73 |
Docs
|
74 |
</h2>
|
75 |
<p className="text-sm text-muted-foreground">
|
76 |
-
Complete API reference for @lerobot/web
|
77 |
</p>
|
78 |
</div>
|
79 |
|
80 |
-
<div className="bg-muted/40 dark:bg-black/30 border
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
{/* Getting Started */}
|
82 |
<div>
|
83 |
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2">
|
@@ -115,28 +137,11 @@ const teleop = await teleoperate({
|
|
115 |
calibrationData,
|
116 |
teleop: { type: "keyboard" },
|
117 |
});
|
118 |
-
teleop.start()
|
119 |
-
</CodeBlock>
|
120 |
-
</div>
|
121 |
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
Browser Requirements
|
126 |
-
</h3>
|
127 |
-
<div className="mt-4 space-y-2 text-sm">
|
128 |
-
<div>
|
129 |
-
• <strong>Chromium 89+</strong> with WebSerial and WebUSB API
|
130 |
-
support
|
131 |
-
</div>
|
132 |
-
<div>
|
133 |
-
• <strong>HTTPS or localhost</strong>
|
134 |
-
</div>
|
135 |
-
<div>
|
136 |
-
• <strong>User gesture</strong> required for initial port
|
137 |
-
selection
|
138 |
-
</div>
|
139 |
-
</div>
|
140 |
</div>
|
141 |
|
142 |
{/* API Reference */}
|
@@ -145,7 +150,7 @@ teleop.start();`}
|
|
145 |
<Code2 className="w-5 h-5" />
|
146 |
API Reference
|
147 |
</h3>
|
148 |
-
<div className="space-y-
|
149 |
{/* findPort */}
|
150 |
<div>
|
151 |
<h4 className="font-bold text-primary">findPort(config?)</h4>
|
@@ -163,14 +168,40 @@ const findProcess = await findPort({
|
|
163 |
{ robotType: "so100_follower", robotId: "left_arm", serialNumber: "USB123" }
|
164 |
],
|
165 |
onMessage: (msg) => console.log(msg),
|
166 |
-
})
|
167 |
-
|
168 |
-
// Returns: FindPortProcess
|
169 |
-
{
|
170 |
-
result: Promise<RobotConnection[]>, // Array of robot connections
|
171 |
-
stop(): void // Cancel discovery process
|
172 |
-
}`}
|
173 |
</CodeBlock>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
174 |
</div>
|
175 |
|
176 |
{/* calibrate */}
|
@@ -195,14 +226,25 @@ const findProcess = await findPort({
|
|
195 |
|
196 |
// Move robot through full range of motion...
|
197 |
calibrationProcess.stop(); // Stop range recording
|
198 |
-
const calibrationData = await calibrationProcess.result
|
199 |
-
|
200 |
-
// Returns: CalibrationProcess
|
201 |
-
{
|
202 |
-
result: Promise<WebCalibrationResults>, // Python-compatible format
|
203 |
-
stop(): void // Stop calibration process
|
204 |
-
}`}
|
205 |
</CodeBlock>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
</div>
|
207 |
|
208 |
{/* teleoperate */}
|
@@ -228,17 +270,37 @@ const directTeleop = await teleoperate({
|
|
228 |
robot,
|
229 |
calibrationData: savedCalibrationData,
|
230 |
teleop: { type: "direct" },
|
231 |
-
})
|
232 |
-
|
233 |
-
// Returns: TeleoperationProcess
|
234 |
-
{
|
235 |
-
start(): void, // Begin teleoperation
|
236 |
-
stop(): void, // Stop teleoperation and clear states
|
237 |
-
getState(): TeleoperationState, // Current state and motor positions
|
238 |
-
teleoperator: BaseWebTeleoperator, // Access teleoperator-specific methods
|
239 |
-
disconnect(): Promise<void> // Stop and disconnect
|
240 |
-
}`}
|
241 |
</CodeBlock>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
</div>
|
243 |
|
244 |
{/* releaseMotors */}
|
@@ -254,12 +316,22 @@ const directTeleop = await teleoperate({
|
|
254 |
await releaseMotors(robot);
|
255 |
|
256 |
// Release specific motors only
|
257 |
-
await releaseMotors(robot, [1, 2, 3])
|
258 |
-
|
259 |
-
// Parameters
|
260 |
-
robot: RobotConnection // Connected robot
|
261 |
-
motorIds?: number[] // Specific motor IDs (default: all motors for robot type)`}
|
262 |
</CodeBlock>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
263 |
</div>
|
264 |
</div>
|
265 |
</div>
|
|
|
30 |
typeof children === "string" ? children : children?.toString() || "";
|
31 |
|
32 |
return (
|
33 |
+
<div className="bg-slate-800 dark:bg-black/40 border border-border dark:border-white/10 rounded-md overflow-hidden my-4 relative">
|
34 |
<SyntaxHighlighter
|
35 |
language={language}
|
36 |
style={oneDark}
|
|
|
40 |
fontSize: "0.875rem",
|
41 |
background: "transparent",
|
42 |
backgroundColor: "transparent",
|
43 |
+
fontFamily:
|
44 |
+
"Geist Mono, ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
|
45 |
}}
|
46 |
wrapLines={true}
|
47 |
wrapLongLines={true}
|
|
|
70 |
return (
|
71 |
<div className="font-mono">
|
72 |
<div className="mb-6">
|
73 |
+
<h2 className="text-3xl font-bold tracking-wider mb-2 uppercase flex items-center gap-3">
|
74 |
<Book className="w-6 h-6" />
|
75 |
Docs
|
76 |
</h2>
|
77 |
<p className="text-sm text-muted-foreground">
|
78 |
+
Complete API reference for @lerobot/web
|
79 |
</p>
|
80 |
</div>
|
81 |
|
82 |
+
<div className="bg-muted/40 dark:bg-black/30 border border-border dark:border-white/10 p-6 md:p-8 rounded-lg space-y-14">
|
83 |
+
{/* Browser Requirements */}
|
84 |
+
<div>
|
85 |
+
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase">
|
86 |
+
Browser Requirements
|
87 |
+
</h3>
|
88 |
+
<div className="mt-4 space-y-2 text-sm">
|
89 |
+
<div>
|
90 |
+
• <strong>Chromium 89+</strong> with WebSerial and WebUSB API
|
91 |
+
support
|
92 |
+
</div>
|
93 |
+
<div>
|
94 |
+
• <strong>HTTPS or localhost</strong>
|
95 |
+
</div>
|
96 |
+
<div>
|
97 |
+
• <strong>User gesture</strong> required for initial port
|
98 |
+
selection
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
|
103 |
{/* Getting Started */}
|
104 |
<div>
|
105 |
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2">
|
|
|
137 |
calibrationData,
|
138 |
teleop: { type: "keyboard" },
|
139 |
});
|
140 |
+
teleop.start();
|
|
|
|
|
141 |
|
142 |
+
// 5. stop control (run this when you're done)
|
143 |
+
teleop.stop();`}
|
144 |
+
</CodeBlock>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
</div>
|
146 |
|
147 |
{/* API Reference */}
|
|
|
150 |
<Code2 className="w-5 h-5" />
|
151 |
API Reference
|
152 |
</h3>
|
153 |
+
<div className="space-y-12 mt-4">
|
154 |
{/* findPort */}
|
155 |
<div>
|
156 |
<h4 className="font-bold text-primary">findPort(config?)</h4>
|
|
|
168 |
{ robotType: "so100_follower", robotId: "left_arm", serialNumber: "USB123" }
|
169 |
],
|
170 |
onMessage: (msg) => console.log(msg),
|
171 |
+
});`}
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
</CodeBlock>
|
173 |
+
<div className="mt-3">
|
174 |
+
<h5 className="font-bold text-sm text-muted-foreground tracking-wider">
|
175 |
+
Options
|
176 |
+
</h5>
|
177 |
+
<ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
|
178 |
+
<li>
|
179 |
+
• <code>robotConfigs?: RobotConfig[]</code> - Auto-connect
|
180 |
+
to these known robots
|
181 |
+
</li>
|
182 |
+
<li>
|
183 |
+
• <code>onMessage?: (message: string) => void</code> -
|
184 |
+
Progress messages callback
|
185 |
+
</li>
|
186 |
+
</ul>
|
187 |
+
</div>
|
188 |
+
<div className="mt-3">
|
189 |
+
<h5 className="font-bold text-sm text-muted-foreground tracking-wider">
|
190 |
+
Returns:{" "}
|
191 |
+
<code className="bg-muted/50 px-1 rounded">
|
192 |
+
FindPortProcess
|
193 |
+
</code>
|
194 |
+
</h5>
|
195 |
+
<ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
|
196 |
+
<li>
|
197 |
+
• <code>result: Promise<RobotConnection[]></code> -
|
198 |
+
Array of robot connections
|
199 |
+
</li>
|
200 |
+
<li>
|
201 |
+
• <code>stop(): void</code> - Cancel discovery process
|
202 |
+
</li>
|
203 |
+
</ul>
|
204 |
+
</div>
|
205 |
</div>
|
206 |
|
207 |
{/* calibrate */}
|
|
|
226 |
|
227 |
// Move robot through full range of motion...
|
228 |
calibrationProcess.stop(); // Stop range recording
|
229 |
+
const calibrationData = await calibrationProcess.result;`}
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
</CodeBlock>
|
231 |
+
<div className="mt-3">
|
232 |
+
<h5 className="font-bold text-sm text-muted-foreground tracking-wider">
|
233 |
+
Returns:{" "}
|
234 |
+
<code className="bg-muted/50 px-1 rounded">
|
235 |
+
CalibrationProcess
|
236 |
+
</code>
|
237 |
+
</h5>
|
238 |
+
<ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
|
239 |
+
<li>
|
240 |
+
• <code>result: Promise<WebCalibrationResults></code>{" "}
|
241 |
+
- Python-compatible format
|
242 |
+
</li>
|
243 |
+
<li>
|
244 |
+
• <code>stop(): void</code> - Stop calibration process
|
245 |
+
</li>
|
246 |
+
</ul>
|
247 |
+
</div>
|
248 |
</div>
|
249 |
|
250 |
{/* teleoperate */}
|
|
|
270 |
robot,
|
271 |
calibrationData: savedCalibrationData,
|
272 |
teleop: { type: "direct" },
|
273 |
+
});`}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
274 |
</CodeBlock>
|
275 |
+
<div className="mt-3">
|
276 |
+
<h5 className="font-bold text-sm text-muted-foreground tracking-wider">
|
277 |
+
Returns:{" "}
|
278 |
+
<code className="bg-muted/50 px-1 rounded">
|
279 |
+
TeleoperationProcess
|
280 |
+
</code>
|
281 |
+
</h5>
|
282 |
+
<ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
|
283 |
+
<li>
|
284 |
+
• <code>start(): void</code> - Begin teleoperation
|
285 |
+
</li>
|
286 |
+
<li>
|
287 |
+
• <code>stop(): void</code> - Stop teleoperation and clear
|
288 |
+
states
|
289 |
+
</li>
|
290 |
+
<li>
|
291 |
+
• <code>getState(): TeleoperationState</code> - Current
|
292 |
+
state and motor positions
|
293 |
+
</li>
|
294 |
+
<li>
|
295 |
+
• <code>teleoperator: BaseWebTeleoperator</code> - Access
|
296 |
+
teleoperator-specific methods
|
297 |
+
</li>
|
298 |
+
<li>
|
299 |
+
• <code>disconnect(): Promise<void></code> - Stop and
|
300 |
+
disconnect
|
301 |
+
</li>
|
302 |
+
</ul>
|
303 |
+
</div>
|
304 |
</div>
|
305 |
|
306 |
{/* releaseMotors */}
|
|
|
316 |
await releaseMotors(robot);
|
317 |
|
318 |
// Release specific motors only
|
319 |
+
await releaseMotors(robot, [1, 2, 3]);`}
|
|
|
|
|
|
|
|
|
320 |
</CodeBlock>
|
321 |
+
<div className="mt-3">
|
322 |
+
<h5 className="font-bold text-sm text-muted-foreground tracking-wider">
|
323 |
+
Parameters
|
324 |
+
</h5>
|
325 |
+
<ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
|
326 |
+
<li>
|
327 |
+
• <code>robot: RobotConnection</code> - Connected robot
|
328 |
+
</li>
|
329 |
+
<li>
|
330 |
+
• <code>motorIds?: number[]</code> - Specific motor IDs
|
331 |
+
(default: all motors for robot type)
|
332 |
+
</li>
|
333 |
+
</ul>
|
334 |
+
</div>
|
335 |
</div>
|
336 |
</div>
|
337 |
</div>
|
examples/cyberpunk-standalone/src/components/footer.tsx
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
-
import { Github } from "lucide-react"
|
2 |
|
3 |
export function Footer() {
|
4 |
return (
|
5 |
-
<footer className="w-full border-t">
|
6 |
<div className="container mx-auto flex h-16 items-center justify-center px-4 md:px-6">
|
7 |
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
8 |
<span>
|
@@ -37,5 +37,5 @@ export function Footer() {
|
|
37 |
</div>
|
38 |
</div>
|
39 |
</footer>
|
40 |
-
)
|
41 |
}
|
|
|
1 |
+
import { Github } from "lucide-react";
|
2 |
|
3 |
export function Footer() {
|
4 |
return (
|
5 |
+
<footer className="w-full border-t border-gray-300 dark:border-border">
|
6 |
<div className="container mx-auto flex h-16 items-center justify-center px-4 md:px-6">
|
7 |
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
8 |
<span>
|
|
|
37 |
</div>
|
38 |
</div>
|
39 |
</footer>
|
40 |
+
);
|
41 |
}
|
examples/cyberpunk-standalone/src/components/hardware-support-section.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
"use client";
|
2 |
|
3 |
-
import { Cpu,
|
4 |
import { Button } from "@/components/ui/button";
|
5 |
import HudCorners from "@/components/hud-corners";
|
6 |
|
@@ -17,7 +17,7 @@ export function HardwareSupportSection() {
|
|
17 |
return (
|
18 |
<div className="font-mono">
|
19 |
<div className="mb-6">
|
20 |
-
<h2 className="text-
|
21 |
<Cpu className="w-6 h-6" />
|
22 |
Hardware Support
|
23 |
</h2>
|
@@ -88,7 +88,8 @@ export function HardwareSupportSection() {
|
|
88 |
<div>
|
89 |
<p className="text-muted-foreground mb-4">
|
90 |
please provide us with access to different robot hardware, so
|
91 |
-
we can add them to lerobot.js
|
|
|
92 |
</p>
|
93 |
|
94 |
<div className="bg-muted/60 dark:bg-black/40 border border-border dark:border-white/10 rounded-lg p-4 mb-4">
|
@@ -104,30 +105,16 @@ export function HardwareSupportSection() {
|
|
104 |
</div>
|
105 |
|
106 |
<div className="flex flex-col sm:flex-row gap-4">
|
107 |
-
<Button
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
Contact Tim
|
118 |
-
</Button>
|
119 |
-
<Button
|
120 |
-
variant="outline"
|
121 |
-
className="font-mono uppercase flex items-center gap-2 bg-transparent"
|
122 |
-
onClick={() =>
|
123 |
-
window.open(
|
124 |
-
"https://github.com/timpietrusky/lerobot.js",
|
125 |
-
"_blank"
|
126 |
-
)
|
127 |
-
}
|
128 |
-
>
|
129 |
-
<ExternalLink className="w-4 h-4" />
|
130 |
-
GitHub
|
131 |
</Button>
|
132 |
</div>
|
133 |
</div>
|
|
|
1 |
"use client";
|
2 |
|
3 |
+
import { Cpu, MessageCircle, Plus } from "lucide-react";
|
4 |
import { Button } from "@/components/ui/button";
|
5 |
import HudCorners from "@/components/hud-corners";
|
6 |
|
|
|
17 |
return (
|
18 |
<div className="font-mono">
|
19 |
<div className="mb-6">
|
20 |
+
<h2 className="text-3xl font-bold tracking-wider mb-2 uppercase flex items-center gap-3">
|
21 |
<Cpu className="w-6 h-6" />
|
22 |
Hardware Support
|
23 |
</h2>
|
|
|
88 |
<div>
|
89 |
<p className="text-muted-foreground mb-4">
|
90 |
please provide us with access to different robot hardware, so
|
91 |
+
we can add them to lerobot.js. join the LeRobot Discord and DM
|
92 |
+
me!
|
93 |
</p>
|
94 |
|
95 |
<div className="bg-muted/60 dark:bg-black/40 border border-border dark:border-white/10 rounded-lg p-4 mb-4">
|
|
|
105 |
</div>
|
106 |
|
107 |
<div className="flex flex-col sm:flex-row gap-4">
|
108 |
+
<Button asChild className="font-mono">
|
109 |
+
<a
|
110 |
+
href="https://discord.gg/s3KuuzsPFb"
|
111 |
+
target="_blank"
|
112 |
+
rel="noopener noreferrer"
|
113 |
+
className="flex items-center gap-2"
|
114 |
+
>
|
115 |
+
<MessageCircle className="w-4 h-4" />
|
116 |
+
LeRobot Discord → dm @NERDDISCO
|
117 |
+
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
</Button>
|
119 |
</div>
|
120 |
</div>
|
examples/cyberpunk-standalone/src/components/header.tsx
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
-
import { ThemeToggle } from "./theme-toggle"
|
2 |
|
3 |
export function Header() {
|
4 |
return (
|
5 |
-
<header className="w-full border-b">
|
6 |
<div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6">
|
7 |
<div className="flex items-center gap-6">
|
8 |
<h1 className="text-2xl font-bold">LeRobot.js</h1>
|
@@ -23,5 +23,5 @@ export function Header() {
|
|
23 |
<ThemeToggle />
|
24 |
</div>
|
25 |
</header>
|
26 |
-
)
|
27 |
}
|
|
|
1 |
+
import { ThemeToggle } from "./theme-toggle";
|
2 |
|
3 |
export function Header() {
|
4 |
return (
|
5 |
+
<header className="w-full border-b border-gray-300 dark:border-border">
|
6 |
<div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6">
|
7 |
<div className="flex items-center gap-6">
|
8 |
<h1 className="text-2xl font-bold">LeRobot.js</h1>
|
|
|
23 |
<ThemeToggle />
|
24 |
</div>
|
25 |
</header>
|
26 |
+
);
|
27 |
}
|
examples/cyberpunk-standalone/src/components/roadmap-section.tsx
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
-
"use client"
|
2 |
-
import { CheckCircle, Clock, Target } from "lucide-react"
|
3 |
-
import { Badge } from "@/components/ui/badge"
|
4 |
-
import { cn } from "@/lib/utils"
|
5 |
|
6 |
interface RoadmapItem {
|
7 |
-
title: string
|
8 |
-
description: string
|
9 |
-
status: "completed" | "planned"
|
10 |
}
|
11 |
|
12 |
const roadmapItems: RoadmapItem[] = [
|
@@ -17,7 +17,8 @@ const roadmapItems: RoadmapItem[] = [
|
|
17 |
},
|
18 |
{
|
19 |
title: "calibrate",
|
20 |
-
description:
|
|
|
21 |
status: "completed",
|
22 |
},
|
23 |
{
|
@@ -32,7 +33,8 @@ const roadmapItems: RoadmapItem[] = [
|
|
32 |
},
|
33 |
{
|
34 |
title: "replay",
|
35 |
-
description:
|
|
|
36 |
status: "planned",
|
37 |
},
|
38 |
{
|
@@ -42,10 +44,11 @@ const roadmapItems: RoadmapItem[] = [
|
|
42 |
},
|
43 |
{
|
44 |
title: "eval",
|
45 |
-
description:
|
|
|
46 |
status: "planned",
|
47 |
},
|
48 |
-
]
|
49 |
|
50 |
const statusConfig = {
|
51 |
completed: {
|
@@ -64,20 +67,33 @@ const statusConfig = {
|
|
64 |
bgColor: "bg-slate-500/10 dark:bg-muted-foreground/5",
|
65 |
borderColor: "border-slate-500/30 dark:border-muted-foreground/20",
|
66 |
},
|
67 |
-
}
|
68 |
|
69 |
export function RoadmapSection() {
|
70 |
-
const completedCount = roadmapItems.filter(
|
71 |
-
|
|
|
|
|
72 |
|
73 |
return (
|
74 |
<div className="font-mono">
|
75 |
<div className="mb-6">
|
76 |
-
<h2 className="text-
|
77 |
<Target className="w-6 h-6" />
|
78 |
Roadmap
|
79 |
</h2>
|
80 |
-
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
</div>
|
82 |
|
83 |
<div className="bg-gradient-to-br from-muted/60 to-muted/40 dark:from-black/40 dark:to-black/20 border border-primary/20 rounded-lg overflow-hidden">
|
@@ -85,13 +101,14 @@ export function RoadmapSection() {
|
|
85 |
<div className="bg-primary/30 dark:bg-primary/10 border-b border-primary/20 p-4">
|
86 |
<div className="flex items-center justify-between">
|
87 |
<div className="flex items-center gap-4">
|
88 |
-
<div className="text-primary font-bold text-lg tracking-wider">SYSTEM OBJECTIVES</div>
|
89 |
<div className="flex items-center gap-2">
|
90 |
<div className="w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full animate-pulse"></div>
|
91 |
-
<span className="text-green-600 dark:text-green-400 text-
|
|
|
|
|
92 |
<div className="w-2 h-2 bg-slate-500 dark:bg-muted-foreground rounded-full"></div>
|
93 |
-
<span className="text-slate-600 dark:text-muted-foreground text-
|
94 |
-
{totalCount - completedCount}
|
95 |
</span>
|
96 |
</div>
|
97 |
</div>
|
@@ -115,8 +132,8 @@ export function RoadmapSection() {
|
|
115 |
<div className="p-6">
|
116 |
<div className="space-y-3">
|
117 |
{roadmapItems.map((item, index) => {
|
118 |
-
const config = statusConfig[item.status]
|
119 |
-
const StatusIcon = config.icon
|
120 |
|
121 |
return (
|
122 |
<div
|
@@ -124,7 +141,7 @@ export function RoadmapSection() {
|
|
124 |
className={cn(
|
125 |
"flex items-center gap-4 p-4 rounded border transition-all hover:bg-muted/30 dark:hover:bg-white/5",
|
126 |
config.bgColor,
|
127 |
-
config.borderColor
|
128 |
)}
|
129 |
>
|
130 |
{/* Number */}
|
@@ -134,8 +151,12 @@ export function RoadmapSection() {
|
|
134 |
|
135 |
{/* Content */}
|
136 |
<div className="flex-1 min-w-0">
|
137 |
-
<h4 className={cn("font-bold text-lg", config.textColor)}>
|
138 |
-
|
|
|
|
|
|
|
|
|
139 |
</div>
|
140 |
|
141 |
{/* Status Badge */}
|
@@ -144,36 +165,41 @@ export function RoadmapSection() {
|
|
144 |
className={cn(
|
145 |
"text-xs font-bold tracking-wider border-0 px-3 py-1 flex-shrink-0",
|
146 |
config.textColor,
|
147 |
-
config.bgColor
|
148 |
)}
|
149 |
>
|
150 |
{config.label}
|
151 |
</Badge>
|
152 |
|
153 |
{/* Status Icon */}
|
154 |
-
<StatusIcon
|
|
|
|
|
|
|
|
|
|
|
155 |
</div>
|
156 |
-
)
|
157 |
})}
|
158 |
</div>
|
159 |
</div>
|
160 |
|
161 |
{/* Footer */}
|
162 |
-
<div className="bg-muted/50 dark:bg-black/30 border-t border-
|
163 |
<p className="text-muted-foreground text-sm">
|
164 |
-
|
165 |
<a
|
166 |
-
href="https://
|
167 |
target="_blank"
|
168 |
rel="noopener noreferrer"
|
169 |
className="text-primary hover:text-accent transition-colors underline"
|
170 |
>
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
</p>
|
175 |
</div>
|
176 |
</div>
|
177 |
</div>
|
178 |
-
)
|
179 |
}
|
|
|
1 |
+
"use client";
|
2 |
+
import { CheckCircle, Clock, Target, Github } from "lucide-react";
|
3 |
+
import { Badge } from "@/components/ui/badge";
|
4 |
+
import { cn } from "@/lib/utils";
|
5 |
|
6 |
interface RoadmapItem {
|
7 |
+
title: string;
|
8 |
+
description: string;
|
9 |
+
status: "completed" | "planned";
|
10 |
}
|
11 |
|
12 |
const roadmapItems: RoadmapItem[] = [
|
|
|
17 |
},
|
18 |
{
|
19 |
title: "calibrate",
|
20 |
+
description:
|
21 |
+
"Real-time joint calibration with visual feedback and data export",
|
22 |
status: "completed",
|
23 |
},
|
24 |
{
|
|
|
33 |
},
|
34 |
{
|
35 |
title: "replay",
|
36 |
+
description:
|
37 |
+
"Replay any recorded episode or episodes from existing datasets",
|
38 |
status: "planned",
|
39 |
},
|
40 |
{
|
|
|
44 |
},
|
45 |
{
|
46 |
title: "eval",
|
47 |
+
description:
|
48 |
+
"Run inference using trained policies for autonomous operation",
|
49 |
status: "planned",
|
50 |
},
|
51 |
+
];
|
52 |
|
53 |
const statusConfig = {
|
54 |
completed: {
|
|
|
67 |
bgColor: "bg-slate-500/10 dark:bg-muted-foreground/5",
|
68 |
borderColor: "border-slate-500/30 dark:border-muted-foreground/20",
|
69 |
},
|
70 |
+
};
|
71 |
|
72 |
export function RoadmapSection() {
|
73 |
+
const completedCount = roadmapItems.filter(
|
74 |
+
(item) => item.status === "completed"
|
75 |
+
).length;
|
76 |
+
const totalCount = roadmapItems.length;
|
77 |
|
78 |
return (
|
79 |
<div className="font-mono">
|
80 |
<div className="mb-6">
|
81 |
+
<h2 className="text-3xl font-bold tracking-wider mb-2 uppercase flex items-center gap-3">
|
82 |
<Target className="w-6 h-6" />
|
83 |
Roadmap
|
84 |
</h2>
|
85 |
+
<p className="text-sm text-muted-foreground">
|
86 |
+
our goal is to provide{" "}
|
87 |
+
<a
|
88 |
+
href="https://huggingface.co/docs/lerobot"
|
89 |
+
target="_blank"
|
90 |
+
rel="noopener noreferrer"
|
91 |
+
className="text-primary hover:text-accent transition-colors underline"
|
92 |
+
>
|
93 |
+
LeRobot
|
94 |
+
</a>
|
95 |
+
's simple, easy-to-use Python functions for the JavaScript community
|
96 |
+
</p>
|
97 |
</div>
|
98 |
|
99 |
<div className="bg-gradient-to-br from-muted/60 to-muted/40 dark:from-black/40 dark:to-black/20 border border-primary/20 rounded-lg overflow-hidden">
|
|
|
101 |
<div className="bg-primary/30 dark:bg-primary/10 border-b border-primary/20 p-4">
|
102 |
<div className="flex items-center justify-between">
|
103 |
<div className="flex items-center gap-4">
|
|
|
104 |
<div className="flex items-center gap-2">
|
105 |
<div className="w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full animate-pulse"></div>
|
106 |
+
<span className="text-green-600 dark:text-green-400 text-xs">
|
107 |
+
{completedCount} COMPLETED
|
108 |
+
</span>
|
109 |
<div className="w-2 h-2 bg-slate-500 dark:bg-muted-foreground rounded-full"></div>
|
110 |
+
<span className="text-slate-600 dark:text-muted-foreground text-xs">
|
111 |
+
{totalCount - completedCount} PLANNED
|
112 |
</span>
|
113 |
</div>
|
114 |
</div>
|
|
|
132 |
<div className="p-6">
|
133 |
<div className="space-y-3">
|
134 |
{roadmapItems.map((item, index) => {
|
135 |
+
const config = statusConfig[item.status];
|
136 |
+
const StatusIcon = config.icon;
|
137 |
|
138 |
return (
|
139 |
<div
|
|
|
141 |
className={cn(
|
142 |
"flex items-center gap-4 p-4 rounded border transition-all hover:bg-muted/30 dark:hover:bg-white/5",
|
143 |
config.bgColor,
|
144 |
+
config.borderColor
|
145 |
)}
|
146 |
>
|
147 |
{/* Number */}
|
|
|
151 |
|
152 |
{/* Content */}
|
153 |
<div className="flex-1 min-w-0">
|
154 |
+
<h4 className={cn("font-bold text-lg", config.textColor)}>
|
155 |
+
{item.title}()
|
156 |
+
</h4>
|
157 |
+
<p className="text-muted-foreground text-sm mt-1">
|
158 |
+
{item.description}
|
159 |
+
</p>
|
160 |
</div>
|
161 |
|
162 |
{/* Status Badge */}
|
|
|
165 |
className={cn(
|
166 |
"text-xs font-bold tracking-wider border-0 px-3 py-1 flex-shrink-0",
|
167 |
config.textColor,
|
168 |
+
config.bgColor
|
169 |
)}
|
170 |
>
|
171 |
{config.label}
|
172 |
</Badge>
|
173 |
|
174 |
{/* Status Icon */}
|
175 |
+
<StatusIcon
|
176 |
+
className={cn(
|
177 |
+
"w-4 h-4 flex-shrink-0 ml-3",
|
178 |
+
config.textColor
|
179 |
+
)}
|
180 |
+
/>
|
181 |
</div>
|
182 |
+
);
|
183 |
})}
|
184 |
</div>
|
185 |
</div>
|
186 |
|
187 |
{/* Footer */}
|
188 |
+
<div className="bg-muted/50 dark:bg-black/30 border-t border-gray-300 dark:border-white/10 p-4">
|
189 |
<p className="text-muted-foreground text-sm">
|
190 |
+
want to help? LeRobot.js is open source on{" "}
|
191 |
<a
|
192 |
+
href="https://github.com/lerobot/lerobot.js"
|
193 |
target="_blank"
|
194 |
rel="noopener noreferrer"
|
195 |
className="text-primary hover:text-accent transition-colors underline"
|
196 |
>
|
197 |
+
<Github className="h-4 w-4 inline align-text-bottom mr-1" />
|
198 |
+
GitHub
|
199 |
+
</a>
|
200 |
</p>
|
201 |
</div>
|
202 |
</div>
|
203 |
</div>
|
204 |
+
);
|
205 |
}
|
examples/cyberpunk-standalone/src/components/setup-cards.tsx
CHANGED
@@ -1,40 +1,48 @@
|
|
1 |
-
"use client"
|
2 |
-
import { useState } from "react"
|
3 |
-
import { Copy, Package, Clock, Check, Terminal } from "lucide-react"
|
4 |
-
import { Button } from "@/components/ui/button"
|
5 |
-
import { Card } from "@/components/ui/card"
|
6 |
-
import { Badge } from "@/components/ui/badge"
|
7 |
-
import HudCorners from "@/components/hud-corners"
|
8 |
-
import { cn } from "@/lib/utils"
|
9 |
|
10 |
-
type PackageManager = "npm" | "yarn" | "pnpm" | "bun"
|
11 |
|
12 |
interface PackageInstallerProps {
|
13 |
-
packageName: string
|
14 |
-
disabled?: boolean
|
15 |
}
|
16 |
|
17 |
-
function PackageInstaller({
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
20 |
|
21 |
-
const packageManagers: {
|
|
|
|
|
|
|
|
|
22 |
{ value: "pnpm", label: "pnpm", command: `pnpm add ${packageName}` },
|
23 |
{ value: "npm", label: "npm", command: `npm i ${packageName}` },
|
24 |
{ value: "yarn", label: "yarn", command: `yarn add ${packageName}` },
|
25 |
-
]
|
26 |
|
27 |
const copyToClipboard = async (text: string) => {
|
28 |
try {
|
29 |
-
await navigator.clipboard.writeText(text)
|
30 |
-
setCopied(true)
|
31 |
-
setTimeout(() => setCopied(false), 2000)
|
32 |
} catch (err) {
|
33 |
-
console.error("Failed to copy:", err)
|
34 |
}
|
35 |
-
}
|
36 |
|
37 |
-
const currentCommand =
|
|
|
38 |
|
39 |
return (
|
40 |
<div className="max-w-md">
|
@@ -51,7 +59,7 @@ function PackageInstaller({ packageName, disabled = false }: PackageInstallerPro
|
|
51 |
"font-mono text-xs px-3 min-w-[60px] h-8 border transition-colors",
|
52 |
selectedPM === pm.value
|
53 |
? "bg-primary text-primary-foreground border-primary"
|
54 |
-
: "bg-transparent border-input hover:bg-accent hover:text-accent-foreground"
|
55 |
)}
|
56 |
>
|
57 |
{pm.label}
|
@@ -65,7 +73,7 @@ function PackageInstaller({ packageName, disabled = false }: PackageInstallerPro
|
|
65 |
"border rounded-md p-3 font-mono text-sm transition-colors",
|
66 |
disabled
|
67 |
? "bg-muted/60 dark:bg-black/20 border-dashed border-muted/40 dark:border-muted/20 text-muted-foreground/70 dark:text-muted-foreground/50"
|
68 |
-
: "bg-muted/60 dark:bg-black/40 border-border dark:border-white/10 text-foreground dark:text-primary"
|
69 |
)}
|
70 |
>
|
71 |
{currentCommand}
|
@@ -88,7 +96,7 @@ function PackageInstaller({ packageName, disabled = false }: PackageInstallerPro
|
|
88 |
</div>
|
89 |
</div>
|
90 |
</div>
|
91 |
-
)
|
92 |
}
|
93 |
|
94 |
export function SetupCards() {
|
@@ -105,8 +113,12 @@ export function SetupCards() {
|
|
105 |
<Package className="w-6 h-6 text-primary" />
|
106 |
</div>
|
107 |
<div>
|
108 |
-
<h3 className="text-xl font-bold text-primary font-mono tracking-wider uppercase">
|
109 |
-
|
|
|
|
|
|
|
|
|
110 |
</div>
|
111 |
</div>
|
112 |
|
@@ -128,8 +140,12 @@ export function SetupCards() {
|
|
128 |
<Terminal className="w-6 h-6 text-muted-foreground" />
|
129 |
</div>
|
130 |
<div>
|
131 |
-
<h3 className="text-xl font-bold text-muted-foreground font-mono tracking-wider uppercase">
|
132 |
-
|
|
|
|
|
|
|
|
|
133 |
</div>
|
134 |
</div>
|
135 |
<Badge
|
@@ -156,5 +172,5 @@ export function SetupCards() {
|
|
156 |
</Card>
|
157 |
</HudCorners>
|
158 |
</div>
|
159 |
-
)
|
160 |
}
|
|
|
1 |
+
"use client";
|
2 |
+
import { useState } from "react";
|
3 |
+
import { Copy, Package, Clock, Check, Terminal } from "lucide-react";
|
4 |
+
import { Button } from "@/components/ui/button";
|
5 |
+
import { Card } from "@/components/ui/card";
|
6 |
+
import { Badge } from "@/components/ui/badge";
|
7 |
+
import HudCorners from "@/components/hud-corners";
|
8 |
+
import { cn } from "@/lib/utils";
|
9 |
|
10 |
+
type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
|
11 |
|
12 |
interface PackageInstallerProps {
|
13 |
+
packageName: string;
|
14 |
+
disabled?: boolean;
|
15 |
}
|
16 |
|
17 |
+
function PackageInstaller({
|
18 |
+
packageName,
|
19 |
+
disabled = false,
|
20 |
+
}: PackageInstallerProps) {
|
21 |
+
const [selectedPM, setSelectedPM] = useState<PackageManager>("pnpm");
|
22 |
+
const [copied, setCopied] = useState(false);
|
23 |
|
24 |
+
const packageManagers: {
|
25 |
+
value: PackageManager;
|
26 |
+
label: string;
|
27 |
+
command: string;
|
28 |
+
}[] = [
|
29 |
{ value: "pnpm", label: "pnpm", command: `pnpm add ${packageName}` },
|
30 |
{ value: "npm", label: "npm", command: `npm i ${packageName}` },
|
31 |
{ value: "yarn", label: "yarn", command: `yarn add ${packageName}` },
|
32 |
+
];
|
33 |
|
34 |
const copyToClipboard = async (text: string) => {
|
35 |
try {
|
36 |
+
await navigator.clipboard.writeText(text);
|
37 |
+
setCopied(true);
|
38 |
+
setTimeout(() => setCopied(false), 2000);
|
39 |
} catch (err) {
|
40 |
+
console.error("Failed to copy:", err);
|
41 |
}
|
42 |
+
};
|
43 |
|
44 |
+
const currentCommand =
|
45 |
+
packageManagers.find((pm) => pm.value === selectedPM)?.command || "";
|
46 |
|
47 |
return (
|
48 |
<div className="max-w-md">
|
|
|
59 |
"font-mono text-xs px-3 min-w-[60px] h-8 border transition-colors",
|
60 |
selectedPM === pm.value
|
61 |
? "bg-primary text-primary-foreground border-primary"
|
62 |
+
: "bg-transparent border-input hover:bg-accent hover:text-accent-foreground"
|
63 |
)}
|
64 |
>
|
65 |
{pm.label}
|
|
|
73 |
"border rounded-md p-3 font-mono text-sm transition-colors",
|
74 |
disabled
|
75 |
? "bg-muted/60 dark:bg-black/20 border-dashed border-muted/40 dark:border-muted/20 text-muted-foreground/70 dark:text-muted-foreground/50"
|
76 |
+
: "bg-muted/60 dark:bg-black/40 border-border dark:border-white/10 text-foreground dark:text-primary"
|
77 |
)}
|
78 |
>
|
79 |
{currentCommand}
|
|
|
96 |
</div>
|
97 |
</div>
|
98 |
</div>
|
99 |
+
);
|
100 |
}
|
101 |
|
102 |
export function SetupCards() {
|
|
|
113 |
<Package className="w-6 h-6 text-primary" />
|
114 |
</div>
|
115 |
<div>
|
116 |
+
<h3 className="text-xl font-bold text-primary font-mono tracking-wider uppercase">
|
117 |
+
web
|
118 |
+
</h3>
|
119 |
+
<p className="text-sm text-muted-foreground font-mono">
|
120 |
+
run LeRobot.js in the browser
|
121 |
+
</p>
|
122 |
</div>
|
123 |
</div>
|
124 |
|
|
|
140 |
<Terminal className="w-6 h-6 text-muted-foreground" />
|
141 |
</div>
|
142 |
<div>
|
143 |
+
<h3 className="text-xl font-bold text-muted-foreground font-mono tracking-wider uppercase">
|
144 |
+
node
|
145 |
+
</h3>
|
146 |
+
<p className="text-sm text-muted-foreground/70 font-mono">
|
147 |
+
run LeRobot.js on the server
|
148 |
+
</p>
|
149 |
</div>
|
150 |
</div>
|
151 |
<Badge
|
|
|
172 |
</Card>
|
173 |
</HudCorners>
|
174 |
</div>
|
175 |
+
);
|
176 |
}
|