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
This commit is contained in:
Michel Aractingi
2025-11-27 13:36:51 +01:00
committed by GitHub
parent 1753235a61
commit f816092993
3 changed files with 156 additions and 4 deletions
+41
View File
@@ -205,6 +205,47 @@ h3 {
transform: none; 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 { .btn-disconnect {
background: #ef4444; background: #ef4444;
color: white; color: white;
+62 -1
View File
@@ -21,6 +21,7 @@ function App() {
const [rampUpRemaining, setRampUpRemaining] = useState(0); const [rampUpRemaining, setRampUpRemaining] = useState(0);
const [movingToZero, setMovingToZero] = useState(false); const [movingToZero, setMovingToZero] = useState(false);
const [configExpanded, setConfigExpanded] = useState(false); const [configExpanded, setConfigExpanded] = useState(false);
const [latestRepoId, setLatestRepoId] = useState(null);
// Configuration // Configuration
const [config, setConfig] = useState({ const [config, setConfig] = useState({
@@ -83,6 +84,11 @@ function App() {
setRampUpRemaining(data.ramp_up_remaining || 0); setRampUpRemaining(data.ramp_up_remaining || 0);
setMovingToZero(data.moving_to_zero || false); 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) { if (data.config) {
// Only merge server config if we don't have a saved config (first load) // Only merge server config if we don't have a saved config (first load)
if (!localStorage.getItem('openarms_config')) { if (!localStorage.getItem('openarms_config')) {
@@ -308,13 +314,54 @@ function App() {
throw new Error(data.detail || 'Failed to stop recording'); throw new Error(data.detail || 'Failed to stop recording');
} }
await response.json(); const data = await response.json();
setError(null); setError(null);
// Update latest repo_id after recording
if (data.dataset_name) {
setLatestRepoId(`lerobot-data-collection/${data.dataset_name}`);
}
} catch (e) { } catch (e) {
setError(e.message); 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 // Reset counter
const resetCounter = async () => { const resetCounter = async () => {
try { try {
@@ -730,6 +777,20 @@ function App() {
</div> </div>
</div> </div>
{/* Delete Latest Episode Button */}
{!isRecording && !isInitializing && latestRepoId && (
<div className="delete-episode-section">
<button
onClick={deleteLatestEpisode}
className="btn-delete"
title="Delete the latest recorded episode from HuggingFace Hub"
>
Delete Latest Episode
</button>
<div className="delete-info">Will delete: {latestRepoId}</div>
</div>
)}
{/* Move to Zero Button */} {/* Move to Zero Button */}
{robotsReady && !isRecording && !isInitializing && ( {robotsReady && !isRecording && !isInitializing && (
<div className="zero-position-section"> <div className="zero-position-section">
@@ -9,6 +9,7 @@ import asyncio
import platform import platform
import re import re
import shutil import shutil
import subprocess
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -127,7 +128,6 @@ pedal_thread = None
stop_pedal_flag = threading.Event() stop_pedal_flag = threading.Event()
pedal_action_lock = threading.Lock() # Prevent concurrent pedal actions pedal_action_lock = threading.Lock() # Prevent concurrent pedal actions
class RecordingConfig(BaseModel): class RecordingConfig(BaseModel):
task: str task: str
leader_type: str # "openarms" or "openarms_mini" leader_type: str # "openarms" or "openarms_mini"
@@ -908,7 +908,6 @@ def cleanup_robot_systems(keep_robots=False):
# Always clean up dataset # Always clean up dataset
robot_instances["dataset"] = None robot_instances["dataset"] = None
robot_instances["dataset_features"] = None robot_instances["dataset_features"] = None
robot_instances["repo_id"] = None
except Exception as e: except Exception as e:
print(f"[Cleanup] Error: {e}") print(f"[Cleanup] Error: {e}")
@@ -1013,6 +1012,7 @@ def do_stop_recording(source: str = "API"):
recording_state["status_message"] = "Ready" recording_state["status_message"] = "Ready"
recording_state["episode_count"] += 1 recording_state["episode_count"] += 1
print(f"[{source}] Upload complete. Episode count: {recording_state['episode_count']}") print(f"[{source}] Upload complete. Episode count: {recording_state['episode_count']}")
else: else:
recording_state["status_message"] = "No data" recording_state["status_message"] = "No data"
recording_state["upload_status"] = "No data" recording_state["upload_status"] = "No data"
@@ -1321,6 +1321,55 @@ async def move_to_zero():
return do_move_to_zero(source="API") 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") @app.get("/api/status")
async def get_status(): async def get_status():
"""Get current recording status.""" """Get current recording status."""
@@ -1345,7 +1394,8 @@ async def get_status():
"upload_status": recording_state["upload_status"], "upload_status": recording_state["upload_status"],
"ramp_up_remaining": recording_state["ramp_up_remaining"], "ramp_up_remaining": recording_state["ramp_up_remaining"],
"moving_to_zero": recording_state["moving_to_zero"], "moving_to_zero": recording_state["moving_to_zero"],
"config": recording_state["config"] "config": recording_state["config"],
"latest_repo_id": robot_instances.get("repo_id")
} }