Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Recording from "@/pages/Recording";
import Training from "@/pages/Training";
import Inference from "@/pages/Inference";
import EditDataset from "@/pages/EditDataset";
import RerecordEpisode from "@/pages/RerecordEpisode";
import Upload from "@/pages/Upload";

import NotFound from "@/pages/NotFound";
Expand Down Expand Up @@ -46,6 +47,7 @@ function App() {
<Route path="/inference" element={<Inference />} />
<Route path="/calibration" element={<Calibration />} />
<Route path="/edit-dataset" element={<EditDataset />} />
<Route path="/rerecord" element={<RerecordEpisode />} />

<Route path="*" element={<NotFound />} />
</Routes>
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/lib/jobsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,28 @@ export async function stopJob(
});
}

export async function pauseJob(
baseUrl: string,
fetcher: Fetcher,
id: string,
): Promise<JobRecord> {
return apiRequest<JobRecord>(baseUrl, fetcher, `/jobs/${id}/pause`, {
method: "POST",
action: "Pause job",
});
}

export async function resumeJob(
baseUrl: string,
fetcher: Fetcher,
id: string,
): Promise<JobRecord> {
return apiRequest<JobRecord>(baseUrl, fetcher, `/jobs/${id}/resume`, {
method: "POST",
action: "Resume job",
});
}

export async function deleteJob(
baseUrl: string,
fetcher: Fetcher,
Expand Down
23 changes: 22 additions & 1 deletion frontend/src/pages/Landing.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { ChevronsUpDown } from "lucide-react";
import { ChevronsUpDown, Pencil, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import LandingTopBar from "@/components/landing/LandingTopBar";
Expand Down Expand Up @@ -266,6 +266,27 @@ const Landing = () => {
</Button>
</DatasetPicker>
</div>
<div className="bg-gray-800 rounded-lg border border-gray-700 p-3 flex flex-col gap-2">
<h3 className="font-semibold text-lg text-left h-10 flex items-center">
Edit Datasets
</h3>
<Button
onClick={() => navigate("/edit-dataset")}
variant="outline"
className="w-full border-gray-600 text-gray-200 hover:bg-gray-700 hover:text-white"
>
<Pencil className="w-4 h-4 mr-2" />
Merge &amp; Edit
</Button>
<Button
onClick={() => navigate("/rerecord")}
variant="outline"
className="w-full border-gray-600 text-gray-200 hover:bg-gray-700 hover:text-white"
>
<RefreshCw className="w-4 h-4 mr-2" />
Add / Re-record
</Button>
</div>
<div className="bg-gray-800 rounded-lg border border-gray-700 p-3 flex flex-col gap-2">
<h3 className="font-semibold text-lg text-left h-10 flex items-center">
Create a model
Expand Down
76 changes: 55 additions & 21 deletions frontend/src/pages/Recording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
RotateCcw,
Square,
SkipForward,
Pause,
Play,
Volume2,
VolumeX,
Expand Down Expand Up @@ -54,10 +55,11 @@ interface RecordingConfig {
streaming_encoding: boolean;
}

type Phase = "preparing" | "recording" | "resetting" | "completed";
type Phase = "preparing" | "recording" | "resetting" | "paused" | "completed";

interface BackendStatus {
recording_active: boolean;
recording_paused: boolean;
current_phase: string;
current_episode?: number;
total_episodes?: number;
Expand All @@ -73,6 +75,8 @@ interface BackendStatus {
stop_recording: boolean;
exit_early: boolean;
rerecord_episode: boolean;
pause_recording: boolean;
resume_recording: boolean;
};
}

Expand Down Expand Up @@ -243,23 +247,6 @@ const Recording = () => {
}

if (!status.recording_active && status.session_ended) {
// A failure can land after episodes were already saved, so surface
// the reason either way and only go home when nothing survived it.
if (status.current_phase === "error") {
const saved = status.saved_episodes || 0;
toast({
title: saved > 0 ? "Recording Interrupted" : "Recording Failed",
description:
saved > 0
? `${saved} episode(s) were saved before the session failed: ${status.error || "unknown error"}`
: status.error || "The recording session failed to start.",
variant: "destructive",
});
if (saved === 0) {
navigate("/");
return;
}
}
const datasetInfo = {
dataset_repo_id:
status.dataset_repo_id || recordingConfig.dataset_repo_id,
Expand All @@ -278,7 +265,7 @@ const Recording = () => {
pollStatus();
const statusInterval = setInterval(pollStatus, 1000);
return () => clearInterval(statusInterval);
}, [recordingSessionStarted, recordingConfig, navigate, baseUrl, fetchWithHeaders, toast]);
}, [recordingSessionStarted, recordingConfig, navigate, baseUrl, fetchWithHeaders]);

const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
Expand Down Expand Up @@ -392,6 +379,26 @@ const Recording = () => {
}
}, [backendStatus, baseUrl, fetchWithHeaders, toast]);

const handlePauseRecording = useCallback(async () => {
if (!backendStatus?.available_controls.pause_recording) return;
try {
await fetchWithHeaders(`${baseUrl}/pause-recording`, { method: "POST" });
toast({ title: "Recording paused", description: "Current episode saved. Click Resume to continue." });
} catch {
toast({ title: "Error", description: "Failed to pause recording.", variant: "destructive" });
}
}, [backendStatus, baseUrl, fetchWithHeaders, toast]);

const handleResumeRecording = useCallback(async () => {
if (!backendStatus?.available_controls.resume_recording) return;
try {
await fetchWithHeaders(`${baseUrl}/resume-recording`, { method: "POST" });
toast({ title: "Recording resumed" });
} catch {
toast({ title: "Error", description: "Failed to resume recording.", variant: "destructive" });
}
}, [backendStatus, baseUrl, fetchWithHeaders, toast]);

const handleStopRecording = useCallback(async () => {
if (!backendStatus?.available_controls.stop_recording) return;
try {
Expand Down Expand Up @@ -510,6 +517,7 @@ const Recording = () => {
if (currentPhase === "recording") return `RECORDING EPISODE ${currentEpisode}`;
if (currentPhase === "resetting") return "RESET — GET READY";
if (currentPhase === "preparing") return "PREPARING SESSION";
if (currentPhase === "paused") return "⏸ PAUSED — CLICK RESUME TO CONTINUE";
return "SESSION COMPLETE";
};

Expand All @@ -518,6 +526,8 @@ const Recording = () => {
? { dot: "bg-red-500", pill: "bg-red-500/15 text-red-300", timer: "text-green-400", bar: "bg-green-500", button: "bg-green-500 hover:bg-green-600" }
: currentPhase === "resetting"
? { dot: "bg-orange-500", pill: "bg-orange-500/15 text-orange-300", timer: "text-orange-400", bar: "bg-orange-500", button: "bg-orange-500 hover:bg-orange-600" }
: currentPhase === "paused"
? { dot: "bg-amber-400", pill: "bg-amber-400/15 text-amber-300", timer: "text-amber-400", bar: "bg-amber-400", button: "bg-amber-500 hover:bg-amber-600" }
: { dot: "bg-gray-500", pill: "bg-gray-500/15 text-gray-300", timer: "text-gray-400", bar: "bg-gray-500", button: "bg-gray-500" };

const primaryLabel =
Expand Down Expand Up @@ -560,6 +570,30 @@ const Recording = () => {
>
{muted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
</Button>
{backendStatus.available_controls.pause_recording && (
<Button
variant="ghost"
size="icon"
onClick={handlePauseRecording}
aria-label="Pause recording"
className="h-8 w-8 text-amber-400 hover:text-amber-300 hover:bg-gray-800"
title="Pause recording (saves current episode)"
>
<Pause className="w-5 h-5" />
</Button>
)}
{backendStatus.available_controls.resume_recording && (
<Button
variant="ghost"
size="icon"
onClick={handleResumeRecording}
aria-label="Resume recording"
className="h-8 w-8 text-green-400 hover:text-green-300 hover:bg-gray-800"
title="Resume recording"
>
<Play className="w-5 h-5" />
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
Expand Down Expand Up @@ -703,6 +737,8 @@ const Recording = () => {
);
};

export default Recording;

interface CameraFeedProps {
baseUrl: string;
name: string;
Expand Down Expand Up @@ -774,5 +810,3 @@ const CameraFeed: React.FC<CameraFeedProps> = ({
</div>
);
};

export default Recording;
Loading