feat(stage): track speech mute toggles

This commit is contained in:
RainbowBird
2026-07-28 18:05:45 +08:00
parent 5ea1dd69e9
commit a1c02f58f3
7 changed files with 82 additions and 13 deletions
@@ -32,7 +32,7 @@ const { t } = useI18n()
:title="speechMuted ? t('stage.speech-output.unmute') : t('stage.speech-output.mute')"
:aria-label="speechMuted ? t('stage.speech-output.unmute') : t('stage.speech-output.mute')"
:aria-pressed="speechMuted"
@click="toggleSpeechMuted"
@click="toggleSpeechMuted('window_title_bar')"
>
<div v-if="speechMuted" class="i-solar:volume-cross-bold-duotone" />
<div v-else class="i-solar:volume-loud-bold-duotone" />
@@ -239,7 +239,7 @@ onMounted(() => {
:title="speechMuted ? t('stage.speech-output.unmute') : t('stage.speech-output.mute')"
:aria-label="speechMuted ? t('stage.speech-output.unmute') : t('stage.speech-output.mute')"
:aria-pressed="speechMuted"
@click="toggleSpeechMuted"
@click="toggleSpeechMuted('mobile_action_rail')"
>
<div v-if="speechMuted" class="i-solar:volume-cross-bold-duotone size-5" />
<div v-else class="i-solar:volume-loud-bold-duotone size-5" />
@@ -46,7 +46,7 @@ function handleCleanupMessages() {
:title="speechMuted ? t('stage.speech-output.unmute') : t('stage.speech-output.mute')"
:aria-label="speechMuted ? t('stage.speech-output.unmute') : t('stage.speech-output.mute')"
:aria-pressed="speechMuted"
@click="toggleSpeechMuted"
@click="toggleSpeechMuted('chat_toolbar')"
>
<div v-if="speechMuted" class="i-solar:volume-cross-bold-duotone" />
<div v-else class="i-solar:volume-loud-bold-duotone" />
@@ -6,7 +6,8 @@ import { useStopSpeakingButton } from './useStopSpeakingButton'
const nowSpeaking = ref(false)
const speechMuted = ref(false)
const requestStopSpeakingMock = vi.fn()
const toggleSpeechMutedMock = vi.fn()
const setSpeechMutedMock = vi.fn()
const trackSpeechMuteToggledMock = vi.fn()
const trackTtsStopClickedMock = vi.fn()
vi.mock('@proj-airi/stage-ui/stores/audio', () => ({
@@ -18,13 +19,14 @@ vi.mock('@proj-airi/stage-ui/stores/audio', () => ({
vi.mock('@proj-airi/stage-ui/stores/speech-output-control', () => ({
useSpeechOutputControlStore: () => ({
requestStopSpeaking: requestStopSpeakingMock,
setSpeechMuted: setSpeechMutedMock,
speechMuted,
toggleSpeechMuted: toggleSpeechMutedMock,
}),
}))
vi.mock('@proj-airi/stage-ui/composables/use-analytics', () => ({
useAnalytics: () => ({
trackSpeechMuteToggled: trackSpeechMuteToggledMock,
trackTtsStopClicked: trackTtsStopClickedMock,
}),
}))
@@ -74,16 +76,33 @@ describe('useStopSpeakingButton', () => {
})
})
it('exposes persisted mute state and toggles it through the shared output store', () => {
speechMuted.value = true
toggleSpeechMutedMock.mockClear()
it('tracks mute and unmute with the entry surface and active playback state', () => {
speechMuted.value = false
nowSpeaking.value = true
setSpeechMutedMock.mockClear()
trackSpeechMuteToggledMock.mockClear()
const controls = useStopSpeakingButton()
expect(controls.speechMuted.value).toBe(true)
controls.toggleSpeechMuted('chat_toolbar')
controls.toggleSpeechMuted()
expect(setSpeechMutedMock).toHaveBeenCalledWith(true)
expect(trackSpeechMuteToggledMock).toHaveBeenCalledWith({
muted: true,
source: 'chat_toolbar',
was_speaking: true,
})
expect(toggleSpeechMutedMock).toHaveBeenCalledOnce()
speechMuted.value = true
nowSpeaking.value = false
controls.toggleSpeechMuted('window_title_bar')
expect(setSpeechMutedMock).toHaveBeenLastCalledWith(false)
expect(trackSpeechMuteToggledMock).toHaveBeenLastCalledWith({
muted: false,
source: 'window_title_bar',
was_speaking: false,
})
})
})
@@ -1,3 +1,5 @@
import type { SpeechMuteAnalyticsSource } from '@proj-airi/stage-ui/composables/use-analytics'
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
import { useSpeakingStore } from '@proj-airi/stage-ui/stores/audio'
import { useSpeechOutputControlStore } from '@proj-airi/stage-ui/stores/speech-output-control'
@@ -14,7 +16,7 @@ export function useStopSpeakingButton() {
const { nowSpeaking } = storeToRefs(useSpeakingStore())
const speechOutputControlStore = useSpeechOutputControlStore()
const { speechMuted } = storeToRefs(speechOutputControlStore)
const { trackTtsStopClicked } = useAnalytics()
const { trackSpeechMuteToggled, trackTtsStopClicked } = useAnalytics()
const showStopSpeakingButton = computed(() => nowSpeaking.value)
@@ -28,11 +30,23 @@ export function useStopSpeakingButton() {
speechOutputControlStore.requestStopSpeaking('manual-all')
}
function toggleSpeechMuted(source: SpeechMuteAnalyticsSource) {
const muted = !speechMuted.value
const wasSpeaking = nowSpeaking.value
speechOutputControlStore.setSpeechMuted(muted)
trackSpeechMuteToggled({
muted,
source,
was_speaking: wasSpeaking,
})
}
return {
showStopSpeakingButton,
speechMuted,
stopSpeakingFromChat,
stopAllSpeaking,
toggleSpeechMuted: speechOutputControlStore.toggleSpeechMuted,
toggleSpeechMuted,
}
}
@@ -83,6 +83,23 @@ describe('useAnalytics conversation product events', () => {
})
})
it('captures speech mute state changes without conversation or audio content', () => {
const analytics = useAnalytics()
analytics.trackSpeechMuteToggled({
muted: true,
was_speaking: true,
source: 'chat_toolbar',
})
expect(analyticsMocks.posthogCaptureMock).toHaveBeenCalledWith('speech_mute_toggled', {
app_surface: 'web',
muted: true,
source: 'chat_toolbar',
was_speaking: true,
})
})
it('captures custom-provider token usage without prompt or response content', () => {
const analytics = useAnalytics()
@@ -22,6 +22,11 @@ export type ConversationAnalyticsSurface = 'web' | 'mobile' | 'electron'
*/
export type ConversationAnalyticsSource = 'chat_controls' | 'history' | 'sessions_drawer'
/**
* Stable UI entry points for the persistent speech mute control.
*/
export type SpeechMuteAnalyticsSource = 'chat_toolbar' | 'mobile_action_rail' | 'window_title_bar'
export type ProviderMode = 'official' | 'custom' | 'unknown'
export type ChatActivationFailureStage = 'provider_config' | 'model_list' | 'message_send' | 'llm_response' | 'tts'
export type ProviderConfigStep = 'settings_auto_validate' | 'manual_chat_ping' | 'onboarding_validate'
@@ -654,6 +659,19 @@ export function useAnalytics() {
})
}
function trackSpeechMuteToggled(properties: {
muted: boolean
was_speaking: boolean
source: SpeechMuteAnalyticsSource
}) {
if (!canCapture())
return
posthog.capture('speech_mute_toggled', {
...properties,
app_surface: getConversationAnalyticsSurface(),
})
}
function trackChatSessionSelected(properties: { source: 'sessions_drawer', message_count: number, cloud_synced: boolean }) {
if (!canCapture())
return
@@ -1305,6 +1323,7 @@ export function useAnalytics() {
trackProviderConfigCompleted,
trackOfficialProviderEnabled,
trackTtsStopClicked,
trackSpeechMuteToggled,
trackChatSessionSelected,
trackChatMessageDeleted,
trackChatMessagesCleared,