ciyidogan commited on
Commit
73af9f5
·
verified ·
1 Parent(s): 5bd660e

Create realtime-chat.component.ts

Browse files
flare-ui/src/app/components/chat/realtime-chat.component.ts ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { MatCardModule } from '@angular/material/card';
4
+ import { MatButtonModule } from '@angular/material/button';
5
+ 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';
13
+
14
+ @Component({
15
+ selector: 'app-realtime-chat',
16
+ standalone: true,
17
+ imports: [
18
+ CommonModule,
19
+ MatCardModule,
20
+ MatButtonModule,
21
+ MatIconModule,
22
+ MatProgressSpinnerModule,
23
+ MatDividerModule,
24
+ MatChipsModule
25
+ ],
26
+ template: `
27
+ <mat-card class="realtime-chat-container">
28
+ <mat-card-header>
29
+ <mat-icon mat-card-avatar>voice_chat</mat-icon>
30
+ <mat-card-title>Real-time Conversation</mat-card-title>
31
+ <mat-card-subtitle>
32
+ <mat-chip-listbox>
33
+ <mat-chip [class.active]="currentState === state"
34
+ *ngFor="let state of conversationStates">
35
+ {{ getStateLabel(state) }}
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>
47
+ <div class="transcription-text">{{ currentTranscription }}</div>
48
+ </div>
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' }}
56
+ </mat-icon>
57
+ <div class="message-content">
58
+ <div class="message-text">{{ msg.text }}</div>
59
+ <div class="message-time">{{ msg.timestamp | date:'HH:mm:ss' }}</div>
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 -->
71
+ <canvas #audioVisualizer
72
+ class="audio-visualizer"
73
+ width="600"
74
+ height="100"
75
+ [class.active]="isRecording">
76
+ </canvas>
77
+ </mat-card-content>
78
+
79
+ <mat-card-actions>
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
89
+ (click)="clearChat()"
90
+ [disabled]="messages.length === 0">
91
+ <mat-icon>clear</mat-icon>
92
+ Temizle
93
+ </button>
94
+ </mat-card-actions>
95
+ </mat-card>
96
+ `,
97
+ styles: [`
98
+ .realtime-chat-container {
99
+ max-width: 800px;
100
+ margin: 20px auto;
101
+ height: 80vh;
102
+ display: flex;
103
+ flex-direction: column;
104
+ }
105
+
106
+ .transcription-area {
107
+ background: #f5f5f5;
108
+ padding: 16px;
109
+ border-radius: 8px;
110
+ margin-bottom: 16px;
111
+ min-height: 60px;
112
+ }
113
+
114
+ .transcription-label {
115
+ font-size: 12px;
116
+ color: #666;
117
+ margin-bottom: 4px;
118
+ }
119
+
120
+ .transcription-text {
121
+ font-size: 16px;
122
+ color: #333;
123
+ min-height: 24px;
124
+ }
125
+
126
+ .chat-messages {
127
+ flex: 1;
128
+ overflow-y: auto;
129
+ padding: 16px;
130
+ background: #fafafa;
131
+ border-radius: 8px;
132
+ }
133
+
134
+ .message {
135
+ display: flex;
136
+ align-items: flex-start;
137
+ margin-bottom: 16px;
138
+ animation: slideIn 0.3s ease-out;
139
+ }
140
+
141
+ @keyframes slideIn {
142
+ from {
143
+ opacity: 0;
144
+ transform: translateY(10px);
145
+ }
146
+ to {
147
+ opacity: 1;
148
+ transform: translateY(0);
149
+ }
150
+ }
151
+
152
+ .message.user {
153
+ flex-direction: row-reverse;
154
+ }
155
+
156
+ .message-icon {
157
+ margin: 0 8px;
158
+ color: #666;
159
+ }
160
+
161
+ .message-content {
162
+ max-width: 70%;
163
+ background: white;
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 {
170
+ background: #3f51b5;
171
+ color: white;
172
+ }
173
+
174
+ .message-text {
175
+ margin-bottom: 4px;
176
+ }
177
+
178
+ .message-time {
179
+ font-size: 11px;
180
+ opacity: 0.7;
181
+ }
182
+
183
+ .audio-button {
184
+ margin-top: 8px;
185
+ }
186
+
187
+ .audio-visualizer {
188
+ width: 100%;
189
+ height: 100px;
190
+ background: #333;
191
+ border-radius: 8px;
192
+ margin-top: 16px;
193
+ opacity: 0.3;
194
+ transition: opacity 0.3s;
195
+ }
196
+
197
+ .audio-visualizer.active {
198
+ opacity: 1;
199
+ }
200
+
201
+ mat-chip {
202
+ font-size: 12px;
203
+ }
204
+
205
+ mat-chip.active {
206
+ background-color: #3f51b5 !important;
207
+ color: white !important;
208
+ }
209
+
210
+ mat-card-actions {
211
+ padding: 16px;
212
+ justify-content: space-between;
213
+ }
214
+ `]
215
+ })
216
+ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecked {
217
+ @ViewChild('scrollContainer') private scrollContainer!: ElementRef;
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 {
274
+ if (this.shouldScrollToBottom) {
275
+ this.scrollToBottom();
276
+ this.shouldScrollToBottom = false;
277
+ }
278
+ }
279
+
280
+ ngOnDestroy(): void {
281
+ this.subscriptions.unsubscribe();
282
+ this.stopVisualization();
283
+ if (this.isConversationActive) {
284
+ this.conversationManager.stopConversation();
285
+ }
286
+ }
287
+
288
+ async toggleConversation(): Promise<void> {
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 {
318
+ const labels: Record<ConversationState, string> = {
319
+ 'idle': 'Bekliyor',
320
+ 'listening': 'Dinliyor',
321
+ 'processing_stt': 'Metin Dönüştürme',
322
+ 'processing_llm': 'Yanıt Hazırlanıyor',
323
+ 'processing_tts': 'Ses Oluşturuluyor',
324
+ 'playing_audio': 'Konuşuyor'
325
+ };
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 {
341
+ if (!this.audioVisualizer) return;
342
+
343
+ const canvas = this.audioVisualizer.nativeElement;
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();
371
+ }
372
+
373
+ private stopVisualization(): void {
374
+ if (this.animationId) {
375
+ cancelAnimationFrame(this.animationId);
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
+ }