From c34935090de7e538fe8436dbd05e992f261b6e5c Mon Sep 17 00:00:00 2001 From: Michel Aractingi Date: Thu, 27 Nov 2025 13:36:51 +0100 Subject: [PATCH] integrate delete button openarm UI (#2535) * add visualize_dataset call from `lerobot_dataset_viz` in web record server * add delete button * fixes * remove viz * unused import --- examples/openarms_web_interface/App.css | 41 ++++++++++++ examples/openarms_web_interface/App.jsx | 63 ++++++++++++++++++- .../web_record_server.py | 56 ++++++++++++++++- 3 files changed, 156 insertions(+), 4 deletions(-) diff --git a/examples/openarms_web_interface/App.css b/examples/openarms_web_interface/App.css index 8a6a42e43..49271ef82 100644 --- a/examples/openarms_web_interface/App.css +++ b/examples/openarms_web_interface/App.css @@ -205,6 +205,47 @@ h3 { transform: none; } +.delete-episode-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #e5e7eb; +} + +.btn-delete { + width: 100%; + background: #ef4444; + color: white; + border: none; + padding: 0.875rem 1.5rem; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2); +} + +.btn-delete:hover:not(:disabled) { + background: #dc2626; + box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3); + transform: translateY(-1px); +} + +.btn-delete:disabled { + background: #d1d5db; + cursor: not-allowed; + box-shadow: none; + transform: none; +} + +.delete-info { + margin-top: 0.5rem; + font-size: 0.875rem; + color: #666; + text-align: center; + font-style: italic; +} + .btn-disconnect { background: #ef4444; color: white; diff --git a/examples/openarms_web_interface/App.jsx b/examples/openarms_web_interface/App.jsx index cd926928f..d634b120c 100644 --- a/examples/openarms_web_interface/App.jsx +++ b/examples/openarms_web_interface/App.jsx @@ -21,6 +21,7 @@ function App() { const [rampUpRemaining, setRampUpRemaining] = useState(0); const [movingToZero, setMovingToZero] = useState(false); const [configExpanded, setConfigExpanded] = useState(false); + const [latestRepoId, setLatestRepoId] = useState(null); // Configuration const [config, setConfig] = useState({ @@ -82,6 +83,11 @@ function App() { setUploadStatus(data.upload_status); setRampUpRemaining(data.ramp_up_remaining || 0); setMovingToZero(data.moving_to_zero || false); + + // Track the latest repo_id from the backend + if (data.latest_repo_id) { + setLatestRepoId(data.latest_repo_id); + } if (data.config) { // Only merge server config if we don't have a saved config (first load) @@ -308,13 +314,54 @@ function App() { throw new Error(data.detail || 'Failed to stop recording'); } - await response.json(); + const data = await response.json(); setError(null); + // Update latest repo_id after recording + if (data.dataset_name) { + setLatestRepoId(`lerobot-data-collection/${data.dataset_name}`); + } } catch (e) { setError(e.message); } }; + const deleteLatestEpisode = async () => { + if (!latestRepoId) { + setError('No episode to delete'); + return; + } + + const confirmed = window.confirm( + `WARNING: This will permanently delete the repository:\n\n${latestRepoId}\n\nThis action cannot be undone. Continue?` + ); + + if (!confirmed) { + return; + } + + try { + const response = await fetch(`${API_BASE}/recording/delete-latest`, { method: 'POST' }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.detail || 'Failed to delete episode'); + } + + const data = await response.json(); + setLatestRepoId(null); + setEpisodeCount(Math.max(0, episodeCount - 1)); + setStatusMessage(`Deleted: ${data.deleted_repo}`); + + setTimeout(() => { + if (!isRecording && !isInitializing) { + setStatusMessage('Ready'); + } + }, 3000); + } catch (e) { + setError(`Delete failed: ${e.message}`); + } + }; + // Reset counter const resetCounter = async () => { try { @@ -730,6 +777,20 @@ function App() { + {/* Delete Latest Episode Button */} + {!isRecording && !isInitializing && latestRepoId && ( +
+ +
Will delete: {latestRepoId}
+
+ )} + {/* Move to Zero Button */} {robotsReady && !isRecording && !isInitializing && (
diff --git a/examples/openarms_web_interface/web_record_server.py b/examples/openarms_web_interface/web_record_server.py index cb01d36b6..c114c110f 100644 --- a/examples/openarms_web_interface/web_record_server.py +++ b/examples/openarms_web_interface/web_record_server.py @@ -9,6 +9,7 @@ import asyncio import platform import re import shutil +import subprocess import time from datetime import datetime from pathlib import Path @@ -127,7 +128,6 @@ pedal_thread = None stop_pedal_flag = threading.Event() pedal_action_lock = threading.Lock() # Prevent concurrent pedal actions - class RecordingConfig(BaseModel): task: str leader_type: str # "openarms" or "openarms_mini" @@ -908,7 +908,6 @@ def cleanup_robot_systems(keep_robots=False): # Always clean up dataset robot_instances["dataset"] = None robot_instances["dataset_features"] = None - robot_instances["repo_id"] = None except Exception as e: print(f"[Cleanup] Error: {e}") @@ -1013,6 +1012,7 @@ def do_stop_recording(source: str = "API"): recording_state["status_message"] = "Ready" recording_state["episode_count"] += 1 print(f"[{source}] Upload complete. Episode count: {recording_state['episode_count']}") + else: recording_state["status_message"] = "No data" recording_state["upload_status"] = "No data" @@ -1321,6 +1321,55 @@ async def move_to_zero(): return do_move_to_zero(source="API") +@app.post("/api/recording/delete-latest") +async def delete_latest_episode(): + """Delete the latest recorded episode from HuggingFace Hub.""" + repo_id = robot_instances.get("repo_id") + + if not repo_id: + print(f"[DeleteEpisode] No repository to delete. Please record an episode first.") + return { + "status": "error", + "message": "No repository to delete. Please record an episode first." + } + + try: + print(f"[DeleteEpisode] Deleting repository: {repo_id}") + + hf_tool_path = shutil.which("hf") + if hf_tool_path is None: + print(f"[DeleteEpisode] HuggingFace CLI not found. Please install it.") + return { + "status": "error", + "message": "HuggingFace CLI not found. Please install it." + } + + subprocess.run( + [hf_tool_path, "repo", "delete", repo_id, "--repo-type", "dataset"], + capture_output=True, + text=True, + check=True, + ) + print(f"[DeleteEpisode] Successfully deleted repository: {repo_id}") + + robot_instances["repo_id"] = None + + return { + "status": "success", + "message": f"Successfully deleted repository: {repo_id}", + "deleted_repo": repo_id + } + + except subprocess.CalledProcessError as e: + error_msg = f"Failed to delete repository: {e.stderr}" + print(f"[DeleteEpisode] Error: {error_msg}") + raise HTTPException(status_code=500, detail=error_msg) + except Exception as e: + error_msg = f"Error deleting repository: {str(e)}" + print(f"[DeleteEpisode] Error: {error_msg}") + raise HTTPException(status_code=500, detail=error_msg) + + @app.get("/api/status") async def get_status(): """Get current recording status.""" @@ -1345,7 +1394,8 @@ async def get_status(): "upload_status": recording_state["upload_status"], "ramp_up_remaining": recording_state["ramp_up_remaining"], "moving_to_zero": recording_state["moving_to_zero"], - "config": recording_state["config"] + "config": recording_state["config"], + "latest_repo_id": robot_instances.get("repo_id") }