flare / flare-ui /src /app /components /chat /realtime-chat.component.ts
ciyidogan's picture
Create realtime-chat.component.ts
73af9f5 verified
raw
history blame
10.6 kB
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatDividerModule } from '@angular/material/divider';
import { MatChipsModule } from '@angular/material/chips';
import { Subscription } from 'rxjs';
import { ConversationManagerService, ConversationState, ConversationMessage } from '../../services/conversation-manager.service';
import { AudioStreamService } from '../../services/audio-stream.service';
@Component({
selector: 'app-realtime-chat',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatDividerModule,
MatChipsModule
],
template: `
<mat-card class="realtime-chat-container">
<mat-card-header>
<mat-icon mat-card-avatar>voice_chat</mat-icon>
<mat-card-title>Real-time Conversation</mat-card-title>
<mat-card-subtitle>
<mat-chip-listbox>
<mat-chip [class.active]="currentState === state"
*ngFor="let state of conversationStates">
{{ getStateLabel(state) }}
</mat-chip>
</mat-chip-listbox>
</mat-card-subtitle>
</mat-card-header>
<mat-divider></mat-divider>
<mat-card-content>
<!-- Transcription Display -->
<div class="transcription-area" *ngIf="currentTranscription">
<div class="transcription-label">Dinleniyor...</div>
<div class="transcription-text">{{ currentTranscription }}</div>
</div>
<!-- Chat Messages -->
<div class="chat-messages" #scrollContainer>
<div *ngFor="let msg of messages"
[class]="'message ' + msg.role">
<mat-icon class="message-icon">
{{ msg.role === 'user' ? 'person' : 'smart_toy' }}
</mat-icon>
<div class="message-content">
<div class="message-text">{{ msg.text }}</div>
<div class="message-time">{{ msg.timestamp | date:'HH:mm:ss' }}</div>
<button *ngIf="msg.audioUrl"
mat-icon-button
(click)="playAudio(msg.audioUrl)"
class="audio-button">
<mat-icon>volume_up</mat-icon>
</button>
</div>
</div>
</div>
<!-- Audio Visualizer -->
<canvas #audioVisualizer
class="audio-visualizer"
width="600"
height="100"
[class.active]="isRecording">
</canvas>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button
color="primary"
(click)="toggleConversation()"
[disabled]="!sessionId">
<mat-icon>{{ isConversationActive ? 'stop' : 'mic' }}</mat-icon>
{{ isConversationActive ? 'Konuşmayı Bitir' : 'Konuşmaya Başla' }}
</button>
<button mat-button
(click)="clearChat()"
[disabled]="messages.length === 0">
<mat-icon>clear</mat-icon>
Temizle
</button>
</mat-card-actions>
</mat-card>
`,
styles: [`
.realtime-chat-container {
max-width: 800px;
margin: 20px auto;
height: 80vh;
display: flex;
flex-direction: column;
}
.transcription-area {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
min-height: 60px;
}
.transcription-label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.transcription-text {
font-size: 16px;
color: #333;
min-height: 24px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.message {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
flex-direction: row-reverse;
}
.message-icon {
margin: 0 8px;
color: #666;
}
.message-content {
max-width: 70%;
background: white;
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.message.user .message-content {
background: #3f51b5;
color: white;
}
.message-text {
margin-bottom: 4px;
}
.message-time {
font-size: 11px;
opacity: 0.7;
}
.audio-button {
margin-top: 8px;
}
.audio-visualizer {
width: 100%;
height: 100px;
background: #333;
border-radius: 8px;
margin-top: 16px;
opacity: 0.3;
transition: opacity 0.3s;
}
.audio-visualizer.active {
opacity: 1;
}
mat-chip {
font-size: 12px;
}
mat-chip.active {
background-color: #3f51b5 !important;
color: white !important;
}
mat-card-actions {
padding: 16px;
justify-content: space-between;
}
`]
})
export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecked {
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;
@ViewChild('audioVisualizer') private audioVisualizer!: ElementRef<HTMLCanvasElement>;
sessionId: string | null = null;
isConversationActive = false;
isRecording = false;
currentState: ConversationState = 'idle';
currentTranscription = '';
messages: ConversationMessage[] = [];
conversationStates: ConversationState[] = [
'idle', 'listening', 'processing_stt', 'processing_llm', 'processing_tts', 'playing_audio'
];
private subscriptions = new Subscription();
private shouldScrollToBottom = false;
private animationId: number | null = null;
constructor(
private conversationManager: ConversationManagerService,
private audioService: AudioStreamService
) {}
ngOnInit(): void {
// Get session ID from parent component or service
this.sessionId = localStorage.getItem('current_session_id');
// Subscribe to conversation state
this.subscriptions.add(
this.conversationManager.currentState$.subscribe(state => {
this.currentState = state;
this.updateRecordingState(state);
})
);
// Subscribe to messages
this.subscriptions.add(
this.conversationManager.messages$.subscribe(messages => {
this.messages = messages;
this.shouldScrollToBottom = true;
})
);
// Subscribe to transcription
this.subscriptions.add(
this.conversationManager.transcription$.subscribe(text => {
this.currentTranscription = text;
})
);
// Check browser support
if (!AudioStreamService.checkBrowserSupport()) {
alert('Tarayıcınız ses kaydını desteklemiyor. Lütfen modern bir tarayıcı kullanın.');
}
}
ngAfterViewChecked(): void {
if (this.shouldScrollToBottom) {
this.scrollToBottom();
this.shouldScrollToBottom = false;
}
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe();
this.stopVisualization();
if (this.isConversationActive) {
this.conversationManager.stopConversation();
}
}
async toggleConversation(): Promise<void> {
if (!this.sessionId) return;
if (this.isConversationActive) {
this.conversationManager.stopConversation();
this.isConversationActive = false;
this.stopVisualization();
} else {
try {
await this.conversationManager.startConversation(this.sessionId);
this.isConversationActive = true;
this.startVisualization();
} catch (error) {
console.error('Failed to start conversation:', error);
alert('Konuşma başlatılamadı. Lütfen tekrar deneyin.');
}
}
}
clearChat(): void {
this.conversationManager.clearMessages();
this.currentTranscription = '';
}
playAudio(audioUrl: string): void {
const audio = new Audio(audioUrl);
audio.play();
}
getStateLabel(state: ConversationState): string {
const labels: Record<ConversationState, string> = {
'idle': 'Bekliyor',
'listening': 'Dinliyor',
'processing_stt': 'Metin Dönüştürme',
'processing_llm': 'Yanıt Hazırlanıyor',
'processing_tts': 'Ses Oluşturuluyor',
'playing_audio': 'Konuşuyor'
};
return labels[state] || state;
}
private updateRecordingState(state: ConversationState): void {
this.isRecording = state === 'listening';
}
private scrollToBottom(): void {
try {
this.scrollContainer.nativeElement.scrollTop =
this.scrollContainer.nativeElement.scrollHeight;
} catch(err) {}
}
private startVisualization(): void {
if (!this.audioVisualizer) return;
const canvas = this.audioVisualizer.nativeElement;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Simple visualization animation
const draw = () => {
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw random bars for demo
ctx.fillStyle = '#4caf50';
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
const barHeight = Math.random() * canvas.height * 0.8;
const x = i * barWidth;
const y = canvas.height - barHeight;
ctx.fillRect(x, y, barWidth - 2, barHeight);
}
if (this.isRecording) {
this.animationId = requestAnimationFrame(draw);
}
};
draw();
}
private stopVisualization(): void {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// Clear canvas
if (this.audioVisualizer) {
const canvas = this.audioVisualizer.nativeElement;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
}
}