mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 00:48:06 +00:00
feat(stage): track speech mute toggles
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user