chore: add custom recordings documentation

This commit is contained in:
Abhishek Kumar 2026-03-25 15:44:54 +05:30
parent 2fa4191d9b
commit dc800bdd63
6 changed files with 211 additions and 37 deletions

View file

@ -1,9 +1,10 @@
import { Loader2, Mic, Square, Trash2Icon, Upload } from "lucide-react";
import { Loader2, Mic, Pause, Play, Square, Trash2Icon, Upload } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
createRecordingApiV1WorkflowRecordingsPost,
deleteRecordingApiV1WorkflowRecordingsRecordingIdDelete,
getSignedUrlApiV1S3SignedUrlGet,
getUploadUrlApiV1WorkflowRecordingsUploadUrlPost,
listRecordingsApiV1WorkflowRecordingsGet,
transcribeAudioApiV1WorkflowRecordingsTranscribePost,
@ -58,6 +59,8 @@ export const RecordingsDialog = ({
const [recordingStep, setRecordingStep] = useState<RecordingStep>("idle");
const [recordingFilename, setRecordingFilename] = useState("");
const [recordingDuration, setRecordingDuration] = useState(0);
const [playingId, setPlayingId] = useState<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
const recordingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@ -110,6 +113,14 @@ export const RecordingsDialog = ({
setRecordingDuration(0);
}, []);
const stopPlayback = useCallback(() => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setPlayingId(null);
}, []);
useEffect(() => {
if (open) {
fetchRecordings();
@ -125,8 +136,9 @@ export const RecordingsDialog = ({
if (!open) {
stopRecording();
stopRecordingTimer();
stopPlayback();
}
}, [open, stopRecording, stopRecordingTimer]);
}, [open, stopRecording, stopRecordingTimer, stopPlayback]);
const transcribeFile = async (file: File) => {
setRecordingStep("transcribing");
@ -295,6 +307,33 @@ export const RecordingsDialog = ({
}
};
const handlePlay = async (rec: RecordingResponseSchema) => {
if (playingId === rec.recording_id) {
stopPlayback();
return;
}
stopPlayback();
try {
const result = await getSignedUrlApiV1S3SignedUrlGet({
query: {
key: rec.storage_key,
storage_backend: rec.storage_backend,
},
});
if (!result.data?.url) {
setError("Failed to get audio URL");
return;
}
const audio = new Audio(result.data.url);
audio.onended = () => setPlayingId(null);
audioRef.current = audio;
setPlayingId(rec.recording_id);
await audio.play();
} catch {
setError("Failed to play recording");
}
};
const isRecording = recordingStep === "recording";
const isTranscribing = recordingStep === "transcribing";
const isBusy = uploading || isRecording || isTranscribing;
@ -540,6 +579,17 @@ export const RecordingsDialog = ({
{rec.transcript}
</p>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handlePlay(rec)}
>
{playingId === rec.recording_id ? (
<Pause className="w-4 h-4" />
) : (
<Play className="w-4 h-4" />
)}
</Button>
<Button
size="sm"
variant="ghost"

View file

@ -1064,11 +1064,15 @@ export const getCampaignDefaultsApiV1OrganizationsCampaignDefaultsGet = <ThrowOn
/**
* Generate a signed S3 URL
* Return a short-lived signed URL for a transcript or recording file stored on S3.
* Return a short-lived signed URL for a file stored on S3 / MinIO.
*
* Access Control:
* * Keys that embed an organization ID (``{prefix}/{org_id}/...``) are
* authorized by matching the org_id against the requesting user's
* organization.
* * Legacy keys (``recordings/{run_id}.wav``, ``transcripts/{run_id}.txt``)
* are authorized via the workflow run they belong to.
* * Superusers can request any key.
* * Regular users can only request resources belonging to **their** workflow runs.
*/
export const getSignedUrlApiV1S3SignedUrlGet = <ThrowOnError extends boolean = false>(options: Options<GetSignedUrlApiV1S3SignedUrlGetData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).get<GetSignedUrlApiV1S3SignedUrlGetResponse, GetSignedUrlApiV1S3SignedUrlGetError, ThrowOnError>({

View file

@ -3964,6 +3964,10 @@ export type GetSignedUrlApiV1S3SignedUrlGetData = {
key: string;
expires_in?: number;
inline?: boolean;
/**
* Storage backend to use (e.g. 'minio', 's3'). When omitted the backend is inferred from the resource.
*/
storage_backend?: string | null;
};
url: '/api/v1/s3/signed-url';
};