mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-13 08:15:21 +02:00
* feat: add stt evals * add smart turn as provider * chore: remove deprecations * chore: format files * fix: remove deprecated UserIdleProcessor * fix: remove deprecated TranscriptProcessor * chore: update pipecat submodule * feat: add evals visualisation * fix: trigger llm generation on client connected and pipeline started * chore: update pipecat * chore: update pipecat submodule * Add tests * fix: slow loading of workflow page * chore: update pipecat submodule * Show version after release * Fixes #99 * fix: provider check for websocket connection * Fixes #107 * Fix #96 * chore: fix documentation * fix: cloudonix campaign call error --------- Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useEffect, useState, useCallback } from "react";
|
|
|
|
interface AudioPlayerProps {
|
|
audioUrl: string;
|
|
duration: number;
|
|
currentTime: number;
|
|
onTimeUpdate: (time: number) => void;
|
|
onPlayingChange: (playing: boolean) => void;
|
|
}
|
|
|
|
function formatTime(seconds: number): string {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
}
|
|
|
|
export default function AudioPlayer({
|
|
audioUrl,
|
|
duration,
|
|
currentTime,
|
|
onTimeUpdate,
|
|
onPlayingChange,
|
|
}: AudioPlayerProps) {
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [internalTime, setInternalTime] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
|
|
const handleTimeUpdate = () => {
|
|
setInternalTime(audio.currentTime);
|
|
onTimeUpdate(audio.currentTime);
|
|
};
|
|
|
|
const handlePlay = () => {
|
|
setIsPlaying(true);
|
|
onPlayingChange(true);
|
|
};
|
|
|
|
const handlePause = () => {
|
|
setIsPlaying(false);
|
|
onPlayingChange(false);
|
|
};
|
|
|
|
const handleEnded = () => {
|
|
setIsPlaying(false);
|
|
onPlayingChange(false);
|
|
};
|
|
|
|
audio.addEventListener("timeupdate", handleTimeUpdate);
|
|
audio.addEventListener("play", handlePlay);
|
|
audio.addEventListener("pause", handlePause);
|
|
audio.addEventListener("ended", handleEnded);
|
|
|
|
return () => {
|
|
audio.removeEventListener("timeupdate", handleTimeUpdate);
|
|
audio.removeEventListener("play", handlePlay);
|
|
audio.removeEventListener("pause", handlePause);
|
|
audio.removeEventListener("ended", handleEnded);
|
|
};
|
|
}, [onTimeUpdate, onPlayingChange]);
|
|
|
|
// Seek to currentTime when it changes externally
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
|
|
// Only seek if the difference is significant (user clicked timeline)
|
|
if (Math.abs(audio.currentTime - currentTime) > 0.5) {
|
|
audio.currentTime = currentTime;
|
|
}
|
|
}, [currentTime]);
|
|
|
|
const togglePlay = useCallback(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
|
|
if (isPlaying) {
|
|
audio.pause();
|
|
} else {
|
|
audio.play();
|
|
}
|
|
}, [isPlaying]);
|
|
|
|
const handleSeek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
|
|
const newTime = parseFloat(e.target.value);
|
|
audio.currentTime = newTime;
|
|
setInternalTime(newTime);
|
|
onTimeUpdate(newTime);
|
|
}, [onTimeUpdate]);
|
|
|
|
return (
|
|
<div className="bg-zinc-900 rounded-lg p-4 space-y-3">
|
|
<audio ref={audioRef} src={audioUrl} preload="metadata" />
|
|
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={togglePlay}
|
|
className="w-12 h-12 rounded-full bg-white text-black flex items-center justify-center hover:bg-zinc-200 transition-colors"
|
|
>
|
|
{isPlaying ? (
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
) : (
|
|
<svg className="w-5 h-5 ml-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
<div className="flex-1 space-y-1">
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={duration}
|
|
step={0.1}
|
|
value={internalTime}
|
|
onChange={handleSeek}
|
|
className="w-full h-2 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-white"
|
|
/>
|
|
<div className="flex justify-between text-xs text-zinc-400">
|
|
<span>{formatTime(internalTime)}</span>
|
|
<span>{formatTime(duration)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|