'use client'; import { ChevronUpDownIcon, SpeedometerIcon } from '@/components/icons/Icons'; import { useConfig } from '@/contexts/ConfigContext'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy'; import { PopoverRoot, PopoverSurface, PopoverTrigger, RangeInput } from '@/components/ui'; export const SpeedControl = ({ setSpeedAndRestart, setAudioPlayerSpeedAndRestart }: { setSpeedAndRestart: (speed: number) => void; setAudioPlayerSpeedAndRestart: (speed: number) => void; }) => { const { voiceSpeed, audioPlayerSpeed, providerType, ttsModel } = useConfig(); const nativeSpeedSupported = resolveTtsProviderModelPolicy({ providerRef: '', providerType, model: ttsModel, }).supportsNativeModelSpeed; const [localVoiceSpeed, setLocalVoiceSpeed] = useState(voiceSpeed); const [localAudioSpeed, setLocalAudioSpeed] = useState(audioPlayerSpeed); useEffect(() => { setLocalVoiceSpeed(voiceSpeed); }, [voiceSpeed]); useEffect(() => { setLocalAudioSpeed(audioPlayerSpeed); }, [audioPlayerSpeed]); const handleVoiceSpeedChange = useCallback((event: React.ChangeEvent) => { setLocalVoiceSpeed(parseFloat(event.target.value)); }, []); const handleAudioSpeedChange = useCallback((event: React.ChangeEvent) => { setLocalAudioSpeed(parseFloat(event.target.value)); }, []); const handleVoiceSpeedChangeComplete = useCallback(() => { if (localVoiceSpeed !== voiceSpeed) { setSpeedAndRestart(localVoiceSpeed); } }, [localVoiceSpeed, voiceSpeed, setSpeedAndRestart]); const handleAudioSpeedChangeComplete = useCallback(() => { if (localAudioSpeed !== audioPlayerSpeed) { setAudioPlayerSpeedAndRestart(localAudioSpeed); } }, [localAudioSpeed, audioPlayerSpeed, setAudioPlayerSpeedAndRestart]); const formatSpeed = useCallback((speed: number, maxDecimals: number) => { const rounded = Number(speed.toFixed(maxDecimals)); return rounded.toString(); }, []); const triggerLabel = useMemo( () => { const parts: string[] = []; if (nativeSpeedSupported && localVoiceSpeed !== 1.0) parts.push(`${formatSpeed(localVoiceSpeed, 1)}x`); if (localAudioSpeed !== 1.0) parts.push(`${formatSpeed(localAudioSpeed, 1)}x`); return parts.length > 0 ? parts.join(' • ') : '1x'; }, [formatSpeed, localVoiceSpeed, localAudioSpeed, nativeSpeedSupported] ); const compactTriggerLabel = useMemo(() => { const voiceIsDefault = !nativeSpeedSupported || localVoiceSpeed === 1.0; const audioIsDefault = localAudioSpeed === 1.0; let combined = 1.0; if (nativeSpeedSupported && !voiceIsDefault && !audioIsDefault) { combined = (localVoiceSpeed + localAudioSpeed) / 2; } else if (nativeSpeedSupported && !voiceIsDefault) { combined = localVoiceSpeed; } else if (!audioIsDefault) { combined = localAudioSpeed; } return `${formatSpeed(combined, 2)}x`; }, [formatSpeed, localVoiceSpeed, localAudioSpeed, nativeSpeedSupported]); const min = 0.5; const max = 3; const step = 0.1; return ( {compactTriggerLabel} {triggerLabel}
{!nativeSpeedSupported && (
Native model speed is not available for this model.
)} {nativeSpeedSupported && (
Native model speed
{min.toFixed(1)}x {Number.isInteger(localVoiceSpeed) ? localVoiceSpeed.toString() : localVoiceSpeed.toFixed(1)}x {max.toFixed(1)}x
)}
Audio player speed
{min.toFixed(1)}x {Number.isInteger(localAudioSpeed) ? localAudioSpeed.toString() : localAudioSpeed.toFixed(1)}x {max.toFixed(1)}x
); };