diff --git a/ui/src/components/record-button.tsx b/ui/src/components/record-button.tsx new file mode 100644 index 00000000..cddd304e --- /dev/null +++ b/ui/src/components/record-button.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; + +const API_BASE = "http://localhost:8000"; + +type RecordingStatus = { + status: "idle" | "recording" | "saving" | "done" | "error" | "no_data"; + message?: string | null; + workflow_file?: string | null; +}; + +interface RecordButtonProps { + onRecordingSaved: (workflowFile: string) => void; +} + +const RecordButton: React.FC = ({ onRecordingSaved }) => { + const [status, setStatus] = useState({ status: "idle" }); + const [busy, setBusy] = useState(false); + const pollRef = useRef(null); + + const stopPolling = useCallback(() => { + if (pollRef.current !== null) { + window.clearInterval(pollRef.current); + pollRef.current = null; + } + }, []); + + const applyStatus = useCallback( + (next: RecordingStatus) => { + setStatus(next); + if (next.status !== "recording" && next.status !== "saving") { + stopPolling(); + if (next.status === "done" && next.workflow_file) { + onRecordingSaved(next.workflow_file); + } + } + }, + [onRecordingSaved, stopPolling] + ); + + const poll = useCallback(async () => { + try { + const res = await fetch(`${API_BASE}/api/workflows/recordings/status`); + applyStatus((await res.json()) as RecordingStatus); + } catch { + /* backend briefly unreachable — keep polling */ + } + }, [applyStatus]); + + const startPolling = useCallback(() => { + stopPolling(); + pollRef.current = window.setInterval(poll, 2000); + }, [poll, stopPolling]); + + useEffect(() => stopPolling, [stopPolling]); + + const start = async () => { + setBusy(true); + try { + const res = await fetch(`${API_BASE}/api/workflows/recordings/start`, { + method: "POST", + }); + applyStatus((await res.json()) as RecordingStatus); + startPolling(); + } catch (e) { + setStatus({ status: "error", message: String(e) }); + } finally { + setBusy(false); + } + }; + + const stop = async () => { + setBusy(true); + try { + const res = await fetch(`${API_BASE}/api/workflows/recordings/stop`, { + method: "POST", + }); + const next = (await res.json()) as RecordingStatus; + applyStatus(next); + // Decide from the response, not the pre-request closure state. + if (next.status === "saving") startPolling(); + } catch (e) { + setStatus({ status: "error", message: String(e) }); + } finally { + setBusy(false); + } + }; + + const isRecording = status.status === "recording" || status.status === "saving"; + + return ( +
+ + + {status.status === "recording" && ( +

+ Recording… interact in the opened browser window, then click Stop (or + close the browser). +

+ )} + {status.status === "saving" && ( +

Saving recording…

+ )} + {status.status === "done" && status.workflow_file && ( +

+ Saved: {status.workflow_file} +

+ )} + {(status.status === "error" || status.status === "no_data") && ( +

+ {status.message ?? "Recording failed"} +

+ )} +
+ ); +}; + +export default RecordButton; diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index 1d6b33e9..1b2519cc 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -1,5 +1,6 @@ import React from "react"; import WorkflowItem from "./workflow-item"; +import RecordButton from "./record-button"; import { WorkflowMetadata } from "../types/workflow-layout.types"; interface SidebarProps { @@ -9,6 +10,7 @@ interface SidebarProps { workflowMetadata: WorkflowMetadata | null; onUpdateMetadata: (metadata: WorkflowMetadata) => Promise; allWorkflowsMetadata?: Record; + onRecordingSaved: (workflowFile: string) => void; } export const Sidebar: React.FC = ({ @@ -18,6 +20,7 @@ export const Sidebar: React.FC = ({ workflowMetadata, onUpdateMetadata, allWorkflowsMetadata = {}, + onRecordingSaved, }) => (