ciyidogan commited on
Commit
3c4e662
·
verified ·
1 Parent(s): c87f24b

Update flare-ui/src/app/components/chat/chat.component.ts

Browse files
flare-ui/src/app/components/chat/chat.component.ts CHANGED
@@ -11,7 +11,8 @@ import { MatDividerModule } from '@angular/material/divider';
11
  import { MatTooltipModule } from '@angular/material/tooltip';
12
  import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
13
  import { MatCheckboxModule } from '@angular/material/checkbox';
14
- import { MatDialog } from '@angular/material/dialog';
 
15
  import { Subject, takeUntil } from 'rxjs';
16
 
17
  import { ApiService } from '../../services/api.service';
@@ -22,7 +23,7 @@ interface ChatMessage {
22
  author: 'user' | 'assistant';
23
  text: string;
24
  timestamp?: Date;
25
- audioUrl?: string; // For TTS audio
26
  }
27
 
28
  @Component({
@@ -41,7 +42,9 @@ interface ChatMessage {
41
  MatDividerModule,
42
  MatTooltipModule,
43
  MatProgressSpinnerModule,
44
- MatCheckboxModule
 
 
45
  ],
46
  templateUrl: './chat.component.html',
47
  styleUrls: ['./chat.component.scss']
@@ -72,17 +75,16 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
72
  analyser?: AnalyserNode;
73
  animationId?: number;
74
 
75
- // Memory leak prevention
76
  private destroyed$ = new Subject<void>();
77
  private shouldScroll = false;
78
- private audioUrls: string[] = []; // Track URLs for cleanup
79
 
80
  constructor(
81
  private fb: FormBuilder,
82
  private api: ApiService,
83
  private environmentService: EnvironmentService,
84
  private dialog: MatDialog,
85
- private router: Router
 
86
  ) {}
87
 
88
  ngOnInit(): void {
@@ -90,9 +92,11 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
90
  this.checkTTSAvailability();
91
  this.checkSTTAvailability();
92
 
93
- // Initialize Audio Context lazily to avoid browser warnings
94
- if ('AudioContext' in window || 'webkitAudioContext' in window) {
95
- // Will be created when needed
 
 
96
  }
97
  }
98
 
@@ -103,44 +107,30 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
103
  }
104
  }
105
 
106
- async ngOnDestroy(): Promise<void> {
107
- // Signal all subscriptions to complete
108
  this.destroyed$.next();
109
  this.destroyed$.complete();
110
 
111
- // Clean up audio resources
 
 
 
 
112
  if (this.animationId) {
113
  cancelAnimationFrame(this.animationId);
114
  this.animationId = undefined;
115
  }
116
 
117
- // Close audio context properly
118
  if (this.audioContext && this.audioContext.state !== 'closed') {
119
- try {
120
- await this.audioContext.close();
121
- } catch (error) {
122
- console.error('Error closing audio context:', error);
123
- }
124
  }
125
 
126
- // Revoke all created object URLs
127
- this.audioUrls.forEach(url => {
128
- try {
129
- URL.revokeObjectURL(url);
130
- } catch (error) {
131
- console.error('Error revoking URL:', error);
132
  }
133
  });
134
- this.audioUrls = [];
135
-
136
- // Clean up any active session
137
- if (this.sessionId) {
138
- this.api.endSession(this.sessionId).pipe(
139
- takeUntil(this.destroyed$)
140
- ).subscribe({
141
- error: (err) => console.error('Error ending session:', err)
142
- });
143
- }
144
  }
145
 
146
  private checkSTTAvailability(): void {
@@ -149,7 +139,6 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
149
  ).subscribe({
150
  next: (env) => {
151
  this.sttAvailable = env.stt_engine !== 'no_stt';
152
- this.environmentService.setSTTEnabled(this.sttAvailable);
153
  if (!this.sttAvailable) {
154
  this.useSTT = false;
155
  }
@@ -157,26 +146,26 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
157
  error: (err) => {
158
  console.error('Failed to check STT availability:', err);
159
  this.sttAvailable = false;
160
- this.environmentService.setSTTEnabled(false);
161
  }
162
  });
163
  }
164
 
165
- startRealtimeChat(): void {
166
  if (!this.selectedProject) {
167
  this.error = 'Please select a project first';
 
168
  return;
169
  }
170
 
171
  if (!this.sttAvailable) {
172
  this.error = 'STT is not configured. Please configure it in Environment settings.';
 
173
  return;
174
  }
175
 
176
  this.loading = true;
177
  this.error = '';
178
 
179
- // Start a new session for realtime chat
180
  this.api.startChat(this.selectedProject).pipe(
181
  takeUntil(this.destroyed$)
182
  ).subscribe({
@@ -191,17 +180,21 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
191
  this.loading = false;
192
  },
193
  error: (err) => {
194
- this.error = err.error?.detail || 'Failed to start realtime session';
195
  this.loading = false;
196
- console.error('Start realtime chat error:', err);
 
 
 
197
  }
198
  });
199
  }
200
 
201
- private openRealtimeDialog(sessionId: string): void {
202
- // Dynamic import to reduce initial bundle size
203
- import('../chat/realtime-chat.component').then(module => {
204
- const dialogRef = this.dialog.open(module.RealtimeChatComponent, {
 
205
  width: '90%',
206
  maxWidth: '900px',
207
  height: '85vh',
@@ -221,9 +214,8 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
221
  localStorage.removeItem('current_session_id');
222
  localStorage.removeItem('current_project');
223
 
224
- // If session was active, we might want to end it
225
- if (result === 'session_active') {
226
- // Optionally end the session on backend
227
  this.api.endSession(sessionId).pipe(
228
  takeUntil(this.destroyed$)
229
  ).subscribe({
@@ -232,22 +224,19 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
232
  });
233
  }
234
  });
235
- }).catch(error => {
236
- console.error('Failed to load realtime chat component:', error);
237
- this.error = 'Failed to open realtime chat';
238
- this.loading = false;
239
- });
240
- }
241
-
242
- // Alternative: Route navigation
243
- private navigateToRealtimeChat(sessionId: string): void {
244
- this.router.navigate(['/realtime-chat', sessionId], {
245
- queryParams: { project: this.selectedProject }
246
- });
247
  }
248
 
249
  loadProjects(): void {
250
  this.loading = true;
 
 
251
  this.api.getChatProjects().pipe(
252
  takeUntil(this.destroyed$)
253
  ).subscribe({
@@ -261,32 +250,33 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
261
  error: (err) => {
262
  this.error = 'Failed to load projects';
263
  this.loading = false;
264
- console.error('Load projects error:', err);
 
 
 
265
  }
266
  });
267
  }
268
 
269
  checkTTSAvailability(): void {
270
- // Subscribe to environment changes
271
  this.environmentService.environment$.pipe(
272
  takeUntil(this.destroyed$)
273
  ).subscribe(env => {
274
  if (env) {
275
  this.ttsAvailable = env.tts_engine !== 'no_tts';
276
- this.environmentService.setTTSEnabled(this.ttsAvailable);
277
  if (!this.ttsAvailable) {
278
  this.useTTS = false;
279
  }
280
  }
281
  });
282
 
283
- // Also get current environment
284
  this.api.getEnvironment().pipe(
285
  takeUntil(this.destroyed$)
286
  ).subscribe({
287
  next: (env) => {
288
  this.ttsAvailable = env.tts_engine !== 'no_tts';
289
- this.environmentService.setTTSEnabled(this.ttsAvailable);
290
  if (!this.ttsAvailable) {
291
  this.useTTS = false;
292
  }
@@ -295,7 +285,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
295
  }
296
 
297
  startChat(): void {
298
- if (!this.selectedProject) return;
 
 
 
299
 
300
  this.loading = true;
301
  this.error = '';
@@ -321,9 +314,12 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
321
  }
322
  },
323
  error: (err) => {
324
- this.error = err.error?.detail || 'Failed to start session';
325
  this.loading = false;
326
- console.error('Start chat error:', err);
 
 
 
327
  }
328
  });
329
  }
@@ -366,55 +362,64 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
366
  }
367
  },
368
  error: (err) => {
 
369
  this.messages.push({
370
  author: 'assistant',
371
- text: '⚠️ ' + (err.error?.detail || 'Failed to send message. Please try again.'),
372
  timestamp: new Date()
373
  });
374
  this.loading = false;
375
  this.shouldScroll = true;
376
- console.error('Chat error:', err);
377
  }
378
  });
379
  }
380
 
381
  generateTTS(text: string, messageIndex: number): void {
382
- if (!this.ttsAvailable) return;
383
 
384
  this.api.generateTTS(text).pipe(
385
  takeUntil(this.destroyed$)
386
  ).subscribe({
387
  next: (audioBlob) => {
388
  const audioUrl = URL.createObjectURL(audioBlob);
389
- this.audioUrls.push(audioUrl); // Track for cleanup
390
 
391
- if (messageIndex < this.messages.length) {
392
- this.messages[messageIndex].audioUrl = audioUrl;
393
-
394
- // Auto-play the latest message
395
- if (messageIndex === this.messages.length - 1 && this.useTTS) {
396
- setTimeout(() => this.playAudio(audioUrl), 100);
397
- }
 
 
 
398
  }
399
  },
400
  error: (err) => {
401
  console.error('TTS generation error:', err);
402
- // Don't show error to user, just silently fail TTS
 
 
 
403
  }
404
  });
405
  }
406
 
407
  playAudio(audioUrl: string): void {
408
- if (!this.audioPlayer?.nativeElement) return;
409
-
410
- // Stop any currently playing audio
411
- this.stopAudio();
412
 
413
  const audio = this.audioPlayer.nativeElement;
 
 
 
 
 
 
 
414
  audio.src = audioUrl;
415
 
416
- // Set up audio visualization if context exists
417
- if (this.audioContext) {
418
  this.setupAudioVisualization(audio);
419
  }
420
 
@@ -422,7 +427,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
422
  this.playingAudio = true;
423
  }).catch(err => {
424
  console.error('Audio play error:', err);
425
- this.playingAudio = false;
 
 
 
426
  });
427
 
428
  audio.onended = () => {
@@ -436,56 +444,37 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
436
 
437
  audio.onerror = () => {
438
  this.playingAudio = false;
439
- console.error('Audio loading error');
440
  };
441
  }
442
 
443
- stopAudio(): void {
444
- if (this.audioPlayer?.nativeElement) {
445
- const audio = this.audioPlayer.nativeElement;
446
- audio.pause();
447
- audio.currentTime = 0;
448
- }
449
- this.playingAudio = false;
450
-
451
- if (this.animationId) {
452
- cancelAnimationFrame(this.animationId);
453
- this.animationId = undefined;
454
- }
455
- }
456
-
457
  setupAudioVisualization(audio: HTMLAudioElement): void {
458
- if (!this.waveformCanvas?.nativeElement) return;
459
-
460
- // Create audio context if not exists
461
- if (!this.audioContext) {
462
- try {
463
- this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
464
- } catch (error) {
465
- console.error('Failed to create audio context:', error);
466
- return;
467
- }
468
- }
469
 
470
  try {
471
- // Create audio source and analyser
472
- const source = this.audioContext.createMediaElementSource(audio);
473
- this.analyser = this.audioContext.createAnalyser();
474
- this.analyser.fftSize = 256;
475
-
476
- // Connect nodes
477
- source.connect(this.analyser);
478
- this.analyser.connect(this.audioContext.destination);
 
 
 
 
 
479
 
480
  // Start visualization
481
  this.drawWaveform();
482
  } catch (error) {
483
- console.error('Audio visualization setup error:', error);
484
  }
485
  }
486
 
487
  drawWaveform(): void {
488
- if (!this.analyser || !this.waveformCanvas?.nativeElement) return;
489
 
490
  const canvas = this.waveformCanvas.nativeElement;
491
  const ctx = canvas.getContext('2d');
@@ -495,7 +484,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
495
  const dataArray = new Uint8Array(bufferLength);
496
 
497
  const draw = () => {
498
- if (!this.playingAudio) return;
 
 
 
499
 
500
  this.animationId = requestAnimationFrame(draw);
501
 
@@ -522,7 +514,7 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
522
  }
523
 
524
  clearWaveform(): void {
525
- if (!this.waveformCanvas?.nativeElement) return;
526
 
527
  const canvas = this.waveformCanvas.nativeElement;
528
  const ctx = canvas.getContext('2d');
@@ -533,15 +525,23 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
533
  }
534
 
535
  endSession(): void {
 
536
  if (this.sessionId) {
537
- // End session on backend
538
  this.api.endSession(this.sessionId).pipe(
539
  takeUntil(this.destroyed$)
540
  ).subscribe({
541
- error: (err) => console.error('Error ending session:', err)
542
  });
543
  }
544
 
 
 
 
 
 
 
 
 
545
  this.sessionId = null;
546
  this.messages = [];
547
  this.selectedProject = null;
@@ -549,28 +549,44 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
549
  this.error = '';
550
 
551
  // Clean up audio
552
- this.stopAudio();
553
- this.clearWaveform();
 
 
554
 
555
- // Revoke audio URLs
556
- this.audioUrls.forEach(url => {
557
- try {
558
- URL.revokeObjectURL(url);
559
- } catch (error) {
560
- console.error('Error revoking URL:', error);
561
- }
562
- });
563
- this.audioUrls = [];
564
  }
565
 
566
  private scrollToBottom(): void {
567
  try {
568
  if (this.myScrollContainer?.nativeElement) {
569
- this.myScrollContainer.nativeElement.scrollTop =
570
- this.myScrollContainer.nativeElement.scrollHeight;
571
  }
572
  } catch(err) {
573
  console.error('Scroll error:', err);
574
  }
575
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  }
 
11
  import { MatTooltipModule } from '@angular/material/tooltip';
12
  import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
13
  import { MatCheckboxModule } from '@angular/material/checkbox';
14
+ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
15
+ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
16
  import { Subject, takeUntil } from 'rxjs';
17
 
18
  import { ApiService } from '../../services/api.service';
 
23
  author: 'user' | 'assistant';
24
  text: string;
25
  timestamp?: Date;
26
+ audioUrl?: string;
27
  }
28
 
29
  @Component({
 
42
  MatDividerModule,
43
  MatTooltipModule,
44
  MatProgressSpinnerModule,
45
+ MatCheckboxModule,
46
+ MatDialogModule,
47
+ MatSnackBarModule
48
  ],
49
  templateUrl: './chat.component.html',
50
  styleUrls: ['./chat.component.scss']
 
75
  analyser?: AnalyserNode;
76
  animationId?: number;
77
 
 
78
  private destroyed$ = new Subject<void>();
79
  private shouldScroll = false;
 
80
 
81
  constructor(
82
  private fb: FormBuilder,
83
  private api: ApiService,
84
  private environmentService: EnvironmentService,
85
  private dialog: MatDialog,
86
+ private router: Router,
87
+ private snackBar: MatSnackBar
88
  ) {}
89
 
90
  ngOnInit(): void {
 
92
  this.checkTTSAvailability();
93
  this.checkSTTAvailability();
94
 
95
+ // Initialize Audio Context with error handling
96
+ try {
97
+ this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
98
+ } catch (error) {
99
+ console.error('Failed to create AudioContext:', error);
100
  }
101
  }
102
 
 
107
  }
108
  }
109
 
110
+ ngOnDestroy(): void {
 
111
  this.destroyed$.next();
112
  this.destroyed$.complete();
113
 
114
+ // Cleanup audio resources
115
+ this.cleanupAudio();
116
+ }
117
+
118
+ private cleanupAudio(): void {
119
  if (this.animationId) {
120
  cancelAnimationFrame(this.animationId);
121
  this.animationId = undefined;
122
  }
123
 
 
124
  if (this.audioContext && this.audioContext.state !== 'closed') {
125
+ this.audioContext.close().catch(err => console.error('Failed to close audio context:', err));
 
 
 
 
126
  }
127
 
128
+ // Clean up audio URLs
129
+ this.messages.forEach(msg => {
130
+ if (msg.audioUrl) {
131
+ URL.revokeObjectURL(msg.audioUrl);
 
 
132
  }
133
  });
 
 
 
 
 
 
 
 
 
 
134
  }
135
 
136
  private checkSTTAvailability(): void {
 
139
  ).subscribe({
140
  next: (env) => {
141
  this.sttAvailable = env.stt_engine !== 'no_stt';
 
142
  if (!this.sttAvailable) {
143
  this.useSTT = false;
144
  }
 
146
  error: (err) => {
147
  console.error('Failed to check STT availability:', err);
148
  this.sttAvailable = false;
 
149
  }
150
  });
151
  }
152
 
153
+ async startRealtimeChat(): Promise<void> {
154
  if (!this.selectedProject) {
155
  this.error = 'Please select a project first';
156
+ this.snackBar.open(this.error, 'Close', { duration: 3000 });
157
  return;
158
  }
159
 
160
  if (!this.sttAvailable) {
161
  this.error = 'STT is not configured. Please configure it in Environment settings.';
162
+ this.snackBar.open(this.error, 'Close', { duration: 5000 });
163
  return;
164
  }
165
 
166
  this.loading = true;
167
  this.error = '';
168
 
 
169
  this.api.startChat(this.selectedProject).pipe(
170
  takeUntil(this.destroyed$)
171
  ).subscribe({
 
180
  this.loading = false;
181
  },
182
  error: (err) => {
183
+ this.error = this.getErrorMessage(err);
184
  this.loading = false;
185
+ this.snackBar.open(this.error, 'Close', {
186
+ duration: 5000,
187
+ panelClass: 'error-snackbar'
188
+ });
189
  }
190
  });
191
  }
192
 
193
+ private async openRealtimeDialog(sessionId: string): Promise<void> {
194
+ try {
195
+ const { default: RealtimeChatComponent } = await import('./realtime-chat.component');
196
+
197
+ const dialogRef = this.dialog.open(RealtimeChatComponent, {
198
  width: '90%',
199
  maxWidth: '900px',
200
  height: '85vh',
 
214
  localStorage.removeItem('current_session_id');
215
  localStorage.removeItem('current_project');
216
 
217
+ // If session was active, end it
218
+ if (result === 'session_active' && sessionId) {
 
219
  this.api.endSession(sessionId).pipe(
220
  takeUntil(this.destroyed$)
221
  ).subscribe({
 
224
  });
225
  }
226
  });
227
+ } catch (error) {
228
+ console.error('Failed to load realtime chat:', error);
229
+ this.snackBar.open('Failed to open realtime chat', 'Close', {
230
+ duration: 3000,
231
+ panelClass: 'error-snackbar'
232
+ });
233
+ }
 
 
 
 
 
234
  }
235
 
236
  loadProjects(): void {
237
  this.loading = true;
238
+ this.error = '';
239
+
240
  this.api.getChatProjects().pipe(
241
  takeUntil(this.destroyed$)
242
  ).subscribe({
 
250
  error: (err) => {
251
  this.error = 'Failed to load projects';
252
  this.loading = false;
253
+ this.snackBar.open(this.error, 'Close', {
254
+ duration: 5000,
255
+ panelClass: 'error-snackbar'
256
+ });
257
  }
258
  });
259
  }
260
 
261
  checkTTSAvailability(): void {
262
+ // Subscribe to environment updates
263
  this.environmentService.environment$.pipe(
264
  takeUntil(this.destroyed$)
265
  ).subscribe(env => {
266
  if (env) {
267
  this.ttsAvailable = env.tts_engine !== 'no_tts';
 
268
  if (!this.ttsAvailable) {
269
  this.useTTS = false;
270
  }
271
  }
272
  });
273
 
274
+ // Get current environment
275
  this.api.getEnvironment().pipe(
276
  takeUntil(this.destroyed$)
277
  ).subscribe({
278
  next: (env) => {
279
  this.ttsAvailable = env.tts_engine !== 'no_tts';
 
280
  if (!this.ttsAvailable) {
281
  this.useTTS = false;
282
  }
 
285
  }
286
 
287
  startChat(): void {
288
+ if (!this.selectedProject) {
289
+ this.snackBar.open('Please select a project', 'Close', { duration: 3000 });
290
+ return;
291
+ }
292
 
293
  this.loading = true;
294
  this.error = '';
 
314
  }
315
  },
316
  error: (err) => {
317
+ this.error = this.getErrorMessage(err);
318
  this.loading = false;
319
+ this.snackBar.open(this.error, 'Close', {
320
+ duration: 5000,
321
+ panelClass: 'error-snackbar'
322
+ });
323
  }
324
  });
325
  }
 
362
  }
363
  },
364
  error: (err) => {
365
+ const errorMsg = this.getErrorMessage(err);
366
  this.messages.push({
367
  author: 'assistant',
368
+ text: '⚠️ ' + errorMsg,
369
  timestamp: new Date()
370
  });
371
  this.loading = false;
372
  this.shouldScroll = true;
 
373
  }
374
  });
375
  }
376
 
377
  generateTTS(text: string, messageIndex: number): void {
378
+ if (!this.ttsAvailable || messageIndex < 0 || messageIndex >= this.messages.length) return;
379
 
380
  this.api.generateTTS(text).pipe(
381
  takeUntil(this.destroyed$)
382
  ).subscribe({
383
  next: (audioBlob) => {
384
  const audioUrl = URL.createObjectURL(audioBlob);
 
385
 
386
+ // Clean up old audio URL if exists
387
+ if (this.messages[messageIndex].audioUrl) {
388
+ URL.revokeObjectURL(this.messages[messageIndex].audioUrl!);
389
+ }
390
+
391
+ this.messages[messageIndex].audioUrl = audioUrl;
392
+
393
+ // Auto-play the latest message
394
+ if (messageIndex === this.messages.length - 1) {
395
+ setTimeout(() => this.playAudio(audioUrl), 100);
396
  }
397
  },
398
  error: (err) => {
399
  console.error('TTS generation error:', err);
400
+ this.snackBar.open('Failed to generate audio', 'Close', {
401
+ duration: 3000,
402
+ panelClass: 'error-snackbar'
403
+ });
404
  }
405
  });
406
  }
407
 
408
  playAudio(audioUrl: string): void {
409
+ if (!this.audioPlayer || !audioUrl) return;
 
 
 
410
 
411
  const audio = this.audioPlayer.nativeElement;
412
+
413
+ // Stop current audio if playing
414
+ if (!audio.paused) {
415
+ audio.pause();
416
+ audio.currentTime = 0;
417
+ }
418
+
419
  audio.src = audioUrl;
420
 
421
+ // Set up audio visualization
422
+ if (this.audioContext && this.audioContext.state !== 'closed') {
423
  this.setupAudioVisualization(audio);
424
  }
425
 
 
427
  this.playingAudio = true;
428
  }).catch(err => {
429
  console.error('Audio play error:', err);
430
+ this.snackBar.open('Failed to play audio', 'Close', {
431
+ duration: 3000,
432
+ panelClass: 'error-snackbar'
433
+ });
434
  });
435
 
436
  audio.onended = () => {
 
444
 
445
  audio.onerror = () => {
446
  this.playingAudio = false;
447
+ console.error('Audio playback error');
448
  };
449
  }
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  setupAudioVisualization(audio: HTMLAudioElement): void {
452
+ if (!this.audioContext || !this.waveformCanvas || this.audioContext.state === 'closed') return;
 
 
 
 
 
 
 
 
 
 
453
 
454
  try {
455
+ // Check if source already exists for this audio element
456
+ if (!(audio as any).audioSource) {
457
+ const source = this.audioContext.createMediaElementSource(audio);
458
+ this.analyser = this.audioContext.createAnalyser();
459
+ this.analyser.fftSize = 256;
460
+
461
+ // Connect nodes
462
+ source.connect(this.analyser);
463
+ this.analyser.connect(this.audioContext.destination);
464
+
465
+ // Store reference to prevent recreation
466
+ (audio as any).audioSource = source;
467
+ }
468
 
469
  // Start visualization
470
  this.drawWaveform();
471
  } catch (error) {
472
+ console.error('Failed to setup audio visualization:', error);
473
  }
474
  }
475
 
476
  drawWaveform(): void {
477
+ if (!this.analyser || !this.waveformCanvas) return;
478
 
479
  const canvas = this.waveformCanvas.nativeElement;
480
  const ctx = canvas.getContext('2d');
 
484
  const dataArray = new Uint8Array(bufferLength);
485
 
486
  const draw = () => {
487
+ if (!this.playingAudio) {
488
+ this.clearWaveform();
489
+ return;
490
+ }
491
 
492
  this.animationId = requestAnimationFrame(draw);
493
 
 
514
  }
515
 
516
  clearWaveform(): void {
517
+ if (!this.waveformCanvas) return;
518
 
519
  const canvas = this.waveformCanvas.nativeElement;
520
  const ctx = canvas.getContext('2d');
 
525
  }
526
 
527
  endSession(): void {
528
+ // Clean up current session
529
  if (this.sessionId) {
 
530
  this.api.endSession(this.sessionId).pipe(
531
  takeUntil(this.destroyed$)
532
  ).subscribe({
533
+ error: (err) => console.error('Failed to end session:', err)
534
  });
535
  }
536
 
537
+ // Clean up audio URLs
538
+ this.messages.forEach(msg => {
539
+ if (msg.audioUrl) {
540
+ URL.revokeObjectURL(msg.audioUrl);
541
+ }
542
+ });
543
+
544
+ // Reset state
545
  this.sessionId = null;
546
  this.messages = [];
547
  this.selectedProject = null;
 
549
  this.error = '';
550
 
551
  // Clean up audio
552
+ if (this.audioPlayer) {
553
+ this.audioPlayer.nativeElement.pause();
554
+ this.audioPlayer.nativeElement.src = '';
555
+ }
556
 
557
+ if (this.animationId) {
558
+ cancelAnimationFrame(this.animationId);
559
+ this.animationId = undefined;
560
+ }
561
+
562
+ this.clearWaveform();
 
 
 
563
  }
564
 
565
  private scrollToBottom(): void {
566
  try {
567
  if (this.myScrollContainer?.nativeElement) {
568
+ const element = this.myScrollContainer.nativeElement;
569
+ element.scrollTop = element.scrollHeight;
570
  }
571
  } catch(err) {
572
  console.error('Scroll error:', err);
573
  }
574
  }
575
+
576
+ private getErrorMessage(error: any): string {
577
+ if (error.status === 0) {
578
+ return 'Unable to connect to server. Please check your connection.';
579
+ } else if (error.status === 401) {
580
+ return 'Session expired. Please login again.';
581
+ } else if (error.status === 403) {
582
+ return 'You do not have permission to use this feature.';
583
+ } else if (error.status === 404) {
584
+ return 'Project or session not found. Please try again.';
585
+ } else if (error.error?.detail) {
586
+ return error.error.detail;
587
+ } else if (error.message) {
588
+ return error.message;
589
+ }
590
+ return 'An unexpected error occurred. Please try again.';
591
+ }
592
  }