ciyidogan commited on
Commit
9fe0a44
·
verified ·
1 Parent(s): e295fed

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

Browse files
flare-ui/src/app/components/chat/chat.component.ts CHANGED
@@ -12,7 +12,7 @@ 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 { Subscription } from 'rxjs';
16
 
17
  import { ApiService } from '../../services/api.service';
18
  import { EnvironmentService } from '../../services/environment.service';
@@ -72,8 +72,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
72
  analyser?: AnalyserNode;
73
  animationId?: number;
74
 
75
- private subs = new Subscription();
 
76
  private shouldScroll = false;
 
77
 
78
  constructor(
79
  private fb: FormBuilder,
@@ -88,8 +90,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
88
  this.checkTTSAvailability();
89
  this.checkSTTAvailability();
90
 
91
- // Initialize Audio Context
92
- this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
 
 
93
  }
94
 
95
  ngAfterViewChecked() {
@@ -99,20 +103,53 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
99
  }
100
  }
101
 
102
- ngOnDestroy(): void {
103
- this.subs.unsubscribe();
 
 
 
 
104
  if (this.animationId) {
105
  cancelAnimationFrame(this.animationId);
 
106
  }
107
- if (this.audioContext) {
108
- this.audioContext.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
  }
111
 
112
  private checkSTTAvailability(): void {
113
- this.api.getEnvironment().subscribe({
 
 
114
  next: (env) => {
115
  this.sttAvailable = env.stt_engine !== 'no_stt';
 
116
  if (!this.sttAvailable) {
117
  this.useSTT = false;
118
  }
@@ -120,6 +157,7 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
120
  error: (err) => {
121
  console.error('Failed to check STT availability:', err);
122
  this.sttAvailable = false;
 
123
  }
124
  });
125
  }
@@ -139,7 +177,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
139
  this.error = '';
140
 
141
  // Start a new session for realtime chat
142
- const sub = this.api.startChat(this.selectedProject).subscribe({
 
 
143
  next: res => {
144
  // Store session ID for realtime component
145
  localStorage.setItem('current_session_id', res.session_id);
@@ -156,13 +196,11 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
156
  console.error('Start realtime chat error:', err);
157
  }
158
  });
159
-
160
- this.subs.add(sub);
161
  }
162
 
163
  private openRealtimeDialog(sessionId: string): void {
164
- // Dinamik import kullan
165
- import('./realtime-chat.component').then(module => {
166
  const dialogRef = this.dialog.open(module.RealtimeChatComponent, {
167
  width: '90%',
168
  maxWidth: '900px',
@@ -176,7 +214,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
176
  }
177
  });
178
 
179
- dialogRef.afterClosed().subscribe(result => {
 
 
180
  // Clean up session data
181
  localStorage.removeItem('current_session_id');
182
  localStorage.removeItem('current_project');
@@ -184,16 +224,22 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
184
  // If session was active, we might want to end it
185
  if (result === 'session_active') {
186
  // Optionally end the session on backend
187
- this.api.endSession(sessionId).subscribe({
 
 
188
  next: () => console.log('Session ended'),
189
  error: (err: any) => console.error('Failed to end session:', err)
190
  });
191
  }
192
  });
 
 
 
 
193
  });
194
  }
195
 
196
- // Alternatif: Route navigasyonu kullanmak isterseniz
197
  private navigateToRealtimeChat(sessionId: string): void {
198
  this.router.navigate(['/realtime-chat', sessionId], {
199
  queryParams: { project: this.selectedProject }
@@ -202,7 +248,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
202
 
203
  loadProjects(): void {
204
  this.loading = true;
205
- const sub = this.api.getChatProjects().subscribe({
 
 
206
  next: projects => {
207
  this.projects = projects;
208
  this.loading = false;
@@ -216,24 +264,29 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
216
  console.error('Load projects error:', err);
217
  }
218
  });
219
- this.subs.add(sub);
220
  }
221
 
222
  checkTTSAvailability(): void {
223
- const sub = this.environmentService.environment$.subscribe(env => {
 
 
 
224
  if (env) {
225
  this.ttsAvailable = env.tts_engine !== 'no_tts';
 
226
  if (!this.ttsAvailable) {
227
  this.useTTS = false;
228
  }
229
  }
230
  });
231
- this.subs.add(sub);
232
 
233
  // Also get current environment
234
- this.api.getEnvironment().subscribe({
 
 
235
  next: (env) => {
236
  this.ttsAvailable = env.tts_engine !== 'no_tts';
 
237
  if (!this.ttsAvailable) {
238
  this.useTTS = false;
239
  }
@@ -247,7 +300,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
247
  this.loading = true;
248
  this.error = '';
249
 
250
- const sub = this.api.startChat(this.selectedProject).subscribe({
 
 
251
  next: res => {
252
  this.sessionId = res.session_id;
253
  const message: ChatMessage = {
@@ -261,7 +316,7 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
261
  this.shouldScroll = true;
262
 
263
  // Generate TTS if enabled
264
- if (this.useTTS) {
265
  this.generateTTS(res.answer, this.messages.length - 1);
266
  }
267
  },
@@ -271,11 +326,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
271
  console.error('Start chat error:', err);
272
  }
273
  });
274
- this.subs.add(sub);
275
  }
276
 
277
  send(): void {
278
- if (!this.sessionId || this.input.invalid) return;
279
 
280
  const text = this.input.value!.trim();
281
  if (!text) return;
@@ -292,7 +346,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
292
  this.shouldScroll = true;
293
 
294
  // Send to backend
295
- const sub = this.api.chat(this.sessionId, text).subscribe({
 
 
296
  next: res => {
297
  const message: ChatMessage = {
298
  author: 'assistant',
@@ -305,7 +361,7 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
305
  this.shouldScroll = true;
306
 
307
  // Generate TTS if enabled
308
- if (this.useTTS) {
309
  this.generateTTS(res.answer, this.messages.length - 1);
310
  }
311
  },
@@ -320,69 +376,116 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
320
  console.error('Chat error:', err);
321
  }
322
  });
323
- this.subs.add(sub);
324
  }
325
 
326
  generateTTS(text: string, messageIndex: number): void {
327
- const sub = this.api.generateTTS(text).subscribe({
 
 
 
 
328
  next: (audioBlob) => {
329
  const audioUrl = URL.createObjectURL(audioBlob);
330
- this.messages[messageIndex].audioUrl = audioUrl;
331
 
332
- // Auto-play the latest message
333
- if (messageIndex === this.messages.length - 1) {
334
- setTimeout(() => this.playAudio(audioUrl), 100);
 
 
 
 
335
  }
336
  },
337
  error: (err) => {
338
  console.error('TTS generation error:', err);
 
339
  }
340
  });
341
- this.subs.add(sub);
342
  }
343
 
344
  playAudio(audioUrl: string): void {
345
- if (!this.audioPlayer) return;
 
 
 
346
 
347
  const audio = this.audioPlayer.nativeElement;
348
  audio.src = audioUrl;
349
 
350
- // Set up audio visualization
351
- this.setupAudioVisualization(audio);
 
 
352
 
353
  audio.play().then(() => {
354
  this.playingAudio = true;
355
  }).catch(err => {
356
  console.error('Audio play error:', err);
 
357
  });
358
 
359
  audio.onended = () => {
360
  this.playingAudio = false;
361
  if (this.animationId) {
362
  cancelAnimationFrame(this.animationId);
 
363
  this.clearWaveform();
364
  }
365
  };
 
 
 
 
 
366
  }
367
 
368
- setupAudioVisualization(audio: HTMLAudioElement): void {
369
- if (!this.audioContext || !this.waveformCanvas) return;
 
 
 
 
 
370
 
371
- // Create audio source and analyser
372
- const source = this.audioContext.createMediaElementSource(audio);
373
- this.analyser = this.audioContext.createAnalyser();
374
- this.analyser.fftSize = 256;
 
 
 
 
375
 
376
- // Connect nodes
377
- source.connect(this.analyser);
378
- this.analyser.connect(this.audioContext.destination);
 
 
 
 
 
 
379
 
380
- // Start visualization
381
- this.drawWaveform();
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
 
384
  drawWaveform(): void {
385
- if (!this.analyser || !this.waveformCanvas) return;
386
 
387
  const canvas = this.waveformCanvas.nativeElement;
388
  const ctx = canvas.getContext('2d');
@@ -392,6 +495,8 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
392
  const dataArray = new Uint8Array(bufferLength);
393
 
394
  const draw = () => {
 
 
395
  this.animationId = requestAnimationFrame(draw);
396
 
397
  this.analyser!.getByteFrequencyData(dataArray);
@@ -417,7 +522,7 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
417
  }
418
 
419
  clearWaveform(): void {
420
- if (!this.waveformCanvas) return;
421
 
422
  const canvas = this.waveformCanvas.nativeElement;
423
  const ctx = canvas.getContext('2d');
@@ -428,6 +533,15 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
428
  }
429
 
430
  endSession(): void {
 
 
 
 
 
 
 
 
 
431
  this.sessionId = null;
432
  this.messages = [];
433
  this.selectedProject = null;
@@ -435,18 +549,23 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
435
  this.error = '';
436
 
437
  // Clean up audio
438
- if (this.audioPlayer) {
439
- this.audioPlayer.nativeElement.pause();
440
- }
441
- if (this.animationId) {
442
- cancelAnimationFrame(this.animationId);
443
- }
444
  this.clearWaveform();
 
 
 
 
 
 
 
 
 
 
445
  }
446
 
447
  private scrollToBottom(): void {
448
  try {
449
- if (this.myScrollContainer) {
450
  this.myScrollContainer.nativeElement.scrollTop =
451
  this.myScrollContainer.nativeElement.scrollHeight;
452
  }
 
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';
18
  import { EnvironmentService } from '../../services/environment.service';
 
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,
 
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
 
99
  ngAfterViewChecked() {
 
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 {
147
+ this.api.getEnvironment().pipe(
148
+ takeUntil(this.destroyed$)
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
  error: (err) => {
158
  console.error('Failed to check STT availability:', err);
159
  this.sttAvailable = false;
160
+ this.environmentService.setSTTEnabled(false);
161
  }
162
  });
163
  }
 
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({
183
  next: res => {
184
  // Store session ID for realtime component
185
  localStorage.setItem('current_session_id', res.session_id);
 
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',
 
214
  }
215
  });
216
 
217
+ dialogRef.afterClosed().pipe(
218
+ takeUntil(this.destroyed$)
219
+ ).subscribe(result => {
220
  // Clean up session data
221
  localStorage.removeItem('current_session_id');
222
  localStorage.removeItem('current_project');
 
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({
230
  next: () => console.log('Session ended'),
231
  error: (err: any) => console.error('Failed to end session:', err)
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 }
 
248
 
249
  loadProjects(): void {
250
  this.loading = true;
251
+ this.api.getChatProjects().pipe(
252
+ takeUntil(this.destroyed$)
253
+ ).subscribe({
254
  next: projects => {
255
  this.projects = projects;
256
  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
  }
 
300
  this.loading = true;
301
  this.error = '';
302
 
303
+ this.api.startChat(this.selectedProject).pipe(
304
+ takeUntil(this.destroyed$)
305
+ ).subscribe({
306
  next: res => {
307
  this.sessionId = res.session_id;
308
  const message: ChatMessage = {
 
316
  this.shouldScroll = true;
317
 
318
  // Generate TTS if enabled
319
+ if (this.useTTS && this.ttsAvailable) {
320
  this.generateTTS(res.answer, this.messages.length - 1);
321
  }
322
  },
 
326
  console.error('Start chat error:', err);
327
  }
328
  });
 
329
  }
330
 
331
  send(): void {
332
+ if (!this.sessionId || this.input.invalid || this.loading) return;
333
 
334
  const text = this.input.value!.trim();
335
  if (!text) return;
 
346
  this.shouldScroll = true;
347
 
348
  // Send to backend
349
+ this.api.chat(this.sessionId, text).pipe(
350
+ takeUntil(this.destroyed$)
351
+ ).subscribe({
352
  next: res => {
353
  const message: ChatMessage = {
354
  author: 'assistant',
 
361
  this.shouldScroll = true;
362
 
363
  // Generate TTS if enabled
364
+ if (this.useTTS && this.ttsAvailable) {
365
  this.generateTTS(res.answer, this.messages.length - 1);
366
  }
367
  },
 
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
 
421
  audio.play().then(() => {
422
  this.playingAudio = true;
423
  }).catch(err => {
424
  console.error('Audio play error:', err);
425
+ this.playingAudio = false;
426
  });
427
 
428
  audio.onended = () => {
429
  this.playingAudio = false;
430
  if (this.animationId) {
431
  cancelAnimationFrame(this.animationId);
432
+ this.animationId = undefined;
433
  this.clearWaveform();
434
  }
435
  };
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
  const dataArray = new Uint8Array(bufferLength);
496
 
497
  const draw = () => {
498
+ if (!this.playingAudio) return;
499
+
500
  this.animationId = requestAnimationFrame(draw);
501
 
502
  this.analyser!.getByteFrequencyData(dataArray);
 
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
  }
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
  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
  }