@@ -33,6 +33,8 @@ public sealed partial class ChatPage : Page
3333 private bool _webViewInitialized ;
3434 private bool _webViewMode ;
3535 private bool _pageActive ;
36+ private readonly SemaphoreSlim _speakerMuteGate = new ( 1 , 1 ) ;
37+ private int _voiceSettingsDialogOpen ;
3638 private bool _navigationStarted ;
3739 private CancellationTokenSource ? _navigationCts ;
3840 private global ::Windows . Foundation . TypedEventHandler < CoreWebView2 , CoreWebView2NavigationCompletedEventArgs > ? _navCompletedHandler ;
@@ -232,7 +234,7 @@ private void ShowFunctionalSurface()
232234
233235 var app = App . Current as App ;
234236 var provider = ResolveChatProvider ( app ) ;
235- Func < string , Task > ? readAloud = app is null ? null : app . SpeakChatTextAsync ;
237+ Func < string , Task > ? readAloud = app is null ? null : ReadChatTextAloudAsync ;
236238
237239 // Consume a pending session-key hand-off from SessionsPage or a
238240 // notification toast so the chat root mounts with that thread selected.
@@ -292,8 +294,8 @@ private void ShowFunctionalSurface()
292294 onVoiceRequest : VoiceTranscribeAsync ,
293295 onAttachClick : OnAttachClicked ,
294296 onSettingsClick : ( ) => _hub ? . NavigateTo ( "voice" ) ,
295- onSpeakerMuteChanged : muted => ( App . Current as App ) ? . SetChatSpeakerMuted ( muted ) ,
296- initialMuted : CurrentApp . Settings ? . VoiceTtsEnabled == false ,
297+ onSpeakerMuteChanged : muted => _ = OnSpeakerMuteChangedAsync ( muted ) ,
298+ initialMuted : ShouldStartSpeakerMuted ( CurrentApp . Settings ) ,
297299 suppressAutoDispose : true ) ;
298300 _mountedProvider = provider ;
299301 _mountedThreadId = threadIdToMount ;
@@ -829,7 +831,8 @@ private void OnRetryChat(object sender, RoutedEventArgs e)
829831 await ShowVoiceSettingsDialogAsync (
830832 LocalizationHelper . GetString ( "ChatVoiceDialog_InputOffTitle" ) ,
831833 LocalizationHelper . GetString ( "ChatVoiceDialog_InputOffMessage" ) ,
832- NavigateToVoiceSettings ) ;
834+ LocalizationHelper . GetString ( "ChatVoiceDialog_OpenPermissionsSettings" ) ,
835+ NavigateToPermissionsSettings ) ;
833836 return null ;
834837 }
835838
@@ -840,7 +843,8 @@ await ShowVoiceSettingsDialogAsync(
840843 await ShowVoiceSettingsDialogAsync (
841844 LocalizationHelper . GetString ( "ChatVoiceDialog_InputOffTitle" ) ,
842845 LocalizationHelper . GetString ( "ChatVoiceDialog_InputOffMessage" ) ,
843- NavigateToVoiceSettings ) ;
846+ LocalizationHelper . GetString ( "ChatVoiceDialog_OpenPermissionsSettings" ) ,
847+ NavigateToPermissionsSettings ) ;
844848 return null ;
845849 }
846850
@@ -850,6 +854,7 @@ await ShowVoiceSettingsDialogAsync(
850854 await ShowVoiceSettingsDialogAsync (
851855 LocalizationHelper . GetString ( "ChatVoiceDialog_ModelRequiredTitle" ) ,
852856 LocalizationHelper . GetString ( "ChatVoiceDialog_ModelRequiredMessage" ) ,
857+ LocalizationHelper . GetString ( "ChatVoiceDialog_OpenVoiceSettings" ) ,
853858 NavigateToVoiceSettings ) ;
854859 return null ;
855860 }
@@ -880,8 +885,80 @@ await ShowVoiceSettingsDialogAsync(
880885 }
881886 }
882887
883- private async Task ShowVoiceSettingsDialogAsync ( string title , string message , Action openVoiceSettings )
888+ private async Task ReadChatTextAloudAsync ( string text )
889+ {
890+ if ( ! await EnsureTtsReadyForChatAsync ( ) )
891+ return ;
892+
893+ await CurrentApp . SpeakChatTextAsync ( text ) ;
894+ }
895+
896+ private async Task OnSpeakerMuteChangedAsync ( bool muted )
897+ {
898+ if ( ! await _speakerMuteGate . WaitAsync ( 0 ) )
899+ return ;
900+
901+ try
902+ {
903+ if ( muted )
904+ {
905+ ( App . Current as App ) ? . SetChatSpeakerMuted ( true ) ;
906+ return ;
907+ }
908+
909+ if ( IsTtsReadyForChat ( ) )
910+ {
911+ ( App . Current as App ) ? . SetChatSpeakerMuted ( false ) ;
912+ return ;
913+ }
914+
915+ ( App . Current as App ) ? . SetChatSpeakerMuted ( true ) ;
916+ _functionalHost ? . SetSpeakerMuted ( true ) ;
917+ await ShowTtsUnavailableDialogAsync ( ) ;
918+ }
919+ catch ( Exception ex )
920+ {
921+ Logger . Warn ( $ "Speaker mute change failed: { ex . Message } ") ;
922+ }
923+ finally
924+ {
925+ _speakerMuteGate . Release ( ) ;
926+ }
927+ }
928+
929+ private async Task < bool > EnsureTtsReadyForChatAsync ( )
930+ {
931+ if ( IsTtsReadyForChat ( ) )
932+ return true ;
933+
934+ await ShowTtsUnavailableDialogAsync ( ) ;
935+ return false ;
936+ }
937+
938+ private static bool IsTtsReadyForChat ( )
939+ {
940+ return SpeechSetupReadiness . IsChatTtsPlaybackReady ( CurrentApp . Settings ) ;
941+ }
942+
943+ private async Task ShowTtsUnavailableDialogAsync ( )
944+ {
945+ await ShowVoiceSettingsDialogAsync (
946+ LocalizationHelper . GetString ( "ChatVoiceDialog_OutputOffTitle" ) ,
947+ LocalizationHelper . GetString ( "ChatVoiceDialog_OutputOffMessage" ) ,
948+ LocalizationHelper . GetString ( "ChatVoiceDialog_OpenPermissionsSettings" ) ,
949+ NavigateToPermissionsSettings ) ;
950+ }
951+
952+ private static bool ShouldStartSpeakerMuted ( SettingsManager ? settings )
953+ {
954+ return ! SpeechSetupReadiness . IsAutomaticChatTtsEnabled ( settings ) ;
955+ }
956+
957+ private async Task ShowVoiceSettingsDialogAsync ( string title , string message , string primaryButtonText , Action openSettings )
884958 {
959+ if ( Interlocked . Exchange ( ref _voiceSettingsDialogOpen , 1 ) == 1 )
960+ return ;
961+
885962 var tcs = new TaskCompletionSource ( ) ;
886963 if ( DispatcherQueue is null || ! DispatcherQueue . TryEnqueue ( async ( ) =>
887964 {
@@ -891,7 +968,7 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac
891968 {
892969 Title = title ,
893970 Content = message ,
894- PrimaryButtonText = LocalizationHelper . GetString ( "ChatVoiceDialog_OpenVoiceSettings" ) ,
971+ PrimaryButtonText = primaryButtonText ,
895972 CloseButtonText = LocalizationHelper . GetString ( "ChatVoiceDialog_Dismiss" ) ,
896973 DefaultButton = ContentDialogButton . Primary ,
897974 XamlRoot = Content ? . XamlRoot
@@ -912,7 +989,7 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac
912989 } ;
913990
914991 if ( await dialog . ShowAsync ( ) == ContentDialogResult . Primary )
915- openVoiceSettings ( ) ;
992+ openSettings ( ) ;
916993 }
917994 catch ( InvalidOperationException ex )
918995 {
@@ -924,10 +1001,18 @@ private async Task ShowVoiceSettingsDialogAsync(string title, string message, Ac
9241001 }
9251002 } ) )
9261003 {
1004+ Interlocked . Exchange ( ref _voiceSettingsDialogOpen , 0 ) ;
9271005 return ;
9281006 }
9291007
930- await tcs . Task ;
1008+ try
1009+ {
1010+ await tcs . Task ;
1011+ }
1012+ finally
1013+ {
1014+ Interlocked . Exchange ( ref _voiceSettingsDialogOpen , 0 ) ;
1015+ }
9311016 }
9321017
9331018 private void NavigateToVoiceSettings ( )
@@ -938,6 +1023,14 @@ private void NavigateToVoiceSettings()
9381023 ( App . Current as App ) ? . ShowHub ( "voice" ) ;
9391024 }
9401025
1026+ private void NavigateToPermissionsSettings ( )
1027+ {
1028+ if ( _hub is not null )
1029+ _hub . NavigateTo ( "permissions" ) ;
1030+ else
1031+ ( App . Current as App ) ? . ShowHub ( "permissions" ) ;
1032+ }
1033+
9411034 private void OnAttachClicked ( )
9421035 {
9431036 Logger . Info ( "[ChatPage] OnAttachClicked invoked" ) ;
0 commit comments