pykara commited on
Commit
62fe943
·
1 Parent(s): 0a2fc2b
src/app/chat/chat.component.html CHANGED
@@ -73,109 +73,110 @@
73
  </div>
74
 
75
  <!-- AI message -->
76
- <div *ngIf="message.from === 'ai' && !message.pending" class="message-wrapper ai">
77
- <div class="profile-pic">
78
- <img src="assets/images/chat/natasha.png" alt="AI" />
79
- </div>
80
-
81
- <div class="message structured-response">
82
- <div [innerHTML]="formatStructuredResponse(message.text)"></div>
83
-
84
- <!-- Follow-ups -->
85
- <div class="followups" *ngIf="message.suggestions?.length">
86
- <div class="followups-title">Follow-up suggestions</div>
87
- <button class="followup-chip"
88
- *ngFor="let s of message.suggestions"
89
- (click)="selectHardcodedQuestion(s)"
90
- title="Ask this next">
91
- {{ s }}
92
- </button>
93
  </div>
94
 
95
- <div class="message-timestamp">
96
- {{ message.timestamp }}
97
-
98
- <!-- Copy -->
99
- <button class="icon-btn" (click)="copyToClipboard(message.text, i)"
100
- [attr.aria-label]="copySuccessIndex === i ? 'Copied' : 'Copy message'"
101
- title="Copy message">
102
- <ng-container *ngIf="copySuccessIndex === i; else showCopy">
103
- <span class="copy-tick">&#10003;</span>
104
- </ng-container>
105
- <ng-template #showCopy>
106
- <img src="assets/images/chat/copy.png" alt="Copy" class="meta-icon" />
107
- </ng-template>
108
- </button>
109
-
110
- <!-- Audio: play / stop -->
111
- <button class="icon-btn"
112
- *ngIf="message.audioUrl && isReadingIndex !== i"
113
- (click)="playServerAudioForMessage(i)"
114
- aria-label="Play audio" title="Play audio">
115
- <img src="assets/images/chat/speaker.png" alt="Play audio" class="meta-icon" />
116
- </button>
117
-
118
- <button class="icon-btn"
119
- *ngIf="message.audioUrl && isReadingIndex === i"
120
- (click)="stopReadAloud()"
121
- aria-label="Stop audio" title="Stop audio">
122
- <img src="assets/images/chat/stop-button.png" alt="Stop audio" class="meta-icon" />
123
- </button>
124
-
125
- <!-- Generate audio on demand -->
126
- <button class="icon-btn"
127
- *ngIf="!message.audioUrl"
128
- (click)="synthesizeAudioAndPlay(i)"
129
- [disabled]="message.isSynthesizing"
130
- aria-label="Generate audio"
131
- title="Generate audio">
132
- <ng-container *ngIf="!message.isSynthesizing; else audioSpinner">
133
- <img src="assets/images/chat/speaker.png" alt="Generate audio" class="meta-icon" />
134
- </ng-container>
135
- <ng-template #audioSpinner>
136
- <img src="assets/images/chat/loading-spinner.gif" alt="Generating audio" class="meta-icon" />
137
- </ng-template>
138
- </button>
139
-
140
- <!-- Video: generate, play, stop -->
141
- <!-- Show Generate when no cached video -->
142
- <button class="icon-btn"
143
- *ngIf="!message.videoUrl && !message.isVideoSynthesizing"
144
- (click)="synthesizeVideoAndPlay(i)"
145
- aria-label="Generate video"
146
- title="Generate video">
147
- <img src="assets/images/chat/video.png" alt="Generate video" class="meta-icon" />
148
- </button>
149
-
150
- <!-- Spinner while generating video -->
151
- <button class="icon-btn" *ngIf="!message.videoUrl && message.isVideoSynthesizing" disabled>
152
- <img src="assets/images/chat/loading-spinner.gif" alt="Generating video" class="meta-icon" />
153
- </button>
154
-
155
- <!-- Toggle inline video (single button). Show video.png initially, switches to no-video.png when enabled -->
156
- <button class="icon-btn"
157
- *ngIf="message.videoUrl"
158
- (click)="toggleMessageVideo(i)"
159
- [class.active]="isVideoEnabledIndex[i]"
160
- aria-label="Toggle video"
161
- title="Toggle video">
162
- <img [src]="isVideoEnabledIndex[i] ? 'assets/images/chat/no-video.png' : 'assets/images/chat/video.png'"
163
- alt="Video" class="meta-icon" />
164
- </button>
165
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
-
168
- <!-- Inline video player -->
169
- <video *ngIf="isVideoEnabledIndex[i] && message.playingVideoUrl"
170
- id="inline-video-{{i}}"
171
- class="tutor-video"
172
- [src]="message.playingVideoUrl"
173
- (ended)="onMessageVideoEnded(i)"
174
- controls autoplay>
175
- </video>
176
  </div>
177
  </div>
178
- </div>
179
 
180
  <!-- Typing indicator -->
181
  <div *ngIf="isTyping" class="typing-indicator" aria-live="polite">
 
73
  </div>
74
 
75
  <!-- AI message -->
76
+
77
+ <div *ngIf="message.from === 'ai'" class="message-wrapper ai">
78
+ <div class="profile-pic">
79
+ <img src="assets/images/chat/natasha.png" alt="AI" />
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
 
82
+ <div class="message structured-response">
83
+ <div [innerHTML]="formatStructuredResponse(message.text)"></div>
84
+
85
+ <!-- Follow-ups -->
86
+ <div class="followups" *ngIf="message.suggestions?.length">
87
+ <div class="followups-title">Follow-up suggestions</div>
88
+ <button class="followup-chip"
89
+ *ngFor="let s of message.suggestions"
90
+ (click)="selectHardcodedQuestion(s)"
91
+ title="Ask this next">
92
+ {{ s }}
93
+ </button>
94
+ </div>
95
+
96
+ <div class="message-timestamp">
97
+ {{ message.timestamp }}
98
+
99
+ <!-- Copy -->
100
+ <button class="icon-btn" (click)="copyToClipboard(message.text, i)"
101
+ [attr.aria-label]="copySuccessIndex === i ? 'Copied' : 'Copy message'"
102
+ title="Copy message">
103
+ <ng-container *ngIf="copySuccessIndex === i; else showCopy">
104
+ <span class="copy-tick">&#10003;</span>
105
+ </ng-container>
106
+ <ng-template #showCopy>
107
+ <img src="assets/images/chat/copy.png" alt="Copy" class="meta-icon" />
108
+ </ng-template>
109
+ </button>
110
+
111
+ <!-- Audio: play / stop -->
112
+ <button class="icon-btn"
113
+ *ngIf="message.audioUrl && isReadingIndex !== i"
114
+ (click)="playServerAudioForMessage(i)"
115
+ aria-label="Play audio" title="Play audio">
116
+ <img src="assets/images/chat/speaker.png" alt="Play audio" class="meta-icon" />
117
+ </button>
118
+
119
+ <button class="icon-btn"
120
+ *ngIf="message.audioUrl && isReadingIndex === i"
121
+ (click)="stopReadAloud()"
122
+ aria-label="Stop audio" title="Stop audio">
123
+ <img src="assets/images/chat/stop-button.png" alt="Stop audio" class="meta-icon" />
124
+ </button>
125
+
126
+ <!-- Generate audio on demand -->
127
+ <button class="icon-btn"
128
+ *ngIf="!message.audioUrl"
129
+ (click)="synthesizeAudioAndPlay(i)"
130
+ [disabled]="message.isSynthesizing"
131
+ aria-label="Generate audio"
132
+ title="Generate audio">
133
+ <ng-container *ngIf="!message.isSynthesizing; else audioSpinner">
134
+ <img src="assets/images/chat/speaker.png" alt="Generate audio" class="meta-icon" />
135
+ </ng-container>
136
+ <ng-template #audioSpinner>
137
+ <img src="assets/images/chat/loading-spinner.gif" alt="Generating audio" class="meta-icon" />
138
+ </ng-template>
139
+ </button>
140
+
141
+ <!-- Video: generate, play, stop -->
142
+ <!-- Show Generate when no cached video -->
143
+ <button class="icon-btn"
144
+ *ngIf="!message.videoUrl && !message.isVideoSynthesizing"
145
+ (click)="synthesizeVideoAndPlay(i)"
146
+ aria-label="Generate video"
147
+ title="Generate video">
148
+ <img src="assets/images/chat/video.png" alt="Generate video" class="meta-icon" />
149
+ </button>
150
+
151
+ <!-- Spinner while generating video -->
152
+ <button class="icon-btn" *ngIf="!message.videoUrl && message.isVideoSynthesizing" disabled>
153
+ <img src="assets/images/chat/loading-spinner.gif" alt="Generating video" class="meta-icon" />
154
+ </button>
155
+
156
+ <!-- Toggle inline video (single button). Show video.png initially, switches to no-video.png when enabled -->
157
+ <button class="icon-btn"
158
+ *ngIf="message.videoUrl"
159
+ (click)="toggleMessageVideo(i)"
160
+ [class.active]="isVideoEnabledIndex[i]"
161
+ aria-label="Toggle video"
162
+ title="Toggle video">
163
+ <img [src]="isVideoEnabledIndex[i] ? 'assets/images/chat/no-video.png' : 'assets/images/chat/video.png'"
164
+ alt="Video" class="meta-icon" />
165
+ </button>
166
+
167
+ </div>
168
+
169
+ <!-- Inline video player -->
170
+ <video *ngIf="isVideoEnabledIndex[i] && message.playingVideoUrl"
171
+ id="inline-video-{{i}}"
172
+ class="tutor-video"
173
+ [src]="message.playingVideoUrl"
174
+ (ended)="onMessageVideoEnded(i)"
175
+ controls autoplay>
176
+ </video>
177
  </div>
 
 
 
 
 
 
 
 
 
178
  </div>
179
  </div>
 
180
 
181
  <!-- Typing indicator -->
182
  <div *ngIf="isTyping" class="typing-indicator" aria-live="polite">
src/app/chat/chat.component.ts CHANGED
@@ -284,14 +284,7 @@ export class ChatComponent implements OnDestroy {
284
  }
285
  }
286
 
287
- //private storageHandler = (e: StorageEvent) => {
288
- // if (e.key === 'hasPDF') this.showRagModeAlert();
289
- //};
290
-
291
- //private showRagModeAlert(): void {
292
- // const ragEnabled = localStorage.getItem('rag_enabled') === '1' || localStorage.getItem('hasPDF') === 'true';
293
- // // No-op placeholder
294
- //}
295
 
296
  ngOnInit(): void {
297
  this.ensureGradeLevel();
@@ -303,11 +296,7 @@ export class ChatComponent implements OnDestroy {
303
  this.loadVoices();
304
  }
305
 
306
- //ngAfterViewChecked() {
307
- // setTimeout(() => {
308
- // this.scrollToBottom();
309
- // }, 100);
310
- //}
311
 
312
  ngOnDestroy(): void {
313
  if (this.currentExplainSub) { this.currentExplainSub.unsubscribe(); this.currentExplainSub = null; }
@@ -335,13 +324,7 @@ export class ChatComponent implements OnDestroy {
335
  });
336
  }
337
 
338
- //scrollToBottom(): void {
339
- // try {
340
- // this.chatBox.nativeElement.scrollTo({ top: this.chatBox.nativeElement.scrollHeight, behavior: 'smooth' });
341
- // } catch (err) {
342
- // console.error('Scroll error:', err);
343
- // }
344
- //}
345
 
346
  scrollToBottom(): void {
347
  if (this.shouldAutoScroll) {
@@ -565,6 +548,7 @@ export class ChatComponent implements OnDestroy {
565
  this.sendMessage();
566
  }
567
 
 
568
  /** Send question to backend for an answer */
569
  sendMessage(inputText?: string): void {
570
  const message = inputText ? inputText.trim() : this.userInput.trim();
@@ -595,49 +579,64 @@ export class ChatComponent implements OnDestroy {
595
  ? response.source_ids.filter((s: any) => typeof s === 'string' && s.trim().length > 0)
596
  : [];
597
 
598
- this.messages.push({
599
- from: 'ai',
600
- text: explanation,
601
- timestamp: new Date().toLocaleTimeString(),
602
- source_ids: sourceIds
603
- });
604
- this.cdr.detectChanges();
605
- this.shouldAutoScroll = true;
606
- this.scrollToBottom();
607
-
608
  this.lastQuestion = message;
609
- this.lastAnswer = explanation;
610
  this.lastSourceIds = sourceIds;
611
 
612
- // NEW: mark if the answer is grounded in textbook pages
613
  const notFound = /No information available in the provided textbook content/i.test(explanation);
614
- this.lastAnswerHasContext = !!sourceIds.length && !notFound;
615
 
616
- this.speakResponse(explanation);
 
 
 
617
  },
618
  error: (err) => {
619
  console.error('API Error:', err);
620
  this.isTyping = false;
621
  const errorMessage = 'Error: Could not get a response from the server.';
622
 
623
- this.messages.push({
624
- from: 'ai',
625
- text: errorMessage,
626
- timestamp: new Date().toLocaleTimeString(),
627
- source_ids: []
628
- });
629
- this.cdr.detectChanges();
630
- this.shouldAutoScroll = true;
631
- this.scrollToBottom();
632
-
633
- // NEW: ensure follow-ups are not attempted after an error
634
- this.lastAnswerHasContext = false;
635
-
636
- this.speakResponse(errorMessage);
637
  }
638
  });
639
  }
640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
 
643
  displaySource(tag: string): string {
@@ -658,11 +657,23 @@ export class ChatComponent implements OnDestroy {
658
  }
659
 
660
  /** TTS helpers */
661
- animateAiResponse(responseText: string, targetIndex?: number): void {
662
- if (!responseText) { this.isAiResponding = false; return; }
 
 
 
 
 
 
 
 
 
663
 
 
664
  let aiIndex: number | null = null;
665
- if (typeof targetIndex === 'number' && this.messages[targetIndex] && this.messages[targetIndex].from === 'ai') {
 
 
666
  aiIndex = targetIndex;
667
  } else {
668
  for (let i = this.messages.length - 1; i >= 0; i--) {
@@ -671,36 +682,58 @@ export class ChatComponent implements OnDestroy {
671
  }
672
 
673
  if (aiIndex === null || aiIndex < 0 || !this.messages[aiIndex]) {
674
- this.messages.push({ from: 'ai', text: '', timestamp: new Date().toLocaleTimeString() } as any);
 
 
 
 
675
  aiIndex = this.messages.length - 1;
676
  this.isVideoEnabledIndex.push(false);
677
  }
678
 
679
  const aiMsg = this.messages[aiIndex] as any;
680
 
681
- if (this.aiResponseInterval) { clearInterval(this.aiResponseInterval); this.aiResponseInterval = null; }
 
 
 
682
 
683
  aiMsg.text = '';
 
 
684
  this.cdr.detectChanges();
685
 
686
- const words = responseText.split(' ');
687
  let idx = 0;
 
 
 
 
688
  this.aiResponseInterval = setInterval(() => {
689
  if (idx < words.length) {
690
  aiMsg.text = words.slice(0, idx + 1).join(' ');
691
  idx++;
692
  this.cdr.detectChanges();
 
693
  } else {
694
- clearInterval(this.aiResponseInterval);
695
  this.aiResponseInterval = null;
696
  aiMsg.text = responseText;
697
  aiMsg.pending = false;
698
- this.cdr.detectChanges();
699
  this.isAiResponding = false;
 
 
 
 
 
 
 
700
  }
701
- }, 280);
702
  }
703
 
 
 
704
  stopAiResponse(): void {
705
  if (this.currentExplainSub) { this.currentExplainSub.unsubscribe(); this.currentExplainSub = null; }
706
  if (this.currentFollowupsSub) { this.currentFollowupsSub.unsubscribe(); this.currentFollowupsSub = null; }
@@ -748,13 +781,19 @@ export class ChatComponent implements OnDestroy {
748
 
749
  speakResponse(responseText: string): void {
750
  if (!responseText) return;
 
 
 
 
751
  this.stopAllVideo();
 
752
  const speech = new SpeechSynthesisUtterance();
753
  speech.text = responseText;
754
  speech.lang = 'en-US';
755
  speech.pitch = 1;
756
  speech.rate = 1;
757
  this.isSpeaking = true;
 
758
  const voices = window.speechSynthesis.getVoices();
759
  const preferred = [
760
  'Google UK English Female',
@@ -769,24 +808,21 @@ export class ChatComponent implements OnDestroy {
769
  if (found) { speech.voice = found; break; }
770
  }
771
  if (!speech.voice && voices.length) speech.voice = voices[0];
772
- speech.onend = () => { this.isSpeaking = false; this.cdr.detectChanges(); };
773
- window.speechSynthesis.speak(speech);
 
 
 
 
 
 
 
 
 
774
  }
775
 
776
- //pauseAudio(): void {
777
- // if (this.serverAudio && !this.serverAudio.paused) {
778
- // this.serverAudio.pause();
779
- // this.isAudioPaused = true;
780
- // if (this.serverAudioMessageIndex !== null) this.messages[this.serverAudioMessageIndex].isPlaying = false;
781
- // this.cdr.detectChanges();
782
- // return;
783
- // }
784
- // if (window.speechSynthesis && window.speechSynthesis.speaking && !window.speechSynthesis.paused) {
785
- // window.speechSynthesis.pause();
786
- // this.isAudioPaused = true;
787
- // this.cdr.detectChanges();
788
- // }
789
- //}
790
 
791
  resumeAudio(): void {
792
  if (this.serverAudio && this.serverAudio.paused) {
 
284
  }
285
  }
286
 
287
+
 
 
 
 
 
 
 
288
 
289
  ngOnInit(): void {
290
  this.ensureGradeLevel();
 
296
  this.loadVoices();
297
  }
298
 
299
+
 
 
 
 
300
 
301
  ngOnDestroy(): void {
302
  if (this.currentExplainSub) { this.currentExplainSub.unsubscribe(); this.currentExplainSub = null; }
 
324
  });
325
  }
326
 
327
+
 
 
 
 
 
 
328
 
329
  scrollToBottom(): void {
330
  if (this.shouldAutoScroll) {
 
548
  this.sendMessage();
549
  }
550
 
551
+ /** Send question to backend for an answer */
552
  /** Send question to backend for an answer */
553
  sendMessage(inputText?: string): void {
554
  const message = inputText ? inputText.trim() : this.userInput.trim();
 
579
  ? response.source_ids.filter((s: any) => typeof s === 'string' && s.trim().length > 0)
580
  : [];
581
 
582
+ // Store question + source ids *now*.
 
 
 
 
 
 
 
 
 
583
  this.lastQuestion = message;
 
584
  this.lastSourceIds = sourceIds;
585
 
 
586
  const notFound = /No information available in the provided textbook content/i.test(explanation);
587
+ const hasContext = !!sourceIds.length && !notFound;
588
 
589
+ // IMPORTANT:
590
+ // Do NOT set this.lastAnswer here.
591
+ // We will set it only after the streaming animation has finished.
592
+ this.streamAiAnswer(explanation, sourceIds, hasContext);
593
  },
594
  error: (err) => {
595
  console.error('API Error:', err);
596
  this.isTyping = false;
597
  const errorMessage = 'Error: Could not get a response from the server.';
598
 
599
+ // We can still stream the error text (or you can show it directly if you prefer)
600
+ this.streamAiAnswer(errorMessage, [], false);
 
 
 
 
 
 
 
 
 
 
 
 
601
  }
602
  });
603
  }
604
 
605
+
606
+
607
+ /** Show AI answer word-by-word and start audio */
608
+ /** Show AI answer word-by-word and start audio */
609
+ private streamAiAnswer(explanation: string, sourceIds: string[], hasContext: boolean): void {
610
+ const text = (explanation || '').trim() || 'No explanation available.';
611
+ const timestamp = new Date().toLocaleTimeString();
612
+
613
+ // Create an empty AI message first
614
+ const aiIndex = this.messages.push({
615
+ from: 'ai',
616
+ text: '',
617
+ timestamp,
618
+ source_ids: sourceIds,
619
+ pending: true
620
+ } as any) - 1;
621
+
622
+ this.isAiResponding = true;
623
+ this.shouldAutoScroll = true;
624
+ this.cdr.detectChanges();
625
+
626
+ // Animate the text word by word
627
+ this.animateAiResponse(text, aiIndex, () => {
628
+ // When streaming is finished, we finally store lastAnswer
629
+ this.lastAnswer = text;
630
+ this.lastAnswerHasContext = hasContext;
631
+ });
632
+
633
+ // Play audio at the same time (only if voice is enabled)
634
+ this.speakResponse(text);
635
+ }
636
+
637
+
638
+
639
+
640
 
641
 
642
  displaySource(tag: string): string {
 
657
  }
658
 
659
  /** TTS helpers */
660
+ /** TTS helpers */
661
+ /** TTS helpers + typing animation */
662
+ animateAiResponse(
663
+ responseText: string,
664
+ targetIndex?: number,
665
+ onDone?: () => void
666
+ ): void {
667
+ if (!responseText) {
668
+ this.isAiResponding = false;
669
+ return;
670
+ }
671
 
672
+ // Find or create the AI message to animate into
673
  let aiIndex: number | null = null;
674
+ if (typeof targetIndex === 'number' &&
675
+ this.messages[targetIndex] &&
676
+ this.messages[targetIndex].from === 'ai') {
677
  aiIndex = targetIndex;
678
  } else {
679
  for (let i = this.messages.length - 1; i >= 0; i--) {
 
682
  }
683
 
684
  if (aiIndex === null || aiIndex < 0 || !this.messages[aiIndex]) {
685
+ this.messages.push({
686
+ from: 'ai',
687
+ text: '',
688
+ timestamp: new Date().toLocaleTimeString()
689
+ } as any);
690
  aiIndex = this.messages.length - 1;
691
  this.isVideoEnabledIndex.push(false);
692
  }
693
 
694
  const aiMsg = this.messages[aiIndex] as any;
695
 
696
+ if (this.aiResponseInterval) {
697
+ clearInterval(this.aiResponseInterval);
698
+ this.aiResponseInterval = null;
699
+ }
700
 
701
  aiMsg.text = '';
702
+ aiMsg.pending = true;
703
+ this.isAiResponding = true;
704
  this.cdr.detectChanges();
705
 
706
+ const words = responseText.split(/\s+/).filter(w => w.length);
707
  let idx = 0;
708
+
709
+ // 1 word every 200 ms – you can tune this
710
+ const speedMs = 200;
711
+
712
  this.aiResponseInterval = setInterval(() => {
713
  if (idx < words.length) {
714
  aiMsg.text = words.slice(0, idx + 1).join(' ');
715
  idx++;
716
  this.cdr.detectChanges();
717
+ this.scrollToBottom(); // keep view at bottom while streaming
718
  } else {
719
+ clearInterval(this.aiResponseInterval!);
720
  this.aiResponseInterval = null;
721
  aiMsg.text = responseText;
722
  aiMsg.pending = false;
 
723
  this.isAiResponding = false;
724
+
725
+ if (onDone) {
726
+ onDone();
727
+ }
728
+
729
+ this.cdr.detectChanges();
730
+ this.scrollToBottom();
731
  }
732
+ }, speedMs);
733
  }
734
 
735
+
736
+
737
  stopAiResponse(): void {
738
  if (this.currentExplainSub) { this.currentExplainSub.unsubscribe(); this.currentExplainSub = null; }
739
  if (this.currentFollowupsSub) { this.currentFollowupsSub.unsubscribe(); this.currentFollowupsSub = null; }
 
781
 
782
  speakResponse(responseText: string): void {
783
  if (!responseText) return;
784
+
785
+ // Only speak when the Voice toggle is ON
786
+ if (!this.isVoiceEnabled) return;
787
+
788
  this.stopAllVideo();
789
+
790
  const speech = new SpeechSynthesisUtterance();
791
  speech.text = responseText;
792
  speech.lang = 'en-US';
793
  speech.pitch = 1;
794
  speech.rate = 1;
795
  this.isSpeaking = true;
796
+
797
  const voices = window.speechSynthesis.getVoices();
798
  const preferred = [
799
  'Google UK English Female',
 
808
  if (found) { speech.voice = found; break; }
809
  }
810
  if (!speech.voice && voices.length) speech.voice = voices[0];
811
+
812
+ speech.onend = () => {
813
+ this.isSpeaking = false;
814
+ this.cdr.detectChanges();
815
+ };
816
+
817
+ try {
818
+ window.speechSynthesis.speak(speech);
819
+ } catch {
820
+ this.isSpeaking = false;
821
+ }
822
  }
823
 
824
+
825
+
 
 
 
 
 
 
 
 
 
 
 
 
826
 
827
  resumeAudio(): void {
828
  if (this.serverAudio && this.serverAudio.paused) {