'use client'; import { useCallback, useEffect, useRef } from 'react'; import { Mic, Loader2 } from 'lucide-react'; import { useAudioRecorder } from '@/lib/hooks/use-audio-recorder'; import { useASRAvailable } from '@/lib/hooks/use-asr-available'; import { useI18n } from '@/lib/hooks/use-i18n'; import { cn } from '@/lib/utils'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from 'sonner'; interface SpeechButtonProps { onTranscription: (text: string) => void; className?: string; disabled?: boolean; size?: 'sm' | 'md'; /** Keep browser-native ASR active until the user manually stops it. */ continuous?: boolean; } export function shouldCancelRecordingOnDisable(args: { continuous?: boolean; disabled?: boolean; isRecording: boolean; }): boolean { return !!args.continuous && !!args.disabled && args.isRecording; } export function SpeechButton({ onTranscription, className, disabled, size = 'sm', continuous, }: SpeechButtonProps) { const { t } = useI18n(); // Ref to always call the latest onTranscription, avoiding stale closures const onTranscriptionRef = useRef(onTranscription); useEffect(() => { onTranscriptionRef.current = onTranscription; }, [onTranscription]); const stableOnTranscription = useCallback((text: string) => { onTranscriptionRef.current(text); }, []); const handleError = useCallback((error: string) => { toast.error(error); }, []); const { isRecording, isProcessing, startRecording, stopRecording, cancelRecording } = useAudioRecorder({ onTranscription: stableOnTranscription, onError: handleError, continuous, }); useEffect(() => { if (shouldCancelRecordingOnDisable({ continuous, disabled, isRecording })) { cancelRecording(); } }, [continuous, disabled, isRecording, cancelRecording]); const active = isRecording || isProcessing; // Gate on ASR availability (toggle + provider configured + browser support) // so every call site is disabled uniformly when ASR is off/unusable — but // never while actively recording/processing, so the user can always click to // stop (the recorder has no auto-stop and would otherwise leave the mic open). const asrAvailable = useASRAvailable(); const isDisabled = (disabled || !asrAvailable) && !active; const handleClick = () => { if (isRecording) { stopRecording(); } else if (!isProcessing) { startRecording(); } }; const isMd = size === 'md'; const sizeClasses = isMd ? 'h-8 w-8' : 'h-6 w-6'; const iconSize = isMd ? 'w-4 h-4' : 'w-3.5 h-3.5'; const barH = isMd ? 14 : 10; return ( {isProcessing ? t('roundtable.processing') : isRecording ? t('voice.stopListening') : t('voice.startListening')} ); }