Spaces:
Paused
Paused
Update flare-ui/src/app/components/chat/realtime-chat.component.ts
Browse files
flare-ui/src/app/components/chat/realtime-chat.component.ts
CHANGED
@@ -40,12 +40,9 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
40 |
isRecording = false;
|
41 |
isPlayingAudio = false;
|
42 |
currentState: ConversationState = 'idle';
|
43 |
-
currentTranscription = '';
|
44 |
messages: ConversationMessage[] = [];
|
45 |
error = '';
|
46 |
loading = false;
|
47 |
-
|
48 |
-
isVisualizationActive = false;
|
49 |
|
50 |
conversationStates: ConversationState[] = [
|
51 |
'idle', 'listening', 'processing_stt', 'processing_llm', 'processing_tts', 'playing_audio'
|
@@ -56,6 +53,7 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
56 |
private shouldScrollToBottom = false;
|
57 |
private animationId: number | null = null;
|
58 |
private currentAudio: HTMLAudioElement | null = null;
|
|
|
59 |
|
60 |
constructor(
|
61 |
private conversationManager: ConversationManagerService,
|
@@ -105,20 +103,18 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
105 |
this.conversationManager.currentState$.pipe(
|
106 |
takeUntil(this.destroyed$)
|
107 |
).subscribe(state => {
|
108 |
-
console.log('📊
|
109 |
this.currentState = state;
|
110 |
|
111 |
-
//
|
112 |
-
this.isRecording =
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
console.log('🎙️ Transcription update:', text ? `"${text}"` : '(empty)');
|
121 |
-
this.currentTranscription = text;
|
122 |
});
|
123 |
|
124 |
// Subscribe to errors
|
@@ -137,7 +133,7 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
137 |
this.shouldScrollToBottom = true;
|
138 |
}
|
139 |
}
|
140 |
-
|
141 |
ngAfterViewChecked(): void {
|
142 |
if (this.shouldScrollToBottom) {
|
143 |
this.scrollToBottom();
|
@@ -313,13 +309,7 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
313 |
}
|
314 |
|
315 |
private startVisualization(): void {
|
316 |
-
if (!this.audioVisualizer) {
|
317 |
-
console.warn('Audio visualizer element not found');
|
318 |
-
return;
|
319 |
-
}
|
320 |
-
|
321 |
-
if (this.animationId) {
|
322 |
-
console.log('🎨 Visualization already running');
|
323 |
return;
|
324 |
}
|
325 |
|
@@ -334,33 +324,62 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
334 |
canvas.width = canvas.offsetWidth;
|
335 |
canvas.height = canvas.offsetHeight;
|
336 |
|
337 |
-
|
|
|
|
|
|
|
|
|
338 |
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
);
|
348 |
|
349 |
// Animation loop
|
350 |
const animate = () => {
|
351 |
-
|
352 |
-
if (!this.isVisualizationActive) {
|
353 |
-
console.log('🎨 Stopping animation - visualization inactive');
|
354 |
this.clearVisualization();
|
355 |
return;
|
356 |
}
|
357 |
|
358 |
-
// Clear canvas
|
359 |
-
ctx.fillStyle = '
|
360 |
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
361 |
|
362 |
-
//
|
363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
364 |
ctx.lineWidth = 1;
|
365 |
ctx.beginPath();
|
366 |
ctx.moveTo(0, canvas.height / 2);
|
@@ -372,7 +391,7 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
372 |
|
373 |
animate();
|
374 |
}
|
375 |
-
|
376 |
private drawVolumeVisualization(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, volume: number): void {
|
377 |
const barCount = 64;
|
378 |
const barWidth = canvas.width / barCount;
|
@@ -406,6 +425,11 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
406 |
this.animationId = null;
|
407 |
}
|
408 |
|
|
|
|
|
|
|
|
|
|
|
409 |
this.clearVisualization();
|
410 |
}
|
411 |
|
@@ -420,6 +444,7 @@ export class RealtimeChatComponent implements OnInit, OnDestroy, AfterViewChecke
|
|
420 |
}
|
421 |
}
|
422 |
|
|
|
423 |
private cleanupAudio(): void {
|
424 |
if (this.currentAudio) {
|
425 |
this.currentAudio.pause();
|
|
|
40 |
isRecording = false;
|
41 |
isPlayingAudio = false;
|
42 |
currentState: ConversationState = 'idle';
|
|
|
43 |
messages: ConversationMessage[] = [];
|
44 |
error = '';
|
45 |
loading = false;
|
|
|
|
|
46 |
|
47 |
conversationStates: ConversationState[] = [
|
48 |
'idle', 'listening', 'processing_stt', 'processing_llm', 'processing_tts', 'playing_audio'
|
|
|
53 |
private shouldScrollToBottom = false;
|
54 |
private animationId: number | null = null;
|
55 |
private currentAudio: HTMLAudioElement | null = null;
|
56 |
+
private volumeUpdateSubscription?: Subscription;
|
57 |
|
58 |
constructor(
|
59 |
private conversationManager: ConversationManagerService,
|
|
|
103 |
this.conversationManager.currentState$.pipe(
|
104 |
takeUntil(this.destroyed$)
|
105 |
).subscribe(state => {
|
106 |
+
console.log('📊 Conversation state:', state);
|
107 |
this.currentState = state;
|
108 |
|
109 |
+
// Update recording state based on conversation state
|
110 |
+
this.isRecording = (state === 'listening');
|
111 |
+
|
112 |
+
// Start/stop visualization based on state
|
113 |
+
if (state === 'listening' && this.isConversationActive) {
|
114 |
+
this.startVisualization();
|
115 |
+
} else {
|
116 |
+
this.stopVisualization();
|
117 |
+
}
|
|
|
|
|
118 |
});
|
119 |
|
120 |
// Subscribe to errors
|
|
|
133 |
this.shouldScrollToBottom = true;
|
134 |
}
|
135 |
}
|
136 |
+
|
137 |
ngAfterViewChecked(): void {
|
138 |
if (this.shouldScrollToBottom) {
|
139 |
this.scrollToBottom();
|
|
|
309 |
}
|
310 |
|
311 |
private startVisualization(): void {
|
312 |
+
if (!this.audioVisualizer || this.animationId) {
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
return;
|
314 |
}
|
315 |
|
|
|
324 |
canvas.width = canvas.offsetWidth;
|
325 |
canvas.height = canvas.offsetHeight;
|
326 |
|
327 |
+
// Create gradient for bars
|
328 |
+
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
329 |
+
gradient.addColorStop(0, '#4caf50');
|
330 |
+
gradient.addColorStop(0.5, '#66bb6a');
|
331 |
+
gradient.addColorStop(1, '#4caf50');
|
332 |
|
333 |
+
let lastVolume = 0;
|
334 |
+
let targetVolume = 0;
|
335 |
+
const smoothingFactor = 0.8;
|
336 |
+
|
337 |
+
// Subscribe to volume updates
|
338 |
+
this.volumeUpdateSubscription = this.audioService.volumeLevel$.subscribe(volume => {
|
339 |
+
targetVolume = volume;
|
340 |
+
});
|
|
|
341 |
|
342 |
// Animation loop
|
343 |
const animate = () => {
|
344 |
+
if (!this.isRecording || !this.isConversationActive) {
|
|
|
|
|
345 |
this.clearVisualization();
|
346 |
return;
|
347 |
}
|
348 |
|
349 |
+
// Clear canvas
|
350 |
+
ctx.fillStyle = '#1a1a1a';
|
351 |
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
352 |
|
353 |
+
// Smooth volume transition
|
354 |
+
lastVolume = lastVolume * smoothingFactor + targetVolume * (1 - smoothingFactor);
|
355 |
+
|
356 |
+
// Draw frequency bars
|
357 |
+
const barCount = 32;
|
358 |
+
const barWidth = canvas.width / barCount;
|
359 |
+
const barSpacing = 2;
|
360 |
+
|
361 |
+
for (let i = 0; i < barCount; i++) {
|
362 |
+
// Create natural wave effect based on volume
|
363 |
+
const frequencyFactor = Math.sin((i / barCount) * Math.PI);
|
364 |
+
const timeFactor = Math.sin(Date.now() * 0.001 + i * 0.2) * 0.2 + 0.8;
|
365 |
+
const randomFactor = 0.8 + Math.random() * 0.2;
|
366 |
+
|
367 |
+
const barHeight = lastVolume * canvas.height * 0.7 * frequencyFactor * timeFactor * randomFactor;
|
368 |
+
|
369 |
+
const x = i * barWidth;
|
370 |
+
const y = (canvas.height - barHeight) / 2;
|
371 |
+
|
372 |
+
// Draw bar
|
373 |
+
ctx.fillStyle = gradient;
|
374 |
+
ctx.fillRect(x + barSpacing / 2, y, barWidth - barSpacing, barHeight);
|
375 |
+
|
376 |
+
// Draw reflection
|
377 |
+
ctx.fillStyle = 'rgba(76, 175, 80, 0.2)';
|
378 |
+
ctx.fillRect(x + barSpacing / 2, canvas.height - y, barWidth - barSpacing, -barHeight * 0.3);
|
379 |
+
}
|
380 |
+
|
381 |
+
// Draw center line
|
382 |
+
ctx.strokeStyle = 'rgba(76, 175, 80, 0.5)';
|
383 |
ctx.lineWidth = 1;
|
384 |
ctx.beginPath();
|
385 |
ctx.moveTo(0, canvas.height / 2);
|
|
|
391 |
|
392 |
animate();
|
393 |
}
|
394 |
+
|
395 |
private drawVolumeVisualization(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, volume: number): void {
|
396 |
const barCount = 64;
|
397 |
const barWidth = canvas.width / barCount;
|
|
|
425 |
this.animationId = null;
|
426 |
}
|
427 |
|
428 |
+
if (this.volumeUpdateSubscription) {
|
429 |
+
this.volumeUpdateSubscription.unsubscribe();
|
430 |
+
this.volumeUpdateSubscription = undefined;
|
431 |
+
}
|
432 |
+
|
433 |
this.clearVisualization();
|
434 |
}
|
435 |
|
|
|
444 |
}
|
445 |
}
|
446 |
|
447 |
+
|
448 |
private cleanupAudio(): void {
|
449 |
if (this.currentAudio) {
|
450 |
this.currentAudio.pause();
|