mcp-voice-hooks 1.0.8 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -7,51 +7,72 @@ class VoiceHooksClient {
7
7
  this.refreshBtn = document.getElementById('refreshBtn');
8
8
  this.clearAllBtn = document.getElementById('clearAllBtn');
9
9
  this.utterancesList = document.getElementById('utterancesList');
10
+ this.infoMessage = document.getElementById('infoMessage');
10
11
  this.totalCount = document.getElementById('totalCount');
11
12
  this.pendingCount = document.getElementById('pendingCount');
12
13
  this.deliveredCount = document.getElementById('deliveredCount');
13
-
14
+
14
15
  // Voice controls
15
16
  this.listenBtn = document.getElementById('listenBtn');
16
17
  this.listenBtnText = document.getElementById('listenBtnText');
17
18
  this.listeningIndicator = document.getElementById('listeningIndicator');
18
19
  this.interimText = document.getElementById('interimText');
19
-
20
+
20
21
  // Speech recognition
21
22
  this.recognition = null;
22
23
  this.isListening = false;
23
24
  this.initializeSpeechRecognition();
24
-
25
+
26
+ // Speech synthesis
27
+ this.initializeSpeechSynthesis();
28
+
29
+ // Server-Sent Events for TTS
30
+ this.initializeTTSEvents();
31
+
32
+ // TTS controls
33
+ this.voiceSelect = document.getElementById('voiceSelect');
34
+ this.speechRateSlider = document.getElementById('speechRate');
35
+ this.speechRateInput = document.getElementById('speechRateInput');
36
+ this.testTTSBtn = document.getElementById('testTTSBtn');
37
+ this.voiceResponsesToggle = document.getElementById('voiceResponsesToggle');
38
+ this.voiceOptions = document.getElementById('voiceOptions');
39
+ this.localVoicesGroup = document.getElementById('localVoicesGroup');
40
+ this.cloudVoicesGroup = document.getElementById('cloudVoicesGroup');
41
+ this.rateWarning = document.getElementById('rateWarning');
42
+ this.systemVoiceInfo = document.getElementById('systemVoiceInfo');
43
+
44
+ // Load saved preferences
45
+ this.loadPreferences();
46
+
25
47
  this.setupEventListeners();
26
48
  this.loadData();
27
-
49
+
28
50
  // Auto-refresh every 2 seconds
29
51
  setInterval(() => this.loadData(), 2000);
30
52
  }
31
-
53
+
32
54
  initializeSpeechRecognition() {
33
55
  // Check for browser support
34
56
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
35
-
57
+
36
58
  if (!SpeechRecognition) {
37
59
  console.error('Speech recognition not supported in this browser');
38
60
  this.listenBtn.disabled = true;
39
61
  this.listenBtnText.textContent = 'Not Supported';
40
62
  return;
41
63
  }
42
-
64
+
43
65
  this.recognition = new SpeechRecognition();
44
66
  this.recognition.continuous = true;
45
67
  this.recognition.interimResults = true;
46
- this.recognition.lang = 'en-US';
47
-
68
+
48
69
  // Handle results
49
70
  this.recognition.onresult = (event) => {
50
71
  let interimTranscript = '';
51
-
72
+
52
73
  for (let i = event.resultIndex; i < event.results.length; i++) {
53
74
  const transcript = event.results[i][0].transcript;
54
-
75
+
55
76
  if (event.results[i].isFinal) {
56
77
  // User paused - send as complete utterance
57
78
  this.sendVoiceUtterance(transcript);
@@ -63,31 +84,31 @@ class VoiceHooksClient {
63
84
  interimTranscript += transcript;
64
85
  }
65
86
  }
66
-
87
+
67
88
  if (interimTranscript) {
68
89
  this.interimText.textContent = interimTranscript;
69
90
  this.interimText.classList.add('active');
70
91
  }
71
92
  };
72
-
93
+
73
94
  // Handle errors
74
95
  this.recognition.onerror = (event) => {
75
96
  console.error('Speech recognition error:', event.error);
76
-
97
+
77
98
  if (event.error === 'no-speech') {
78
99
  // Continue listening
79
100
  return;
80
101
  }
81
-
102
+
82
103
  if (event.error === 'not-allowed') {
83
104
  alert('Microphone access denied. Please allow microphone access to use voice input.');
84
105
  } else {
85
106
  alert(`Speech recognition error: ${event.error}`);
86
107
  }
87
-
108
+
88
109
  this.stopListening();
89
110
  };
90
-
111
+
91
112
  // Handle end
92
113
  this.recognition.onend = () => {
93
114
  if (this.isListening) {
@@ -101,27 +122,67 @@ class VoiceHooksClient {
101
122
  }
102
123
  };
103
124
  }
104
-
125
+
105
126
  setupEventListeners() {
106
127
  this.sendBtn.addEventListener('click', () => this.sendUtterance());
107
128
  this.refreshBtn.addEventListener('click', () => this.loadData());
108
129
  this.clearAllBtn.addEventListener('click', () => this.clearAllUtterances());
109
130
  this.listenBtn.addEventListener('click', () => this.toggleListening());
110
-
131
+
111
132
  this.utteranceInput.addEventListener('keypress', (e) => {
112
133
  if (e.key === 'Enter') {
113
134
  this.sendUtterance();
114
135
  }
115
136
  });
137
+
138
+ // TTS controls
139
+ this.voiceSelect.addEventListener('change', (e) => {
140
+ this.selectedVoice = e.target.value;
141
+ // Save selected voice to localStorage
142
+ localStorage.setItem('selectedVoice', this.selectedVoice);
143
+ this.updateVoicePreferences();
144
+ this.updateVoiceWarnings();
145
+ });
146
+
147
+ this.speechRateSlider.addEventListener('input', (e) => {
148
+ this.speechRate = parseFloat(e.target.value);
149
+ this.speechRateInput.value = this.speechRate.toFixed(1);
150
+ // Save rate to localStorage
151
+ localStorage.setItem('speechRate', this.speechRate.toString());
152
+ });
153
+
154
+ this.speechRateInput.addEventListener('input', (e) => {
155
+ let value = parseFloat(e.target.value);
156
+ if (!isNaN(value)) {
157
+ value = Math.max(0.5, Math.min(5, value)); // Clamp to valid range
158
+ this.speechRate = value;
159
+ this.speechRateSlider.value = value.toString();
160
+ this.speechRateInput.value = value.toFixed(1);
161
+ // Save rate to localStorage
162
+ localStorage.setItem('speechRate', this.speechRate.toString());
163
+ }
164
+ });
165
+
166
+ this.testTTSBtn.addEventListener('click', () => {
167
+ this.speakText('Hello! This is a test of the text-to-speech voice.');
168
+ });
169
+
170
+ // Voice toggle listeners
171
+ this.voiceResponsesToggle.addEventListener('change', (e) => {
172
+ const enabled = e.target.checked;
173
+ localStorage.setItem('voiceResponsesEnabled', enabled);
174
+ this.updateVoicePreferences();
175
+ this.updateVoiceOptionsVisibility();
176
+ });
116
177
  }
117
-
178
+
118
179
  async sendUtterance() {
119
180
  const text = this.utteranceInput.value.trim();
120
181
  if (!text) return;
121
-
182
+
122
183
  this.sendBtn.disabled = true;
123
184
  this.sendBtn.textContent = 'Sending...';
124
-
185
+
125
186
  try {
126
187
  const response = await fetch(`${this.baseUrl}/api/potential-utterances`, {
127
188
  method: 'POST',
@@ -133,7 +194,7 @@ class VoiceHooksClient {
133
194
  timestamp: new Date().toISOString()
134
195
  }),
135
196
  });
136
-
197
+
137
198
  if (response.ok) {
138
199
  this.utteranceInput.value = '';
139
200
  this.loadData(); // Refresh the list
@@ -149,7 +210,7 @@ class VoiceHooksClient {
149
210
  this.sendBtn.textContent = 'Send';
150
211
  }
151
212
  }
152
-
213
+
153
214
  async loadData() {
154
215
  try {
155
216
  // Load status
@@ -158,7 +219,7 @@ class VoiceHooksClient {
158
219
  const status = await statusResponse.json();
159
220
  this.updateStatus(status);
160
221
  }
161
-
222
+
162
223
  // Load utterances
163
224
  const utterancesResponse = await fetch(`${this.baseUrl}/api/utterances?limit=20`);
164
225
  if (utterancesResponse.ok) {
@@ -169,19 +230,30 @@ class VoiceHooksClient {
169
230
  console.error('Failed to load data:', error);
170
231
  }
171
232
  }
172
-
233
+
173
234
  updateStatus(status) {
174
235
  this.totalCount.textContent = status.total;
175
236
  this.pendingCount.textContent = status.pending;
176
237
  this.deliveredCount.textContent = status.delivered;
177
238
  }
178
-
239
+
179
240
  updateUtterancesList(utterances) {
180
241
  if (utterances.length === 0) {
181
242
  this.utterancesList.innerHTML = '<div class="empty-state">No utterances yet. Type something above to get started!</div>';
243
+ this.infoMessage.style.display = 'none';
182
244
  return;
183
245
  }
184
-
246
+
247
+ // Check if all messages are pending
248
+ const allPending = utterances.every(u => u.status === 'pending');
249
+ if (allPending) {
250
+ // Show info message but don't replace the utterances list
251
+ this.infoMessage.style.display = 'block';
252
+ } else {
253
+ // Hide info message when at least one utterance is delivered
254
+ this.infoMessage.style.display = 'none';
255
+ }
256
+
185
257
  this.utterancesList.innerHTML = utterances.map(utterance => `
186
258
  <div class="utterance-item">
187
259
  <div class="utterance-text">${this.escapeHtml(utterance.text)}</div>
@@ -194,18 +266,18 @@ class VoiceHooksClient {
194
266
  </div>
195
267
  `).join('');
196
268
  }
197
-
269
+
198
270
  formatTimestamp(timestamp) {
199
271
  const date = new Date(timestamp);
200
272
  return date.toLocaleTimeString();
201
273
  }
202
-
274
+
203
275
  escapeHtml(text) {
204
276
  const div = document.createElement('div');
205
277
  div.textContent = text;
206
278
  return div.innerHTML;
207
279
  }
208
-
280
+
209
281
  toggleListening() {
210
282
  if (this.isListening) {
211
283
  this.stopListening();
@@ -213,13 +285,13 @@ class VoiceHooksClient {
213
285
  this.startListening();
214
286
  }
215
287
  }
216
-
217
- startListening() {
288
+
289
+ async startListening() {
218
290
  if (!this.recognition) {
219
291
  alert('Speech recognition not supported in this browser');
220
292
  return;
221
293
  }
222
-
294
+
223
295
  try {
224
296
  this.recognition.start();
225
297
  this.isListening = true;
@@ -227,13 +299,16 @@ class VoiceHooksClient {
227
299
  this.listenBtnText.textContent = 'Stop Listening';
228
300
  this.listeningIndicator.classList.add('active');
229
301
  this.debugLog('Started listening');
302
+
303
+ // Notify server that voice input is active
304
+ await this.updateVoiceInputState(true);
230
305
  } catch (e) {
231
306
  console.error('Failed to start recognition:', e);
232
307
  alert('Failed to start speech recognition. Please try again.');
233
308
  }
234
309
  }
235
-
236
- stopListening() {
310
+
311
+ async stopListening() {
237
312
  if (this.recognition) {
238
313
  this.isListening = false;
239
314
  this.recognition.stop();
@@ -243,15 +318,18 @@ class VoiceHooksClient {
243
318
  this.interimText.textContent = '';
244
319
  this.interimText.classList.remove('active');
245
320
  this.debugLog('Stopped listening');
321
+
322
+ // Notify server that voice input is no longer active
323
+ await this.updateVoiceInputState(false);
246
324
  }
247
325
  }
248
-
326
+
249
327
  async sendVoiceUtterance(text) {
250
328
  const trimmedText = text.trim();
251
329
  if (!trimmedText) return;
252
-
330
+
253
331
  this.debugLog('Sending voice utterance:', trimmedText);
254
-
332
+
255
333
  try {
256
334
  const response = await fetch(`${this.baseUrl}/api/potential-utterances`, {
257
335
  method: 'POST',
@@ -263,7 +341,7 @@ class VoiceHooksClient {
263
341
  timestamp: new Date().toISOString()
264
342
  }),
265
343
  });
266
-
344
+
267
345
  if (response.ok) {
268
346
  this.loadData(); // Refresh the list
269
347
  } else {
@@ -274,12 +352,12 @@ class VoiceHooksClient {
274
352
  console.error('Failed to send voice utterance:', error);
275
353
  }
276
354
  }
277
-
355
+
278
356
  async clearAllUtterances() {
279
-
357
+
280
358
  this.clearAllBtn.disabled = true;
281
359
  this.clearAllBtn.textContent = 'Clearing...';
282
-
360
+
283
361
  try {
284
362
  const response = await fetch(`${this.baseUrl}/api/utterances`, {
285
363
  method: 'DELETE',
@@ -287,7 +365,7 @@ class VoiceHooksClient {
287
365
  'Content-Type': 'application/json',
288
366
  }
289
367
  });
290
-
368
+
291
369
  if (response.ok) {
292
370
  const result = await response.json();
293
371
  this.loadData(); // Refresh the list
@@ -304,12 +382,340 @@ class VoiceHooksClient {
304
382
  this.clearAllBtn.textContent = 'Clear All';
305
383
  }
306
384
  }
307
-
385
+
308
386
  debugLog(...args) {
309
387
  if (this.debug) {
310
388
  console.log(...args);
311
389
  }
312
390
  }
391
+
392
+ initializeSpeechSynthesis() {
393
+ // Check for browser support
394
+ if (!window.speechSynthesis) {
395
+ console.warn('Speech synthesis not supported in this browser');
396
+ return;
397
+ }
398
+
399
+ // Get available voices
400
+ this.voices = [];
401
+ const loadVoices = () => {
402
+ this.voices = window.speechSynthesis.getVoices();
403
+ this.debugLog('Available voices:', this.voices);
404
+ this.populateVoiceList();
405
+ };
406
+
407
+ // Load voices initially and on change
408
+ loadVoices();
409
+ if (window.speechSynthesis.onvoiceschanged !== undefined) {
410
+ window.speechSynthesis.onvoiceschanged = loadVoices;
411
+ }
412
+
413
+ // Set default voice preferences
414
+ this.speechRate = 1.0;
415
+ this.speechPitch = 1.0;
416
+ this.selectedVoice = 'system';
417
+ }
418
+
419
+ initializeTTSEvents() {
420
+ // Connect to Server-Sent Events endpoint
421
+ this.eventSource = new EventSource(`${this.baseUrl}/api/tts-events`);
422
+
423
+ this.eventSource.onmessage = (event) => {
424
+ try {
425
+ const data = JSON.parse(event.data);
426
+ this.debugLog('TTS Event:', data);
427
+
428
+ if (data.type === 'speak' && data.text) {
429
+ this.speakText(data.text);
430
+ }
431
+ } catch (error) {
432
+ console.error('Failed to parse TTS event:', error);
433
+ }
434
+ };
435
+
436
+ this.eventSource.onerror = (error) => {
437
+ console.error('SSE connection error:', error);
438
+ // Will automatically reconnect
439
+ };
440
+
441
+ this.eventSource.onopen = () => {
442
+ this.debugLog('TTS Events connected');
443
+ // Sync state when connection is established (includes reconnections)
444
+ this.syncStateWithServer();
445
+ };
446
+ }
447
+
448
+ populateVoiceList() {
449
+ if (!this.voiceSelect || !this.localVoicesGroup || !this.cloudVoicesGroup) return;
450
+
451
+ // Clear existing browser voice options
452
+ this.localVoicesGroup.innerHTML = '';
453
+ this.cloudVoicesGroup.innerHTML = '';
454
+
455
+ // List of voices to exclude (novelty, Eloquence, and non-premium voices)
456
+ const excludedVoices = [
457
+ // Eloquence voices
458
+ 'Eddy', 'Flo', 'Grandma', 'Grandpa', 'Reed', 'Rocko', 'Sandy', 'Shelley',
459
+ // Novelty voices
460
+ 'Albert', 'Bad News', 'Bahh', 'Bells', 'Boing', 'Bubbles', 'Cellos',
461
+ 'Good News', 'Jester', 'Organ', 'Superstar', 'Trinoids', 'Whisper',
462
+ 'Wobble', 'Zarvox',
463
+ // Voices without premium options
464
+ 'Fred', 'Junior', 'Kathy', 'Ralph'
465
+ ];
466
+
467
+ // Filter and add only English voices
468
+ this.voices.forEach((voice, index) => {
469
+ // Only include English voices (en-US, en-GB, en-AU, etc.)
470
+ if (voice.lang.toLowerCase().startsWith('en-')) {
471
+ // Check if voice should be excluded
472
+ const voiceName = voice.name;
473
+ const isExcluded = excludedVoices.some(excluded =>
474
+ voiceName.toLowerCase().startsWith(excluded.toLowerCase())
475
+ );
476
+
477
+ if (!isExcluded) {
478
+ const option = document.createElement('option');
479
+ option.value = `browser:${index}`;
480
+ // Show voice name and language code
481
+ option.textContent = `${voice.name} (${voice.lang})`;
482
+
483
+ // Categorize voices
484
+ if (voice.localService) {
485
+ this.localVoicesGroup.appendChild(option);
486
+ this.debugLog(voice.voiceURI);
487
+ } else {
488
+ this.cloudVoicesGroup.appendChild(option);
489
+ }
490
+ }
491
+ }
492
+ });
493
+
494
+ // Hide empty groups
495
+ if (this.localVoicesGroup.children.length === 0) {
496
+ this.localVoicesGroup.style.display = 'none';
497
+ } else {
498
+ this.localVoicesGroup.style.display = '';
499
+ }
500
+
501
+ if (this.cloudVoicesGroup.children.length === 0) {
502
+ this.cloudVoicesGroup.style.display = 'none';
503
+ } else {
504
+ this.cloudVoicesGroup.style.display = '';
505
+ }
506
+
507
+ // Restore saved selection
508
+ const savedVoice = localStorage.getItem('selectedVoice');
509
+ if (savedVoice) {
510
+ this.voiceSelect.value = savedVoice;
511
+ this.selectedVoice = savedVoice;
512
+ } else {
513
+ this.selectedVoice = 'system';
514
+ }
515
+
516
+ // Update warnings based on selected voice
517
+ this.updateVoiceWarnings();
518
+ }
519
+
520
+ async speakText(text) {
521
+ // Check if we should use system voice
522
+ if (this.selectedVoice === 'system') {
523
+ // Use Mac system voice via server
524
+ try {
525
+ const response = await fetch(`${this.baseUrl}/api/speak-system`, {
526
+ method: 'POST',
527
+ headers: {
528
+ 'Content-Type': 'application/json',
529
+ },
530
+ body: JSON.stringify({
531
+ text: text,
532
+ rate: Math.round(this.speechRate * 150) // Convert rate to words per minute
533
+ }),
534
+ });
535
+
536
+ if (!response.ok) {
537
+ const error = await response.json();
538
+ console.error('Failed to speak via system voice:', error);
539
+ }
540
+ } catch (error) {
541
+ console.error('Failed to call speak-system API:', error);
542
+ }
543
+ } else {
544
+ // Use browser voice
545
+ if (!window.speechSynthesis) {
546
+ console.error('Speech synthesis not available');
547
+ return;
548
+ }
549
+
550
+ // Cancel any ongoing speech
551
+ window.speechSynthesis.cancel();
552
+
553
+ // Create utterance
554
+ const utterance = new SpeechSynthesisUtterance(text);
555
+
556
+ // Set voice if using browser voice
557
+ if (this.selectedVoice && this.selectedVoice.startsWith('browser:')) {
558
+ const voiceIndex = parseInt(this.selectedVoice.substring(8));
559
+ if (this.voices[voiceIndex]) {
560
+ utterance.voice = this.voices[voiceIndex];
561
+ }
562
+ }
563
+
564
+ // Set speech properties
565
+ utterance.rate = this.speechRate;
566
+ utterance.pitch = this.speechPitch;
567
+
568
+ // Event handlers
569
+ utterance.onstart = () => {
570
+ this.debugLog('Started speaking:', text);
571
+ };
572
+
573
+ utterance.onend = () => {
574
+ this.debugLog('Finished speaking');
575
+ };
576
+
577
+ utterance.onerror = (event) => {
578
+ console.error('Speech synthesis error:', event);
579
+ };
580
+
581
+ // Speak the text
582
+ window.speechSynthesis.speak(utterance);
583
+ }
584
+ }
585
+
586
+ loadPreferences() {
587
+ // Simple localStorage with defaults to true
588
+ const storedVoiceResponses = localStorage.getItem('voiceResponsesEnabled');
589
+
590
+ // Default to true if not stored
591
+ const voiceResponsesEnabled = storedVoiceResponses !== null
592
+ ? storedVoiceResponses === 'true'
593
+ : true;
594
+
595
+ // Set the checkbox
596
+ this.voiceResponsesToggle.checked = voiceResponsesEnabled;
597
+
598
+ // Save to localStorage if this is first time
599
+ if (storedVoiceResponses === null) {
600
+ localStorage.setItem('voiceResponsesEnabled', 'true');
601
+ }
602
+
603
+ // Load voice settings
604
+ const storedRate = localStorage.getItem('speechRate');
605
+ if (storedRate !== null) {
606
+ this.speechRate = parseFloat(storedRate);
607
+ this.speechRateSlider.value = storedRate;
608
+ this.speechRateInput.value = this.speechRate.toFixed(1);
609
+ }
610
+
611
+ // Load selected voice (will be applied after voices load)
612
+ this.selectedVoice = localStorage.getItem('selectedVoice') || 'system';
613
+
614
+ // Update UI visibility
615
+ this.updateVoiceOptionsVisibility();
616
+
617
+ // Send preferences to server
618
+ this.updateVoicePreferences();
619
+
620
+ // Update warnings after preferences are loaded
621
+ this.updateVoiceWarnings();
622
+ }
623
+
624
+ updateVoiceOptionsVisibility() {
625
+ const voiceResponsesEnabled = this.voiceResponsesToggle.checked;
626
+ this.voiceOptions.style.display = voiceResponsesEnabled ? 'flex' : 'none';
627
+ }
628
+
629
+ async updateVoicePreferences() {
630
+ const voiceResponsesEnabled = this.voiceResponsesToggle.checked;
631
+
632
+ try {
633
+ // Send preferences to server
634
+ await fetch(`${this.baseUrl}/api/voice-preferences`, {
635
+ method: 'POST',
636
+ headers: {
637
+ 'Content-Type': 'application/json',
638
+ },
639
+ body: JSON.stringify({
640
+ voiceResponsesEnabled
641
+ }),
642
+ });
643
+
644
+ this.debugLog('Voice preferences updated:', { voiceResponsesEnabled });
645
+ } catch (error) {
646
+ console.error('Failed to update voice preferences:', error);
647
+ }
648
+ }
649
+
650
+ async updateVoiceInputState(active) {
651
+ try {
652
+ // Send voice input state to server
653
+ await fetch(`${this.baseUrl}/api/voice-input-state`, {
654
+ method: 'POST',
655
+ headers: {
656
+ 'Content-Type': 'application/json',
657
+ },
658
+ body: JSON.stringify({ active }),
659
+ });
660
+
661
+ this.debugLog('Voice input state updated:', { active });
662
+ } catch (error) {
663
+ console.error('Failed to update voice input state:', error);
664
+ }
665
+ }
666
+
667
+ async syncStateWithServer() {
668
+ this.debugLog('Syncing state with server after reconnection');
669
+
670
+ // Sync voice response preferences
671
+ await this.updateVoicePreferences();
672
+
673
+ // Sync voice input state if currently listening
674
+ if (this.isListening) {
675
+ await this.updateVoiceInputState(true);
676
+ }
677
+ }
678
+
679
+ updateVoiceWarnings() {
680
+ // Show/hide warnings based on selected voice
681
+ if (this.selectedVoice === 'system') {
682
+ // Show system voice info for Mac System Voice
683
+ this.systemVoiceInfo.style.display = 'flex';
684
+ this.rateWarning.style.display = 'none';
685
+ } else if (this.selectedVoice && this.selectedVoice.startsWith('browser:')) {
686
+ // Check voice properties
687
+ const voiceIndex = parseInt(this.selectedVoice.substring(8));
688
+ const voice = this.voices[voiceIndex];
689
+
690
+ if (voice) {
691
+ const isGoogleVoice = voice.name.toLowerCase().includes('google');
692
+ const isLocalVoice = voice.localService === true;
693
+
694
+ // Show appropriate warnings
695
+ if (isGoogleVoice) {
696
+ // Show rate warning for Google voices
697
+ this.rateWarning.style.display = 'flex';
698
+ } else {
699
+ this.rateWarning.style.display = 'none';
700
+ }
701
+
702
+ if (isLocalVoice) {
703
+ // Show system info for local browser voices
704
+ this.systemVoiceInfo.style.display = 'flex';
705
+ } else {
706
+ this.systemVoiceInfo.style.display = 'none';
707
+ }
708
+ } else {
709
+ // Hide both warnings if voice not found
710
+ this.rateWarning.style.display = 'none';
711
+ this.systemVoiceInfo.style.display = 'none';
712
+ }
713
+ } else {
714
+ // Hide both warnings if no voice selected
715
+ this.rateWarning.style.display = 'none';
716
+ this.systemVoiceInfo.style.display = 'none';
717
+ }
718
+ }
313
719
  }
314
720
 
315
721
  // Initialize the client when the page loads