fix: voice status global store, per-view mic reactivity, single-voice preview

- Move voice status into settings store (voiceSttReady, voiceTtsReady),
  checked once at login and refreshed after admin model reload
- ChatView, BriefingView, DashboardChatInput now use computed refs from
  the store — mic buttons appear reactively without needing a page reload
- BriefingView: separate STT-only guard for mic PTT vs TTS-only guard for
  listen mode / speak buttons
- Add ▶ Preview button to Voice & Speed section in Settings for single-
  voice testing without enabling blend mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 20:53:06 -04:00
parent b4b4b0d9d6
commit 71ca0ecb5c
7 changed files with 70 additions and 37 deletions
+9 -15
View File
@@ -4,6 +4,7 @@ import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
import { useVoiceRecorder } from '@/composables/useVoiceRecorder'
import { useVoiceAudio } from '@/composables/useVoiceAudio'
import { useChatStore } from '@/stores/chat'
import { useSettingsStore } from '@/stores/settings'
import ChatMessage from '@/components/ChatMessage.vue'
import WeatherCard from '@/components/WeatherCard.vue'
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
@@ -17,7 +18,6 @@ import {
postRssReaction,
deleteRssReaction,
getNewsItems,
getVoiceStatus,
transcribeAudio,
synthesiseSpeech,
type BriefingConversation,
@@ -39,6 +39,7 @@ interface WeatherData {
}
const chatStore = useChatStore()
const settingsStore = useSettingsStore()
// Setup wizard
const showWizard = ref(false)
@@ -230,7 +231,9 @@ function convLabel(c: BriefingConversation): string {
}
// ─── Voice ────────────────────────────────────────────────────────────────────
const voiceEnabled = ref(false)
// Mic PTT needs STT; listen/speak mode also needs TTS
const voiceEnabled = computed(() => settingsStore.voiceSttReady)
const voiceTtsEnabled = computed(() => settingsStore.voiceTtsReady)
const listenMode = ref(false)
const transcribing = ref(false)
const synthesising = ref(false)
@@ -238,14 +241,6 @@ const synthesising = ref(false)
const recorder = useVoiceRecorder()
const audio = useVoiceAudio()
// Check voice availability once on mount
async function checkVoice() {
try {
const status = await getVoiceStatus()
voiceEnabled.value = status.enabled && status.stt && status.tts
} catch { /* voice feature absent */ }
}
// Read the latest assistant message aloud
async function listenToLatest() {
const lastAssistant = [...messages.value].reverse().find((m) => m.role === 'assistant')
@@ -254,7 +249,7 @@ async function listenToLatest() {
}
async function speakText(text: string) {
if (!voiceEnabled.value || synthesising.value) return
if (!voiceTtsEnabled.value || synthesising.value) return
// Strip markdown for cleaner TTS output
const plain = text
.replace(/```[\s\S]*?```/g, '')
@@ -304,7 +299,7 @@ async function stopPtt() {
// Auto-TTS when listen mode is on and streaming ends
watch(() => chatStore.streaming, async (streaming) => {
if (!streaming && listenMode.value && voiceEnabled.value) {
if (!streaming && listenMode.value && voiceTtsEnabled.value) {
// Small delay to let messages update after stream
await new Promise((r) => setTimeout(r, 200))
await listenToLatest()
@@ -348,7 +343,6 @@ useBackgroundRefresh(
onMounted(async () => {
await checkSetup()
if (!showWizard.value) await loadAll()
checkVoice()
})
</script>
@@ -430,7 +424,7 @@ onMounted(async () => {
<div v-if="isToday" class="briefing-input-bar">
<!-- Listen toggle -->
<button
v-if="voiceEnabled"
v-if="voiceTtsEnabled"
class="btn-icon"
:class="{ 'btn-icon-active': listenMode, 'btn-icon-busy': synthesising || audio.playing.value }"
@click="listenMode ? (listenMode = false) : (listenMode = true, listenToLatest())"
@@ -446,7 +440,7 @@ onMounted(async () => {
</button>
<!-- Stop TTS playback -->
<button
v-if="voiceEnabled && (synthesising || audio.playing.value)"
v-if="voiceTtsEnabled && (synthesising || audio.playing.value)"
class="btn-icon btn-icon-danger"
@click="audio.stop(); synthesising = false"
title="Stop playback"