diff --git a/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx b/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx index 455774cb..fb8619af 100644 --- a/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx @@ -92,6 +92,8 @@ export const VoiceSettings: React.FC = () => { const setSttSilenceThresholdDb = useConfigStore((state) => state.setSttSilenceThresholdDb); const sttSilenceHoldMs = useConfigStore((state) => state.sttSilenceHoldMs); const setSttSilenceHoldMs = useConfigStore((state) => state.setSttSilenceHoldMs); + const sttTranscribeOnStop = useConfigStore((state) => state.sttTranscribeOnStop); + const setSttTranscribeOnStop = useConfigStore((state) => state.setSttTranscribeOnStop); const setShowMessageTTSButtons = useConfigStore((state) => state.setShowMessageTTSButtons); const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled); const setVoiceModeEnabled = useConfigStore((state) => state.setVoiceModeEnabled); @@ -769,6 +771,18 @@ export const VoiceSettings: React.FC = () => { {sttProvider === 'server' && (
+
setSttTranscribeOnStop(!sttTranscribeOnStop)} + onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setSttTranscribeOnStop(!sttTranscribeOnStop); } }} + > + + {t('settings.voice.page.field.transcribeOnStop')} +
+ {!audioStreamService.isSupported() && (

{t('settings.voice.page.field.sttBrowserSupportError')} diff --git a/packages/ui/src/components/voice/BrowserVoiceButton.tsx b/packages/ui/src/components/voice/BrowserVoiceButton.tsx index 70b17431..d72abb4b 100644 --- a/packages/ui/src/components/voice/BrowserVoiceButton.tsx +++ b/packages/ui/src/components/voice/BrowserVoiceButton.tsx @@ -25,6 +25,7 @@ import { import { VoiceStatusIndicator } from './VoiceStatusIndicator'; import { toast } from '@/components/ui/toast'; import { Icon } from "@/components/icon/Icon"; +import { useI18n } from '@/lib/i18n'; // Status text for accessibility and labels const statusLabels: Record = { @@ -65,7 +66,10 @@ const normalizeVoiceErrorMessage = (error: string): string => { * Browser Voice Button with language selection */ export function BrowserVoiceButton() { + const { t } = useI18n(); const voiceModeEnabled = useConfigStore((s) => s.voiceModeEnabled); + const sttProvider = useConfigStore((s) => s.sttProvider); + const sttTranscribeOnStop = useConfigStore((s) => s.sttTranscribeOnStop); const { status, @@ -74,6 +78,7 @@ export function BrowserVoiceButton() { startVoice, stopVoice, + finishVoiceInput, conversationMode, toggleConversationMode, isMobile, @@ -108,6 +113,8 @@ export function BrowserVoiceButton() { const isIdle = status === 'idle'; const isSpeaking = status === 'speaking'; + const canTranscribeOnStop = sttProvider === 'server' && sttTranscribeOnStop; + const isListeningWithTranscribeOnStop = status === 'listening' && canTranscribeOnStop; // Show toast notification when voice error occurs useEffect(() => { @@ -131,6 +138,8 @@ export function BrowserVoiceButton() { // Status text for accessibility const statusText = isError ? error || 'Voice Error' + : isListeningWithTranscribeOnStop + ? t('voice.action.finishAndTranscribe') : conversationMode && status === 'idle' ? 'Start Voice (Continuous mode on)' : statusLabels[status] || 'Start Voice'; @@ -140,6 +149,9 @@ export function BrowserVoiceButton() { if (isError && error) { return normalizeVoiceErrorMessage(error); } + if (isListeningWithTranscribeOnStop) { + return t('voice.action.finishAndTranscribe'); + } if (isActive) { return 'Stop voice conversation'; } @@ -152,6 +164,10 @@ export function BrowserVoiceButton() { // Handle voice activation (used by both click and touch) const activateVoice = useCallback(async () => { if (isActive) { + if (status === 'listening' && canTranscribeOnStop) { + finishVoiceInput(); + return; + } stopVoice(); } else if (status !== 'error') { // On mobile, we must NOT do any async operations before calling startVoice() @@ -181,7 +197,7 @@ export function BrowserVoiceButton() { } } } - }, [isActive, status, startVoice, stopVoice, isMobile]); + }, [isActive, status, canTranscribeOnStop, finishVoiceInput, startVoice, stopVoice, isMobile]); // Handle Shift+Click to toggle conversation mode const handleClick = useCallback(async (e: React.MouseEvent) => { diff --git a/packages/ui/src/hooks/useBrowserVoice.ts b/packages/ui/src/hooks/useBrowserVoice.ts index c641816c..783c9c2f 100644 --- a/packages/ui/src/hooks/useBrowserVoice.ts +++ b/packages/ui/src/hooks/useBrowserVoice.ts @@ -53,6 +53,8 @@ export interface UseBrowserVoiceReturn { startVoice: () => void; /** Stop voice mode */ stopVoice: () => void; + /** Finish current voice input and process it */ + finishVoiceInput: () => void; /** Whether conversation mode is active */ conversationMode: boolean; /** Toggle conversation mode */ @@ -834,6 +836,55 @@ export function useBrowserVoice(): UseBrowserVoiceReturn { setStatus('idle'); setError(null); }, [stopServerTTS, stopSayTTS]); + + const finishVoiceInput = useCallback(() => { + if (!isActiveRef.current) { + return; + } + + pendingResumeOnVisibleRef.current = false; + if (deviceChangeRestartTimerRef.current) { + clearTimeout(deviceChangeRestartTimerRef.current); + deviceChangeRestartTimerRef.current = null; + } + setStatus('processing'); + + if (sttProvider === 'server') { + void audioStreamService.finishListening().then(() => { + window.setTimeout(() => { + if (!isActiveRef.current) { + return; + } + if (pendingFinalTranscriptRef.current || finalTranscriptTimerRef.current) { + return; + } + if (processingMessageRef.current) { + return; + } + isActiveRef.current = false; + processingMessageRef.current = false; + setStatus('idle'); + }, FINAL_TRANSCRIPT_SETTLE_MS + 200); + }); + return; + } + + browserVoiceService.stopListening(); + window.setTimeout(() => { + if (!isActiveRef.current) { + return; + } + if (pendingFinalTranscriptRef.current || finalTranscriptTimerRef.current) { + return; + } + if (processingMessageRef.current) { + return; + } + isActiveRef.current = false; + processingMessageRef.current = false; + setStatus('idle'); + }, FINAL_TRANSCRIPT_SETTLE_MS + 300); + }, [sttProvider]); // Cleanup on unmount useEffect(() => { @@ -863,6 +914,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn { setLanguage, startVoice, stopVoice, + finishVoiceInput, conversationMode, toggleConversationMode, prepareVoice, diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 5c80ca14..49724cbc 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1330,6 +1330,8 @@ export const settingsDict = { 'settings.voice.page.field.sttLanguageHint': 'BCP-47 code (for example en, fr). Leave blank for auto-detect.', 'settings.voice.page.field.silenceThreshold': 'Silence Threshold', 'settings.voice.page.field.silenceHold': 'Silence Hold', + 'settings.voice.page.field.transcribeOnStopAria': 'Transcribe when stopping recording', + 'settings.voice.page.field.transcribeOnStop': 'Transcribe on Stop', 'settings.voice.page.field.millisecondsUnit': 'ms', 'settings.voice.page.field.messageReadAloudButtonAria': 'Message read aloud button', 'settings.voice.page.field.messageReadAloudButton': 'Message Read Aloud Button', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 9da22acc..c11cb6b1 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -2021,6 +2021,7 @@ export const dict = { 'voice.status.speaking': 'Speaking...', 'voice.status.error': 'Voice Error', 'voice.status.conversationModeActiveAria': 'Conversation mode active', + 'voice.action.finishAndTranscribe': 'Finish and transcribe voice input', 'onboarding.common.actions.back': 'Back', 'onboarding.common.copyToClipboard': 'Copy to clipboard', 'onboarding.common.status.copiedToClipboard': 'Copied to clipboard', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index c2fd1527..188ff9d9 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1330,6 +1330,8 @@ export const settingsDict = { "settings.voice.page.field.sttLanguageHint": "Código BCP-47 (por ejemplo en, fr). Dejar en blanco para detección automática.", "settings.voice.page.field.silenceThreshold": "Umbral de silencio", "settings.voice.page.field.silenceHold": "Retención de silencio", + "settings.voice.page.field.transcribeOnStopAria": "Transcribir al detener la grabación", + "settings.voice.page.field.transcribeOnStop": "Transcribir al detener", "settings.voice.page.field.millisecondsUnit": "ms", "settings.voice.page.field.messageReadAloudButtonAria": "Botón de lectura en voz alta del mensaje", "settings.voice.page.field.messageReadAloudButton": "Botón de lectura en voz alta del mensaje", diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 4c883b1b..6afb330c 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1987,6 +1987,7 @@ export const dict: Record = { "voice.status.speaking": "Hablando...", "voice.status.error": "Error de voz", "voice.status.conversationModeActiveAria": "Modo de conversación activo", + "voice.action.finishAndTranscribe": "Finalizar y transcribir la entrada de voz", "onboarding.common.actions.back": "Atrás", "onboarding.common.copyToClipboard": "Copiar al portapapeles", "onboarding.common.status.copiedToClipboard": "Copiado al portapapeles", diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index ddcede71..f87baebd 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1330,6 +1330,8 @@ export const settingsDict = { 'settings.voice.page.field.sttLanguageHint': 'BCP-47 코드(예: en, fr). 자동 감지하려면 비워 두세요.', 'settings.voice.page.field.silenceThreshold': '무음 기준', 'settings.voice.page.field.silenceHold': '무음 유지', + 'settings.voice.page.field.transcribeOnStopAria': '녹음을 중지할 때 받아쓰기', + 'settings.voice.page.field.transcribeOnStop': '중지 시 받아쓰기', 'settings.voice.page.field.millisecondsUnit': 'ms', 'settings.voice.page.field.messageReadAloudButtonAria': '메시지 읽어주기 버튼', 'settings.voice.page.field.messageReadAloudButton': '메시지 읽어주기 버튼', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 7680f558..943886aa 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -2021,6 +2021,7 @@ export const dict: Record = { 'voice.status.speaking': '말하는 중…', 'voice.status.error': '음성 오류', 'voice.status.conversationModeActiveAria': '대화 모드 활성 상태', + 'voice.action.finishAndTranscribe': '음성 입력을 완료하고 받아쓰기', 'onboarding.common.actions.back': '뒤로', 'onboarding.common.copyToClipboard': '클립보드에 복사', 'onboarding.common.status.copiedToClipboard': '클립보드에 복사했습니다', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 50d82870..5e48e5b2 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -1502,6 +1502,8 @@ export const settingsDict = { 'settings.voice.page.field.serverUrlHint': 'Bazowy URL serwera TTS zgodnego z OpenAI', 'settings.voice.page.field.silenceHold': 'Podtrzymanie ciszy', 'settings.voice.page.field.silenceThreshold': 'Próg ciszy', + 'settings.voice.page.field.transcribeOnStopAria': 'Transkrybuj po zatrzymaniu nagrania', + 'settings.voice.page.field.transcribeOnStop': 'Transkrybuj po zatrzymaniu', 'settings.voice.page.field.speechPitch': 'Wysokość głosu', 'settings.voice.page.field.speechRate': 'Tempo mowy', 'settings.voice.page.field.speechVolume': 'Głośność mowy', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 418d3c14..58f81033 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -582,6 +582,7 @@ export const dict: Record = { 'voice.status.speaking': 'Mówię...', 'voice.status.error': 'Błąd głosu', 'voice.status.conversationModeActiveAria': 'Tryb rozmowy aktywny', + 'voice.action.finishAndTranscribe': 'Zakończ i transkrybuj wejście głosowe', 'onboarding.common.actions.back': 'Wstecz', 'onboarding.common.copyToClipboard': 'Kopiuj do schowka', 'onboarding.common.status.copiedToClipboard': 'Skopiowano do schowka', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index c5f37d7f..4157aab4 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1330,6 +1330,8 @@ export const settingsDict = { "settings.voice.page.field.sttLanguageHint": "Código BCP-47 (por exemplo en, fr). Deixe em branco para detecção automática.", "settings.voice.page.field.silenceThreshold": "Limite de silêncio", "settings.voice.page.field.silenceHold": "Retenção de silêncio", + "settings.voice.page.field.transcribeOnStopAria": "Transcrever ao parar a gravação", + "settings.voice.page.field.transcribeOnStop": "Transcrever ao parar", "settings.voice.page.field.millisecondsUnit": "ms", "settings.voice.page.field.messageReadAloudButtonAria": "Botão de leitura em voz alta da mensagem", "settings.voice.page.field.messageReadAloudButton": "Botão de leitura em voz alta da mensagem", diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index f514e13b..9e1ddc00 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1987,6 +1987,7 @@ export const dict: Record = { "voice.status.speaking": "Falando...", "voice.status.error": "Erro de voz", "voice.status.conversationModeActiveAria": "Modo de conversa ativo", + "voice.action.finishAndTranscribe": "Finalizar e transcrever a entrada de voz", "onboarding.common.actions.back": "Voltar", "onboarding.common.copyToClipboard": "Copiar para a área de transferência", "onboarding.common.status.copiedToClipboard": "Copiado para a área de transferência", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 5655c138..8e3a7669 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1330,6 +1330,8 @@ export const settingsDict = { "settings.voice.page.field.sttLanguageHint": "BCP-47 код (наприклад en, fr). Залиште поле порожнім для автоматичного визначення.", "settings.voice.page.field.silenceThreshold": "Поріг тиші", "settings.voice.page.field.silenceHold": "Утримання тиші", + "settings.voice.page.field.transcribeOnStopAria": "Розпізнавати після зупинки запису", + "settings.voice.page.field.transcribeOnStop": "Розпізнавати при зупинці", "settings.voice.page.field.millisecondsUnit": "мс", "settings.voice.page.field.messageReadAloudButtonAria": "Кнопка «Прочитати повідомлення вголос»", "settings.voice.page.field.messageReadAloudButton": "Кнопка «Прочитати вголос» для повідомлень", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 813cf9b7..9170a8a9 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1987,6 +1987,7 @@ export const dict: Record = { "voice.status.speaking": "Говорить...", "voice.status.error": "Голосова помилка", "voice.status.conversationModeActiveAria": "Активний режим розмови", + "voice.action.finishAndTranscribe": "Завершити й розпізнати голосове введення", "onboarding.common.actions.back": "Назад", "onboarding.common.copyToClipboard": "Копіювати в буфер обміну", "onboarding.common.status.copiedToClipboard": "Скопійовано в буфер обміну", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 34bdc06f..4d94a006 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1330,6 +1330,8 @@ export const settingsDict = { 'settings.voice.page.field.sttLanguageHint': 'BCP-47 代码(例如 en、fr)。留空为自动检测。', 'settings.voice.page.field.silenceThreshold': '静音阈值', 'settings.voice.page.field.silenceHold': '静音保持时长', + 'settings.voice.page.field.transcribeOnStopAria': '停止录音时转写', + 'settings.voice.page.field.transcribeOnStop': '停止时转写', 'settings.voice.page.field.millisecondsUnit': '毫秒', 'settings.voice.page.field.messageReadAloudButtonAria': '消息朗读按钮', 'settings.voice.page.field.messageReadAloudButton': '消息朗读按钮', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index bd879f48..d3f03260 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1987,6 +1987,7 @@ export const dict: Record = { 'voice.status.speaking': '播放中...', 'voice.status.error': '语音错误', 'voice.status.conversationModeActiveAria': '会话模式已启用', + 'voice.action.finishAndTranscribe': '结束并转写语音输入', 'onboarding.common.actions.back': '返回', 'onboarding.common.copyToClipboard': '复制到剪贴板', 'onboarding.common.status.copiedToClipboard': '已复制到剪贴板', diff --git a/packages/ui/src/lib/voice/audioStreamService.ts b/packages/ui/src/lib/voice/audioStreamService.ts index fe1d5f61..1e26ceb6 100644 --- a/packages/ui/src/lib/voice/audioStreamService.ts +++ b/packages/ui/src/lib/voice/audioStreamService.ts @@ -59,6 +59,7 @@ class AudioStreamService { private silenceSince: number | null = null; private onResult: SpeechResultCallback | null = null; private onError: ErrorCallback | null = null; + private finishResolver: (() => void) | null = null; private lang = 'en'; // Configurable parameters @@ -125,16 +126,29 @@ class AudioStreamService { /** Stop listening and clean up all resources. */ stopListening(): void { - this.isActive = false; this._stopVAD(); this._stopRecorder(); - this._teardownAudioContext(); - this._releaseStream(); - this.chunks = []; + this._cleanupAfterStop(true); + } + + async finishListening(): Promise { + if (!this.isActive) return; + + this._stopVAD(); this.isSpeaking = false; this.silenceSince = null; - this.onResult = null; - this.onError = null; + + if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') { + this._cleanupAfterStop(true); + return; + } + + await new Promise((resolve) => { + this.finishResolver = resolve; + this._finaliseUtterance(false); + }); + + this._cleanupAfterStop(true); } /** Whether currently listening. */ @@ -186,11 +200,18 @@ class AudioStreamService { this.mediaRecorder.onstop = () => { const blobs = this.chunks.splice(0); const durationMs = Date.now() - this.recordingStartMs; - if (blobs.length === 0 || durationMs < MIN_UTTERANCE_MS) return; + if (blobs.length === 0 || durationMs < MIN_UTTERANCE_MS) { + this.finishResolver?.(); + this.finishResolver = null; + return; + } const mType = blobs[0].type || mimeType || 'audio/webm'; const blob = new Blob(blobs, { type: mType }); - void this._upload(blob, mType); + void this._upload(blob, mType).finally(() => { + this.finishResolver?.(); + this.finishResolver = null; + }); }; // Collect data every 250 ms so we don't lose the tail on stop() @@ -202,6 +223,8 @@ class AudioStreamService { try { this.mediaRecorder.stop(); } catch { + this.finishResolver?.(); + this.finishResolver = null; // ignore } } @@ -244,7 +267,7 @@ class AudioStreamService { // End of utterance — stop recorder (triggers onstop → upload) this.isSpeaking = false; this.silenceSince = null; - this._finaliseUtterance(); + this._finaliseUtterance(true); } } } @@ -258,12 +281,31 @@ class AudioStreamService { } } - /** Stop the current recorder to flush the utterance, then restart it. */ - private _finaliseUtterance(): void { + private _cleanupAfterStop(clearChunks: boolean): void { + const pendingResolver = this.finishResolver; + this.isActive = false; + this.finishResolver = null; + this.mediaRecorder = null; + this._teardownAudioContext(); + this._releaseStream(); + if (clearChunks) { + this.chunks = []; + } + this.isSpeaking = false; + this.silenceSince = null; + this.onResult = null; + this.onError = null; + pendingResolver?.(); + } + + /** Stop the current recorder to flush the utterance, optionally restarting for the next one. */ + private _finaliseUtterance(restart: boolean): void { if (!this.isActive) return; if (this.mediaRecorder && this.mediaRecorder.state === 'recording') { this.mediaRecorder.stop(); } + if (!restart) return; + // Restart recorder for the next utterance after a short delay // (MediaRecorder.onstop fires asynchronously; we wait for it to complete) setTimeout(() => { diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index f65e1d9b..68e32ea6 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -510,6 +510,7 @@ interface ConfigStore { sttLanguage: string; sttSilenceThresholdDb: number; sttSilenceHoldMs: number; + sttTranscribeOnStop: boolean; showMessageTTSButtons: boolean; voiceModeEnabled: boolean; // Summarization settings @@ -533,6 +534,7 @@ interface ConfigStore { setSttLanguage: (lang: string) => void; setSttSilenceThresholdDb: (db: number) => void; setSttSilenceHoldMs: (ms: number) => void; + setSttTranscribeOnStop: (enabled: boolean) => void; setShowMessageTTSButtons: (show: boolean) => void; setVoiceModeEnabled: (enabled: boolean) => void; setSummarizeMessageTTS: (enabled: boolean) => void; @@ -760,6 +762,13 @@ export const useConfigStore = create()( } return 1500; })(), + sttTranscribeOnStop: (() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('sttTranscribeOnStop'); + if (saved === 'true') return true; + } + return false; + })(), // Show TTS buttons on messages - disabled by default until user enables it showMessageTTSButtons: (() => { if (typeof window !== 'undefined') { @@ -1865,6 +1874,13 @@ export const useConfigStore = create()( } }, + setSttTranscribeOnStop: (enabled: boolean) => { + set({ sttTranscribeOnStop: enabled }); + if (typeof window !== 'undefined') { + localStorage.setItem('sttTranscribeOnStop', String(enabled)); + } + }, + setShowMessageTTSButtons: (show: boolean) => { set({ showMessageTTSButtons: show }); if (typeof window !== 'undefined') {