Spaces:
Building
Building
Create conversation-manager.service.ts
Browse files
flare-ui/src/app/services/conversation-manager.service.ts
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { Subject, Observable, timer } from 'rxjs';
|
3 |
+
import { retry, tap } from 'rxjs/operators';
|
4 |
+
|
5 |
+
export interface WebSocketMessage {
|
6 |
+
type: string;
|
7 |
+
[key: string]: any;
|
8 |
+
}
|
9 |
+
|
10 |
+
export interface TranscriptionResult {
|
11 |
+
text: string;
|
12 |
+
is_final: boolean;
|
13 |
+
confidence: number;
|
14 |
+
}
|
15 |
+
|
16 |
+
export interface StateChangeMessage {
|
17 |
+
from: string;
|
18 |
+
to: string;
|
19 |
+
}
|
20 |
+
|
21 |
+
@Injectable({
|
22 |
+
providedIn: 'root'
|
23 |
+
})
|
24 |
+
export class WebSocketService {
|
25 |
+
private socket: WebSocket | null = null;
|
26 |
+
private url: string = '';
|
27 |
+
private reconnectAttempts = 0;
|
28 |
+
private maxReconnectAttempts = 5;
|
29 |
+
private reconnectDelay = 1000;
|
30 |
+
|
31 |
+
// Subjects for different message types
|
32 |
+
private messageSubject = new Subject<WebSocketMessage>();
|
33 |
+
private transcriptionSubject = new Subject<TranscriptionResult>();
|
34 |
+
private stateChangeSubject = new Subject<StateChangeMessage>();
|
35 |
+
private errorSubject = new Subject<string>();
|
36 |
+
private connectionSubject = new Subject<boolean>();
|
37 |
+
|
38 |
+
// Public observables
|
39 |
+
public message$ = this.messageSubject.asObservable();
|
40 |
+
public transcription$ = this.transcriptionSubject.asObservable();
|
41 |
+
public stateChange$ = this.stateChangeSubject.asObservable();
|
42 |
+
public error$ = this.errorSubject.asObservable();
|
43 |
+
public connection$ = this.connectionSubject.asObservable();
|
44 |
+
|
45 |
+
constructor() {}
|
46 |
+
|
47 |
+
connect(sessionId: string): Promise<void> {
|
48 |
+
return new Promise((resolve, reject) => {
|
49 |
+
try {
|
50 |
+
// Construct WebSocket URL
|
51 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
52 |
+
const host = window.location.host;
|
53 |
+
this.url = `${protocol}//${host}/ws/conversation/${sessionId}`;
|
54 |
+
|
55 |
+
console.log(`🔌 Connecting to WebSocket: ${this.url}`);
|
56 |
+
|
57 |
+
this.socket = new WebSocket(this.url);
|
58 |
+
|
59 |
+
this.socket.onopen = () => {
|
60 |
+
console.log('✅ WebSocket connected');
|
61 |
+
this.reconnectAttempts = 0;
|
62 |
+
this.connectionSubject.next(true);
|
63 |
+
|
64 |
+
// Start keep-alive ping
|
65 |
+
this.startKeepAlive();
|
66 |
+
|
67 |
+
resolve();
|
68 |
+
};
|
69 |
+
|
70 |
+
this.socket.onmessage = (event) => {
|
71 |
+
try {
|
72 |
+
const message: WebSocketMessage = JSON.parse(event.data);
|
73 |
+
this.handleMessage(message);
|
74 |
+
} catch (error) {
|
75 |
+
console.error('Failed to parse WebSocket message:', error);
|
76 |
+
}
|
77 |
+
};
|
78 |
+
|
79 |
+
this.socket.onerror = (error) => {
|
80 |
+
console.error('❌ WebSocket error:', error);
|
81 |
+
this.errorSubject.next('WebSocket bağlantı hatası');
|
82 |
+
reject(error);
|
83 |
+
};
|
84 |
+
|
85 |
+
this.socket.onclose = () => {
|
86 |
+
console.log('🔌 WebSocket disconnected');
|
87 |
+
this.connectionSubject.next(false);
|
88 |
+
this.stopKeepAlive();
|
89 |
+
|
90 |
+
// Attempt reconnection
|
91 |
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
92 |
+
this.attemptReconnect(sessionId);
|
93 |
+
}
|
94 |
+
};
|
95 |
+
|
96 |
+
} catch (error) {
|
97 |
+
console.error('Failed to create WebSocket:', error);
|
98 |
+
reject(error);
|
99 |
+
}
|
100 |
+
});
|
101 |
+
}
|
102 |
+
|
103 |
+
disconnect(): void {
|
104 |
+
this.stopKeepAlive();
|
105 |
+
|
106 |
+
if (this.socket) {
|
107 |
+
this.socket.close();
|
108 |
+
this.socket = null;
|
109 |
+
}
|
110 |
+
|
111 |
+
this.connectionSubject.next(false);
|
112 |
+
}
|
113 |
+
|
114 |
+
send(message: WebSocketMessage): void {
|
115 |
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
116 |
+
this.socket.send(JSON.stringify(message));
|
117 |
+
} else {
|
118 |
+
console.warn('WebSocket is not connected');
|
119 |
+
this.errorSubject.next('WebSocket bağlantısı yok');
|
120 |
+
}
|
121 |
+
}
|
122 |
+
|
123 |
+
sendAudioChunk(audioData: string): void {
|
124 |
+
this.send({
|
125 |
+
type: 'audio_chunk',
|
126 |
+
data: audioData,
|
127 |
+
timestamp: Date.now()
|
128 |
+
});
|
129 |
+
}
|
130 |
+
|
131 |
+
sendControl(action: string, config?: any): void {
|
132 |
+
this.send({
|
133 |
+
type: 'control',
|
134 |
+
action: action,
|
135 |
+
config: config
|
136 |
+
});
|
137 |
+
}
|
138 |
+
|
139 |
+
private handleMessage(message: WebSocketMessage): void {
|
140 |
+
// Emit to general message stream
|
141 |
+
this.messageSubject.next(message);
|
142 |
+
|
143 |
+
// Handle specific message types
|
144 |
+
switch (message.type) {
|
145 |
+
case 'transcription':
|
146 |
+
this.transcriptionSubject.next({
|
147 |
+
text: message.text,
|
148 |
+
is_final: message.is_final,
|
149 |
+
confidence: message.confidence
|
150 |
+
});
|
151 |
+
break;
|
152 |
+
|
153 |
+
case 'state_change':
|
154 |
+
this.stateChangeSubject.next({
|
155 |
+
from: message.from,
|
156 |
+
to: message.to
|
157 |
+
});
|
158 |
+
break;
|
159 |
+
|
160 |
+
case 'error':
|
161 |
+
this.errorSubject.next(message.message);
|
162 |
+
break;
|
163 |
+
|
164 |
+
case 'tts_audio':
|
165 |
+
// Handle TTS audio chunks
|
166 |
+
this.messageSubject.next(message);
|
167 |
+
break;
|
168 |
+
|
169 |
+
case 'assistant_response':
|
170 |
+
// Handle assistant text response
|
171 |
+
this.messageSubject.next(message);
|
172 |
+
break;
|
173 |
+
}
|
174 |
+
}
|
175 |
+
|
176 |
+
private attemptReconnect(sessionId: string): void {
|
177 |
+
this.reconnectAttempts++;
|
178 |
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
179 |
+
|
180 |
+
console.log(`🔄 Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
|
181 |
+
|
182 |
+
setTimeout(() => {
|
183 |
+
this.connect(sessionId).catch(error => {
|
184 |
+
console.error('Reconnection failed:', error);
|
185 |
+
});
|
186 |
+
}, delay);
|
187 |
+
}
|
188 |
+
|
189 |
+
// Keep-alive mechanism
|
190 |
+
private keepAliveInterval: any;
|
191 |
+
|
192 |
+
private startKeepAlive(): void {
|
193 |
+
this.keepAliveInterval = setInterval(() => {
|
194 |
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
195 |
+
this.send({ type: 'ping' });
|
196 |
+
}
|
197 |
+
}, 30000); // Ping every 30 seconds
|
198 |
+
}
|
199 |
+
|
200 |
+
private stopKeepAlive(): void {
|
201 |
+
if (this.keepAliveInterval) {
|
202 |
+
clearInterval(this.keepAliveInterval);
|
203 |
+
this.keepAliveInterval = null;
|
204 |
+
}
|
205 |
+
}
|
206 |
+
|
207 |
+
isConnected(): boolean {
|
208 |
+
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
|
209 |
+
}
|
210 |
+
}
|