ciyidogan commited on
Commit
c166c74
·
verified ·
1 Parent(s): b4cf038

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

Browse files
flare-ui/src/app/components/chat/chat.component.ts CHANGED
@@ -10,14 +10,17 @@ import { MatSelectModule } from '@angular/material/select';
10
  import { MatDividerModule } from '@angular/material/divider';
11
  import { MatTooltipModule } from '@angular/material/tooltip';
12
  import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 
13
  import { Subscription } from 'rxjs';
14
 
15
  import { ApiService } from '../../services/api.service';
 
16
 
17
  interface ChatMessage {
18
  author: 'user' | 'assistant';
19
  text: string;
20
  timestamp?: Date;
 
21
  }
22
 
23
  @Component({
@@ -35,16 +38,21 @@ interface ChatMessage {
35
  MatSelectModule,
36
  MatDividerModule,
37
  MatTooltipModule,
38
- MatProgressSpinnerModule
 
39
  ],
40
  templateUrl: './chat.component.html',
41
  styleUrls: ['./chat.component.scss']
42
  })
43
  export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
44
  @ViewChild('scrollMe') private myScrollContainer!: ElementRef;
 
 
45
 
46
  projects: string[] = [];
47
- selectedProject: string | null = null; // Düzeltildi: ! kaldırıldı, null değer atandı
 
 
48
 
49
  sessionId: string | null = null;
50
  messages: ChatMessage[] = [];
@@ -52,17 +60,28 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
52
 
53
  loading = false;
54
  error = '';
 
 
 
 
 
 
55
 
56
  private subs = new Subscription();
57
  private shouldScroll = false;
58
 
59
  constructor(
60
  private fb: FormBuilder,
61
- private api: ApiService
 
62
  ) {}
63
 
64
  ngOnInit(): void {
65
  this.loadProjects();
 
 
 
 
66
  }
67
 
68
  ngAfterViewChecked() {
@@ -74,6 +93,12 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
74
 
75
  ngOnDestroy(): void {
76
  this.subs.unsubscribe();
 
 
 
 
 
 
77
  }
78
 
79
  loadProjects(): void {
@@ -95,6 +120,28 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
95
  this.subs.add(sub);
96
  }
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  startChat(): void {
99
  if (!this.selectedProject) return;
100
 
@@ -104,13 +151,20 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
104
  const sub = this.api.startChat(this.selectedProject).subscribe({
105
  next: res => {
106
  this.sessionId = res.session_id;
107
- this.messages = [{
108
  author: 'assistant',
109
  text: res.answer,
110
  timestamp: new Date()
111
- }];
 
 
112
  this.loading = false;
113
  this.shouldScroll = true;
 
 
 
 
 
114
  },
115
  error: (err) => {
116
  this.error = err.error?.detail || 'Failed to start session';
@@ -141,13 +195,20 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
141
  // Send to backend
142
  const sub = this.api.chat(this.sessionId, text).subscribe({
143
  next: res => {
144
- this.messages.push({
145
  author: 'assistant',
146
  text: res.answer,
147
  timestamp: new Date()
148
- });
 
 
149
  this.loading = false;
150
  this.shouldScroll = true;
 
 
 
 
 
151
  },
152
  error: (err) => {
153
  this.messages.push({
@@ -163,12 +224,125 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
163
  this.subs.add(sub);
164
  }
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  endSession(): void {
167
  this.sessionId = null;
168
  this.messages = [];
169
  this.selectedProject = null;
170
  this.input.reset();
171
  this.error = '';
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
  private scrollToBottom(): void {
 
10
  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 { Subscription } from 'rxjs';
15
 
16
  import { ApiService } from '../../services/api.service';
17
+ import { EnvironmentService } from '../../services/environment.service';
18
 
19
  interface ChatMessage {
20
  author: 'user' | 'assistant';
21
  text: string;
22
  timestamp?: Date;
23
+ audioUrl?: string; // For TTS audio
24
  }
25
 
26
  @Component({
 
38
  MatSelectModule,
39
  MatDividerModule,
40
  MatTooltipModule,
41
+ MatProgressSpinnerModule,
42
+ MatCheckboxModule
43
  ],
44
  templateUrl: './chat.component.html',
45
  styleUrls: ['./chat.component.scss']
46
  })
47
  export class ChatComponent implements OnInit, OnDestroy, AfterViewChecked {
48
  @ViewChild('scrollMe') private myScrollContainer!: ElementRef;
49
+ @ViewChild('audioPlayer') private audioPlayer!: ElementRef<HTMLAudioElement>;
50
+ @ViewChild('waveformCanvas') private waveformCanvas!: ElementRef<HTMLCanvasElement>;
51
 
52
  projects: string[] = [];
53
+ selectedProject: string | null = null;
54
+ useTTS = false;
55
+ ttsAvailable = false;
56
 
57
  sessionId: string | null = null;
58
  messages: ChatMessage[] = [];
 
60
 
61
  loading = false;
62
  error = '';
63
+ playingAudio = false;
64
+
65
+ // Audio visualization
66
+ audioContext?: AudioContext;
67
+ analyser?: AnalyserNode;
68
+ animationId?: number;
69
 
70
  private subs = new Subscription();
71
  private shouldScroll = false;
72
 
73
  constructor(
74
  private fb: FormBuilder,
75
+ private api: ApiService,
76
+ private environmentService: EnvironmentService
77
  ) {}
78
 
79
  ngOnInit(): void {
80
  this.loadProjects();
81
+ this.checkTTSAvailability();
82
+
83
+ // Initialize Audio Context
84
+ this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
85
  }
86
 
87
  ngAfterViewChecked() {
 
93
 
94
  ngOnDestroy(): void {
95
  this.subs.unsubscribe();
96
+ if (this.animationId) {
97
+ cancelAnimationFrame(this.animationId);
98
+ }
99
+ if (this.audioContext) {
100
+ this.audioContext.close();
101
+ }
102
  }
103
 
104
  loadProjects(): void {
 
120
  this.subs.add(sub);
121
  }
122
 
123
+ checkTTSAvailability(): void {
124
+ const sub = this.environmentService.environment$.subscribe(env => {
125
+ if (env) {
126
+ this.ttsAvailable = env.tts_engine !== 'no_tts';
127
+ if (!this.ttsAvailable) {
128
+ this.useTTS = false;
129
+ }
130
+ }
131
+ });
132
+ this.subs.add(sub);
133
+
134
+ // Also get current environment
135
+ this.api.getEnvironment().subscribe({
136
+ next: (env) => {
137
+ this.ttsAvailable = env.tts_engine !== 'no_tts';
138
+ if (!this.ttsAvailable) {
139
+ this.useTTS = false;
140
+ }
141
+ }
142
+ });
143
+ }
144
+
145
  startChat(): void {
146
  if (!this.selectedProject) return;
147
 
 
151
  const sub = this.api.startChat(this.selectedProject).subscribe({
152
  next: res => {
153
  this.sessionId = res.session_id;
154
+ const message: ChatMessage = {
155
  author: 'assistant',
156
  text: res.answer,
157
  timestamp: new Date()
158
+ };
159
+
160
+ this.messages = [message];
161
  this.loading = false;
162
  this.shouldScroll = true;
163
+
164
+ // Generate TTS if enabled
165
+ if (this.useTTS) {
166
+ this.generateTTS(res.answer, this.messages.length - 1);
167
+ }
168
  },
169
  error: (err) => {
170
  this.error = err.error?.detail || 'Failed to start session';
 
195
  // Send to backend
196
  const sub = this.api.chat(this.sessionId, text).subscribe({
197
  next: res => {
198
+ const message: ChatMessage = {
199
  author: 'assistant',
200
  text: res.answer,
201
  timestamp: new Date()
202
+ };
203
+
204
+ this.messages.push(message);
205
  this.loading = false;
206
  this.shouldScroll = true;
207
+
208
+ // Generate TTS if enabled
209
+ if (this.useTTS) {
210
+ this.generateTTS(res.answer, this.messages.length - 1);
211
+ }
212
  },
213
  error: (err) => {
214
  this.messages.push({
 
224
  this.subs.add(sub);
225
  }
226
 
227
+ generateTTS(text: string, messageIndex: number): void {
228
+ const sub = this.api.generateTTS(text).subscribe({
229
+ next: (audioBlob) => {
230
+ const audioUrl = URL.createObjectURL(audioBlob);
231
+ this.messages[messageIndex].audioUrl = audioUrl;
232
+
233
+ // Auto-play the latest message
234
+ if (messageIndex === this.messages.length - 1) {
235
+ setTimeout(() => this.playAudio(audioUrl), 100);
236
+ }
237
+ },
238
+ error: (err) => {
239
+ console.error('TTS generation error:', err);
240
+ }
241
+ });
242
+ this.subs.add(sub);
243
+ }
244
+
245
+ playAudio(audioUrl: string): void {
246
+ if (!this.audioPlayer) return;
247
+
248
+ const audio = this.audioPlayer.nativeElement;
249
+ audio.src = audioUrl;
250
+
251
+ // Set up audio visualization
252
+ this.setupAudioVisualization(audio);
253
+
254
+ audio.play().then(() => {
255
+ this.playingAudio = true;
256
+ }).catch(err => {
257
+ console.error('Audio play error:', err);
258
+ });
259
+
260
+ audio.onended = () => {
261
+ this.playingAudio = false;
262
+ if (this.animationId) {
263
+ cancelAnimationFrame(this.animationId);
264
+ this.clearWaveform();
265
+ }
266
+ };
267
+ }
268
+
269
+ setupAudioVisualization(audio: HTMLAudioElement): void {
270
+ if (!this.audioContext || !this.waveformCanvas) return;
271
+
272
+ // Create audio source and analyser
273
+ const source = this.audioContext.createMediaElementSource(audio);
274
+ this.analyser = this.audioContext.createAnalyser();
275
+ this.analyser.fftSize = 256;
276
+
277
+ // Connect nodes
278
+ source.connect(this.analyser);
279
+ this.analyser.connect(this.audioContext.destination);
280
+
281
+ // Start visualization
282
+ this.drawWaveform();
283
+ }
284
+
285
+ drawWaveform(): void {
286
+ if (!this.analyser || !this.waveformCanvas) return;
287
+
288
+ const canvas = this.waveformCanvas.nativeElement;
289
+ const ctx = canvas.getContext('2d');
290
+ if (!ctx) return;
291
+
292
+ const bufferLength = this.analyser.frequencyBinCount;
293
+ const dataArray = new Uint8Array(bufferLength);
294
+
295
+ const draw = () => {
296
+ this.animationId = requestAnimationFrame(draw);
297
+
298
+ this.analyser!.getByteFrequencyData(dataArray);
299
+
300
+ ctx.fillStyle = 'rgb(240, 240, 240)';
301
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
302
+
303
+ const barWidth = (canvas.width / bufferLength) * 2.5;
304
+ let barHeight;
305
+ let x = 0;
306
+
307
+ for (let i = 0; i < bufferLength; i++) {
308
+ barHeight = (dataArray[i] / 255) * canvas.height * 0.8;
309
+
310
+ ctx.fillStyle = `rgb(63, 81, 181)`;
311
+ ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
312
+
313
+ x += barWidth + 1;
314
+ }
315
+ };
316
+
317
+ draw();
318
+ }
319
+
320
+ clearWaveform(): void {
321
+ if (!this.waveformCanvas) return;
322
+
323
+ const canvas = this.waveformCanvas.nativeElement;
324
+ const ctx = canvas.getContext('2d');
325
+ if (!ctx) return;
326
+
327
+ ctx.fillStyle = 'rgb(240, 240, 240)';
328
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
329
+ }
330
+
331
  endSession(): void {
332
  this.sessionId = null;
333
  this.messages = [];
334
  this.selectedProject = null;
335
  this.input.reset();
336
  this.error = '';
337
+
338
+ // Clean up audio
339
+ if (this.audioPlayer) {
340
+ this.audioPlayer.nativeElement.pause();
341
+ }
342
+ if (this.animationId) {
343
+ cancelAnimationFrame(this.animationId);
344
+ }
345
+ this.clearWaveform();
346
  }
347
 
348
  private scrollToBottom(): void {