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

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

Browse files
flare-ui/src/app/components/chat/realtime-chat.component.ts CHANGED
@@ -6,7 +6,10 @@ import { MatIconModule } from '@angular/material/icon';
6
  import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
7
  import { MatDividerModule } from '@angular/material/divider';
8
  import { MatChipsModule } from '@angular/material/chips';
9
- import { Subscription } from 'rxjs';
 
 
 
10
 
11
  import { ConversationManagerService, ConversationState, ConversationMessage } from '../../services/conversation-manager.service';
12
  import { AudioStreamService } from '../../services/audio-stream.service';
@@ -21,7 +24,8 @@ import { AudioStreamService } from '../../services/audio-stream.service';
21
  MatIconModule,
22
  MatProgressSpinnerModule,
23
  MatDividerModule,
24
- MatChipsModule
 
25
  ],
26
  template: `
27
  <mat-card class="realtime-chat-container">
@@ -36,11 +40,23 @@ import { AudioStreamService } from '../../services/audio-stream.service';
36
  </mat-chip>
37
  </mat-chip-listbox>
38
  </mat-card-subtitle>
 
 
 
39
  </mat-card-header>
40
 
41
  <mat-divider></mat-divider>
42
 
43
  <mat-card-content>
 
 
 
 
 
 
 
 
 
44
  <!-- Transcription Display -->
45
  <div class="transcription-area" *ngIf="currentTranscription">
46
  <div class="transcription-label">Dinleniyor...</div>
@@ -49,7 +65,7 @@ import { AudioStreamService } from '../../services/audio-stream.service';
49
 
50
  <!-- Chat Messages -->
51
  <div class="chat-messages" #scrollContainer>
52
- <div *ngFor="let msg of messages"
53
  [class]="'message ' + msg.role">
54
  <mat-icon class="message-icon">
55
  {{ msg.role === 'user' ? 'person' : 'smart_toy' }}
@@ -60,11 +76,18 @@ import { AudioStreamService } from '../../services/audio-stream.service';
60
  <button *ngIf="msg.audioUrl"
61
  mat-icon-button
62
  (click)="playAudio(msg.audioUrl)"
63
- class="audio-button">
64
- <mat-icon>volume_up</mat-icon>
 
65
  </button>
66
  </div>
67
  </div>
 
 
 
 
 
 
68
  </div>
69
 
70
  <!-- Audio Visualizer -->
@@ -80,9 +103,13 @@ import { AudioStreamService } from '../../services/audio-stream.service';
80
  <button mat-raised-button
81
  color="primary"
82
  (click)="toggleConversation()"
83
- [disabled]="!sessionId">
84
- <mat-icon>{{ isConversationActive ? 'stop' : 'mic' }}</mat-icon>
85
- {{ isConversationActive ? 'Konuşmayı Bitir' : 'Konuşmaya Başla' }}
 
 
 
 
86
  </button>
87
 
88
  <button mat-button
@@ -91,6 +118,13 @@ import { AudioStreamService } from '../../services/audio-stream.service';
91
  <mat-icon>clear</mat-icon>
92
  Temizle
93
  </button>
 
 
 
 
 
 
 
94
  </mat-card-actions>
95
  </mat-card>
96
  `,
@@ -101,6 +135,38 @@ import { AudioStreamService } from '../../services/audio-stream.service';
101
  height: 80vh;
102
  display: flex;
103
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
 
106
  .transcription-area {
@@ -109,6 +175,13 @@ import { AudioStreamService } from '../../services/audio-stream.service';
109
  border-radius: 8px;
110
  margin-bottom: 16px;
111
  min-height: 60px;
 
 
 
 
 
 
 
112
  }
113
 
114
  .transcription-label {
@@ -129,6 +202,8 @@ import { AudioStreamService } from '../../services/audio-stream.service';
129
  padding: 16px;
130
  background: #fafafa;
131
  border-radius: 8px;
 
 
132
  }
133
 
134
  .message {
@@ -164,6 +239,7 @@ import { AudioStreamService } from '../../services/audio-stream.service';
164
  padding: 12px 16px;
165
  border-radius: 12px;
166
  box-shadow: 0 1px 2px rgba(0,0,0,0.1);
 
167
  }
168
 
169
  .message.user .message-content {
@@ -184,6 +260,19 @@ import { AudioStreamService } from '../../services/audio-stream.service';
184
  margin-top: 8px;
185
  }
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  .audio-visualizer {
188
  width: 100%;
189
  height: 100px;
@@ -209,7 +298,14 @@ import { AudioStreamService } from '../../services/audio-stream.service';
209
 
210
  mat-card-actions {
211
  padding: 16px;
212
- justify-content: space-between;
 
 
 
 
 
 
 
213
  }
214
  `]
215
  })
@@ -218,56 +314,72 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
218
  @ViewChild('audioVisualizer') private audioVisualizer!: ElementRef<HTMLCanvasElement>;
219
 
220
  sessionId: string | null = null;
 
221
  isConversationActive = false;
222
  isRecording = false;
 
223
  currentState: ConversationState = 'idle';
224
  currentTranscription = '';
225
  messages: ConversationMessage[] = [];
 
 
226
 
227
  conversationStates: ConversationState[] = [
228
  'idle', 'listening', 'processing_stt', 'processing_llm', 'processing_tts', 'playing_audio'
229
  ];
230
 
231
- private subscriptions = new Subscription();
232
  private shouldScrollToBottom = false;
233
  private animationId: number | null = null;
 
234
 
235
  constructor(
236
  private conversationManager: ConversationManagerService,
237
- private audioService: AudioStreamService
238
- ) {}
 
 
 
 
 
 
239
 
240
  ngOnInit(): void {
241
- // Get session ID from parent component or service
242
- this.sessionId = localStorage.getItem('current_session_id');
 
 
 
 
 
 
 
 
 
 
243
 
244
  // Subscribe to conversation state
245
- this.subscriptions.add(
246
- this.conversationManager.currentState$.subscribe(state => {
247
- this.currentState = state;
248
- this.updateRecordingState(state);
249
- })
250
- );
251
 
252
  // Subscribe to messages
253
- this.subscriptions.add(
254
- this.conversationManager.messages$.subscribe(messages => {
255
- this.messages = messages;
256
- this.shouldScrollToBottom = true;
257
- })
258
- );
259
 
260
  // Subscribe to transcription
261
- this.subscriptions.add(
262
- this.conversationManager.transcription$.subscribe(text => {
263
- this.currentTranscription = text;
264
- })
265
- );
266
-
267
- // Check browser support
268
- if (!AudioStreamService.checkBrowserSupport()) {
269
- alert('Tarayıcınız ses kaydını desteklemiyor. Lütfen modern bir tarayıcı kullanın.');
270
- }
271
  }
272
 
273
  ngAfterViewChecked(): void {
@@ -278,8 +390,11 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
278
  }
279
 
280
  ngOnDestroy(): void {
281
- this.subscriptions.unsubscribe();
 
282
  this.stopVisualization();
 
 
283
  if (this.isConversationActive) {
284
  this.conversationManager.stopConversation();
285
  }
@@ -289,29 +404,99 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
289
  if (!this.sessionId) return;
290
 
291
  if (this.isConversationActive) {
292
- this.conversationManager.stopConversation();
293
- this.isConversationActive = false;
294
- this.stopVisualization();
295
  } else {
296
- try {
297
- await this.conversationManager.startConversation(this.sessionId);
298
- this.isConversationActive = true;
299
- this.startVisualization();
300
- } catch (error) {
301
- console.error('Failed to start conversation:', error);
302
- alert('Konuşma başlatılamadı. Lütfen tekrar deneyin.');
303
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
  }
306
 
307
  clearChat(): void {
308
  this.conversationManager.clearMessages();
309
  this.currentTranscription = '';
 
310
  }
311
 
312
- playAudio(audioUrl: string): void {
313
- const audio = new Audio(audioUrl);
314
- audio.play();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  }
316
 
317
  getStateLabel(state: ConversationState): string {
@@ -326,15 +511,43 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
326
  return labels[state] || state;
327
  }
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  private updateRecordingState(state: ConversationState): void {
330
  this.isRecording = state === 'listening';
331
  }
332
 
333
  private scrollToBottom(): void {
334
  try {
335
- this.scrollContainer.nativeElement.scrollTop =
336
- this.scrollContainer.nativeElement.scrollHeight;
337
- } catch(err) {}
 
 
 
 
338
  }
339
 
340
  private startVisualization(): void {
@@ -344,27 +557,50 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
344
  const ctx = canvas.getContext('2d');
345
  if (!ctx) return;
346
 
347
- // Simple visualization animation
348
- const draw = () => {
 
 
 
 
 
349
  ctx.fillStyle = '#333';
350
  ctx.fillRect(0, 0, canvas.width, canvas.height);
351
 
352
- // Draw random bars for demo
353
- ctx.fillStyle = '#4caf50';
354
- const barCount = 50;
355
- const barWidth = canvas.width / barCount;
356
-
357
- for (let i = 0; i < barCount; i++) {
358
- const barHeight = Math.random() * canvas.height * 0.8;
359
- const x = i * barWidth;
360
- const y = canvas.height - barHeight;
361
-
362
- ctx.fillRect(x, y, barWidth - 2, barHeight);
363
- }
364
-
365
  if (this.isRecording) {
366
- this.animationId = requestAnimationFrame(draw);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
 
 
368
  };
369
 
370
  draw();
@@ -376,14 +612,25 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
376
  this.animationId = null;
377
  }
378
 
379
- // Clear canvas
380
- if (this.audioVisualizer) {
381
- const canvas = this.audioVisualizer.nativeElement;
382
- const ctx = canvas.getContext('2d');
383
- if (ctx) {
384
- ctx.fillStyle = '#333';
385
- ctx.fillRect(0, 0, canvas.width, canvas.height);
386
- }
 
 
 
 
 
 
 
 
 
 
 
387
  }
388
  }
389
  }
 
6
  import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
7
  import { MatDividerModule } from '@angular/material/divider';
8
  import { MatChipsModule } from '@angular/material/chips';
9
+ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
10
+ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
11
+ import { Inject } from '@angular/core';
12
+ import { Subject, takeUntil } from 'rxjs';
13
 
14
  import { ConversationManagerService, ConversationState, ConversationMessage } from '../../services/conversation-manager.service';
15
  import { AudioStreamService } from '../../services/audio-stream.service';
 
24
  MatIconModule,
25
  MatProgressSpinnerModule,
26
  MatDividerModule,
27
+ MatChipsModule,
28
+ MatSnackBarModule
29
  ],
30
  template: `
31
  <mat-card class="realtime-chat-container">
 
40
  </mat-chip>
41
  </mat-chip-listbox>
42
  </mat-card-subtitle>
43
+ <button mat-icon-button class="close-button" (click)="closeDialog()">
44
+ <mat-icon>close</mat-icon>
45
+ </button>
46
  </mat-card-header>
47
 
48
  <mat-divider></mat-divider>
49
 
50
  <mat-card-content>
51
+ <!-- Error State -->
52
+ <div class="error-banner" *ngIf="error">
53
+ <mat-icon>error_outline</mat-icon>
54
+ <span>{{ error }}</span>
55
+ <button mat-icon-button (click)="retryConnection()">
56
+ <mat-icon>refresh</mat-icon>
57
+ </button>
58
+ </div>
59
+
60
  <!-- Transcription Display -->
61
  <div class="transcription-area" *ngIf="currentTranscription">
62
  <div class="transcription-label">Dinleniyor...</div>
 
65
 
66
  <!-- Chat Messages -->
67
  <div class="chat-messages" #scrollContainer>
68
+ <div *ngFor="let msg of messages; trackBy: trackByIndex"
69
  [class]="'message ' + msg.role">
70
  <mat-icon class="message-icon">
71
  {{ msg.role === 'user' ? 'person' : 'smart_toy' }}
 
76
  <button *ngIf="msg.audioUrl"
77
  mat-icon-button
78
  (click)="playAudio(msg.audioUrl)"
79
+ class="audio-button"
80
+ [disabled]="isPlayingAudio">
81
+ <mat-icon>{{ isPlayingAudio ? 'stop' : 'volume_up' }}</mat-icon>
82
  </button>
83
  </div>
84
  </div>
85
+
86
+ <!-- Empty State -->
87
+ <div class="empty-state" *ngIf="messages.length === 0 && !isConversationActive">
88
+ <mat-icon>mic_off</mat-icon>
89
+ <p>Konuşmaya başlamak için aşağıdaki butona tıklayın</p>
90
+ </div>
91
  </div>
92
 
93
  <!-- Audio Visualizer -->
 
103
  <button mat-raised-button
104
  color="primary"
105
  (click)="toggleConversation()"
106
+ [disabled]="!sessionId || loading">
107
+ @if (loading) {
108
+ <mat-spinner diameter="20"></mat-spinner>
109
+ } @else {
110
+ <mat-icon>{{ isConversationActive ? 'stop' : 'mic' }}</mat-icon>
111
+ {{ isConversationActive ? 'Konuşmayı Bitir' : 'Konuşmaya Başla' }}
112
+ }
113
  </button>
114
 
115
  <button mat-button
 
118
  <mat-icon>clear</mat-icon>
119
  Temizle
120
  </button>
121
+
122
+ <button mat-button
123
+ (click)="performBargeIn()"
124
+ [disabled]="!isConversationActive || currentState === 'idle' || currentState === 'listening'">
125
+ <mat-icon>pan_tool</mat-icon>
126
+ Kesme (Barge-in)
127
+ </button>
128
  </mat-card-actions>
129
  </mat-card>
130
  `,
 
135
  height: 80vh;
136
  display: flex;
137
  flex-direction: column;
138
+ position: relative;
139
+ }
140
+
141
+ mat-card-header {
142
+ position: relative;
143
+
144
+ .close-button {
145
+ position: absolute;
146
+ top: 8px;
147
+ right: 8px;
148
+ }
149
+ }
150
+
151
+ .error-banner {
152
+ background-color: #ffebee;
153
+ color: #c62828;
154
+ padding: 12px;
155
+ border-radius: 4px;
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 8px;
159
+ margin-bottom: 16px;
160
+
161
+ mat-icon {
162
+ font-size: 20px;
163
+ width: 20px;
164
+ height: 20px;
165
+ }
166
+
167
+ span {
168
+ flex: 1;
169
+ }
170
  }
171
 
172
  .transcription-area {
 
175
  border-radius: 8px;
176
  margin-bottom: 16px;
177
  min-height: 60px;
178
+ animation: pulse 2s infinite;
179
+ }
180
+
181
+ @keyframes pulse {
182
+ 0% { opacity: 1; }
183
+ 50% { opacity: 0.7; }
184
+ 100% { opacity: 1; }
185
  }
186
 
187
  .transcription-label {
 
202
  padding: 16px;
203
  background: #fafafa;
204
  border-radius: 8px;
205
+ min-height: 200px;
206
+ max-height: 400px;
207
  }
208
 
209
  .message {
 
239
  padding: 12px 16px;
240
  border-radius: 12px;
241
  box-shadow: 0 1px 2px rgba(0,0,0,0.1);
242
+ position: relative;
243
  }
244
 
245
  .message.user .message-content {
 
260
  margin-top: 8px;
261
  }
262
 
263
+ .empty-state {
264
+ text-align: center;
265
+ padding: 60px 20px;
266
+ color: #999;
267
+
268
+ mat-icon {
269
+ font-size: 48px;
270
+ width: 48px;
271
+ height: 48px;
272
+ margin-bottom: 16px;
273
+ }
274
+ }
275
+
276
  .audio-visualizer {
277
  width: 100%;
278
  height: 100px;
 
298
 
299
  mat-card-actions {
300
  padding: 16px;
301
+ display: flex;
302
+ gap: 16px;
303
+ justify-content: flex-start;
304
+
305
+ mat-spinner {
306
+ display: inline-block;
307
+ margin-right: 8px;
308
+ }
309
  }
310
  `]
311
  })
 
314
  @ViewChild('audioVisualizer') private audioVisualizer!: ElementRef<HTMLCanvasElement>;
315
 
316
  sessionId: string | null = null;
317
+ projectName: string | null = null;
318
  isConversationActive = false;
319
  isRecording = false;
320
+ isPlayingAudio = false;
321
  currentState: ConversationState = 'idle';
322
  currentTranscription = '';
323
  messages: ConversationMessage[] = [];
324
+ error = '';
325
+ loading = false;
326
 
327
  conversationStates: ConversationState[] = [
328
  'idle', 'listening', 'processing_stt', 'processing_llm', 'processing_tts', 'playing_audio'
329
  ];
330
 
331
+ private destroyed$ = new Subject<void>();
332
  private shouldScrollToBottom = false;
333
  private animationId: number | null = null;
334
+ private currentAudio: HTMLAudioElement | null = null;
335
 
336
  constructor(
337
  private conversationManager: ConversationManagerService,
338
+ private audioService: AudioStreamService,
339
+ private snackBar: MatSnackBar,
340
+ public dialogRef: MatDialogRef<RealtimeChatComponent>,
341
+ @Inject(MAT_DIALOG_DATA) public data: { sessionId: string; projectName?: string }
342
+ ) {
343
+ this.sessionId = data.sessionId;
344
+ this.projectName = data.projectName || null;
345
+ }
346
 
347
  ngOnInit(): void {
348
+ // Check browser support
349
+ if (!AudioStreamService.checkBrowserSupport()) {
350
+ this.error = 'Tarayıcınız ses kaydını desteklemiyor. Lütfen modern bir tarayıcı kullanın.';
351
+ this.snackBar.open(this.error, 'Close', {
352
+ duration: 5000,
353
+ panelClass: 'error-snackbar'
354
+ });
355
+ return;
356
+ }
357
+
358
+ // Check microphone permission
359
+ this.checkMicrophonePermission();
360
 
361
  // Subscribe to conversation state
362
+ this.conversationManager.currentState$.pipe(
363
+ takeUntil(this.destroyed$)
364
+ ).subscribe(state => {
365
+ this.currentState = state;
366
+ this.updateRecordingState(state);
367
+ });
368
 
369
  // Subscribe to messages
370
+ this.conversationManager.messages$.pipe(
371
+ takeUntil(this.destroyed$)
372
+ ).subscribe(messages => {
373
+ this.messages = messages;
374
+ this.shouldScrollToBottom = true;
375
+ });
376
 
377
  // Subscribe to transcription
378
+ this.conversationManager.transcription$.pipe(
379
+ takeUntil(this.destroyed$)
380
+ ).subscribe(text => {
381
+ this.currentTranscription = text;
382
+ });
 
 
 
 
 
383
  }
384
 
385
  ngAfterViewChecked(): void {
 
390
  }
391
 
392
  ngOnDestroy(): void {
393
+ this.destroyed$.next();
394
+ this.destroyed$.complete();
395
  this.stopVisualization();
396
+ this.cleanupAudio();
397
+
398
  if (this.isConversationActive) {
399
  this.conversationManager.stopConversation();
400
  }
 
404
  if (!this.sessionId) return;
405
 
406
  if (this.isConversationActive) {
407
+ this.stopConversation();
 
 
408
  } else {
409
+ await this.startConversation();
410
+ }
411
+ }
412
+
413
+ private async startConversation(): Promise<void> {
414
+ try {
415
+ this.loading = true;
416
+ this.error = '';
417
+
418
+ await this.conversationManager.startConversation(this.sessionId!);
419
+ this.isConversationActive = true;
420
+ this.startVisualization();
421
+
422
+ this.snackBar.open('Konuşma başlatıldı', 'Close', {
423
+ duration: 2000
424
+ });
425
+ } catch (error: any) {
426
+ console.error('Failed to start conversation:', error);
427
+ this.error = 'Konuşma başlatılamadı. Lütfen tekrar deneyin.';
428
+ this.snackBar.open(this.error, 'Close', {
429
+ duration: 5000,
430
+ panelClass: 'error-snackbar'
431
+ });
432
+ } finally {
433
+ this.loading = false;
434
+ }
435
+ }
436
+
437
+ private stopConversation(): void {
438
+ this.conversationManager.stopConversation();
439
+ this.isConversationActive = false;
440
+ this.stopVisualization();
441
+
442
+ this.snackBar.open('Konuşma sonlandırıldı', 'Close', {
443
+ duration: 2000
444
+ });
445
+ }
446
+
447
+ async retryConnection(): Promise<void> {
448
+ this.error = '';
449
+ if (!this.isConversationActive && this.sessionId) {
450
+ await this.startConversation();
451
  }
452
  }
453
 
454
  clearChat(): void {
455
  this.conversationManager.clearMessages();
456
  this.currentTranscription = '';
457
+ this.error = '';
458
  }
459
 
460
+ performBargeIn(): void {
461
+ this.conversationManager.performBargeIn();
462
+ this.snackBar.open('Kesme yapıldı', 'Close', {
463
+ duration: 1000
464
+ });
465
+ }
466
+
467
+ playAudio(audioUrl?: string): void {
468
+ if (!audioUrl) return;
469
+
470
+ // Stop current audio if playing
471
+ if (this.currentAudio) {
472
+ this.currentAudio.pause();
473
+ this.currentAudio = null;
474
+ this.isPlayingAudio = false;
475
+ return;
476
+ }
477
+
478
+ this.currentAudio = new Audio(audioUrl);
479
+ this.isPlayingAudio = true;
480
+
481
+ this.currentAudio.play().catch(error => {
482
+ console.error('Audio playback error:', error);
483
+ this.isPlayingAudio = false;
484
+ this.currentAudio = null;
485
+ });
486
+
487
+ this.currentAudio.onended = () => {
488
+ this.isPlayingAudio = false;
489
+ this.currentAudio = null;
490
+ };
491
+
492
+ this.currentAudio.onerror = () => {
493
+ this.isPlayingAudio = false;
494
+ this.currentAudio = null;
495
+ this.snackBar.open('Ses çalınamadı', 'Close', {
496
+ duration: 2000,
497
+ panelClass: 'error-snackbar'
498
+ });
499
+ };
500
  }
501
 
502
  getStateLabel(state: ConversationState): string {
 
511
  return labels[state] || state;
512
  }
513
 
514
+ closeDialog(): void {
515
+ const result = this.isConversationActive ? 'session_active' : 'closed';
516
+ this.dialogRef.close(result);
517
+ }
518
+
519
+ trackByIndex(index: number): number {
520
+ return index;
521
+ }
522
+
523
+ private async checkMicrophonePermission(): Promise<void> {
524
+ try {
525
+ const permission = await this.audioService.checkMicrophonePermission();
526
+ if (permission === 'denied') {
527
+ this.error = 'Mikrofon erişimi reddedildi. Lütfen tarayıcı ayarlarından izin verin.';
528
+ this.snackBar.open(this.error, 'Close', {
529
+ duration: 5000,
530
+ panelClass: 'error-snackbar'
531
+ });
532
+ }
533
+ } catch (error) {
534
+ console.error('Failed to check microphone permission:', error);
535
+ }
536
+ }
537
+
538
  private updateRecordingState(state: ConversationState): void {
539
  this.isRecording = state === 'listening';
540
  }
541
 
542
  private scrollToBottom(): void {
543
  try {
544
+ if (this.scrollContainer?.nativeElement) {
545
+ const element = this.scrollContainer.nativeElement;
546
+ element.scrollTop = element.scrollHeight;
547
+ }
548
+ } catch(err) {
549
+ console.error('Scroll error:', err);
550
+ }
551
  }
552
 
553
  private startVisualization(): void {
 
557
  const ctx = canvas.getContext('2d');
558
  if (!ctx) return;
559
 
560
+ // Get volume level and visualize
561
+ const draw = async () => {
562
+ if (!this.isConversationActive) {
563
+ this.clearVisualization();
564
+ return;
565
+ }
566
+
567
  ctx.fillStyle = '#333';
568
  ctx.fillRect(0, 0, canvas.width, canvas.height);
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  if (this.isRecording) {
571
+ // Get actual volume level
572
+ try {
573
+ const volume = await this.audioService.getVolumeLevel();
574
+
575
+ // Draw volume bars
576
+ ctx.fillStyle = '#4caf50';
577
+ const barCount = 50;
578
+ const barWidth = canvas.width / barCount;
579
+
580
+ for (let i = 0; i < barCount; i++) {
581
+ const barHeight = Math.random() * volume * canvas.height;
582
+ const x = i * barWidth;
583
+ const y = canvas.height - barHeight;
584
+
585
+ ctx.fillRect(x, y, barWidth - 2, barHeight);
586
+ }
587
+ } catch (error) {
588
+ // Fallback to random visualization
589
+ ctx.fillStyle = '#4caf50';
590
+ const barCount = 50;
591
+ const barWidth = canvas.width / barCount;
592
+
593
+ for (let i = 0; i < barCount; i++) {
594
+ const barHeight = Math.random() * canvas.height * 0.8;
595
+ const x = i * barWidth;
596
+ const y = canvas.height - barHeight;
597
+
598
+ ctx.fillRect(x, y, barWidth - 2, barHeight);
599
+ }
600
+ }
601
  }
602
+
603
+ this.animationId = requestAnimationFrame(draw);
604
  };
605
 
606
  draw();
 
612
  this.animationId = null;
613
  }
614
 
615
+ this.clearVisualization();
616
+ }
617
+
618
+ private clearVisualization(): void {
619
+ if (!this.audioVisualizer) return;
620
+
621
+ const canvas = this.audioVisualizer.nativeElement;
622
+ const ctx = canvas.getContext('2d');
623
+ if (ctx) {
624
+ ctx.fillStyle = '#333';
625
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
626
+ }
627
+ }
628
+
629
+ private cleanupAudio(): void {
630
+ if (this.currentAudio) {
631
+ this.currentAudio.pause();
632
+ this.currentAudio = null;
633
+ this.isPlayingAudio = false;
634
  }
635
  }
636
  }