From c81a499e6e4ca0aa068928314a91fef6d8224ebe Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 3 Apr 2026 14:18:34 -0400 Subject: [PATCH] fix(tts): add speak() for complete responses; fix briefing message commit race --- frontend/src/composables/useStreamingTts.ts | 17 ++++++++++++++++- frontend/src/views/BriefingView.vue | 13 ++++++++----- frontend/src/views/ChatView.vue | 2 +- frontend/src/views/WorkspaceView.vue | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/frontend/src/composables/useStreamingTts.ts b/frontend/src/composables/useStreamingTts.ts index f813caa..dda7084 100644 --- a/frontend/src/composables/useStreamingTts.ts +++ b/frontend/src/composables/useStreamingTts.ts @@ -52,6 +52,8 @@ export interface UseStreamingTtsReturn { speaking: ComputedRef /** Cancel all in-flight synthesis/playback and clear the queue. */ stop: () => void + /** Speak a complete text through the same sentence-chunking pipeline. */ + speak: (text: string) => void } export function useStreamingTts(options: UseStreamingTtsOptions): UseStreamingTtsReturn { @@ -139,5 +141,18 @@ export function useStreamingTts(options: UseStreamingTtsOptions): UseStreamingTt } }) - return { speaking, stop } + function speak(text: string): void { + stop() + if (!text.trim()) return + const myAbortId = abortId + const { sentences, remainder } = extractSentences(text) + for (const sentence of sentences) { + enqueueSentence(sentence, myAbortId) + } + if (remainder.trim().length >= MIN_CHARS) { + enqueueSentence(remainder.trim(), myAbortId) + } + } + + return { speaking, stop, speak } } diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue index 661a5ec..89b50c9 100644 --- a/frontend/src/views/BriefingView.vue +++ b/frontend/src/views/BriefingView.vue @@ -148,11 +148,14 @@ watch(selectedConvId, async (id) => { } }) -// Refresh messages after streaming ends +// Refresh messages after streaming ends — read from store directly (already updated before streaming=false) watch(() => chatStore.streaming, async (streaming) => { - if (!streaming && _mounted && selectedConvId.value === todayConvId.value && todayConvId.value) { - const today = await getBriefingToday().catch(() => null) - if (_mounted && today) messages.value = today.messages + if (!streaming && _mounted && selectedConvId.value === todayConvId.value) { + const storeMessages = chatStore.currentConversation?.messages + if (_mounted && storeMessages?.length) { + messages.value = storeMessages as unknown as BriefingMessage[] + } + await nextTick() if (_mounted) scrollToBottom() } }) @@ -420,7 +423,7 @@ onMounted(async () => { v-if="voiceTtsEnabled" class="btn-icon" :class="{ 'btn-icon-active': listenMode, 'btn-icon-busy': tts.speaking.value }" - @click="listenMode = !listenMode; if (!listenMode) tts.stop()" + @click="listenMode = !listenMode; if (listenMode) tts.speak([...messages].reverse().find(m => m.role === 'assistant')?.content ?? ''); else tts.stop()" :title="listenMode ? 'Stop auto-read' : 'Read replies aloud'" aria-label="Toggle listen mode" > diff --git a/frontend/src/views/ChatView.vue b/frontend/src/views/ChatView.vue index 1e6dba7..6f21ed2 100644 --- a/frontend/src/views/ChatView.vue +++ b/frontend/src/views/ChatView.vue @@ -892,7 +892,7 @@ onUnmounted(() => {