mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-19 08:28:10 +02:00
chore: add custom recordings documentation
This commit is contained in:
parent
2fa4191d9b
commit
dc800bdd63
6 changed files with 211 additions and 37 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>({
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue