ciyidogan commited on
Commit
70429fe
·
verified ·
1 Parent(s): 890fc3a

Upload 2 files

Browse files
flare-ui/src/app/services/audio-stream-service.ts ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // audio-stream.service.ts (YENİ DOSYA)
2
+ // Path: /flare-ui/src/app/services/audio-stream.service.ts
3
+
4
+ import { Injectable, OnDestroy } from '@angular/core';
5
+ import { Subject, Observable } from 'rxjs';
6
+
7
+ export interface AudioChunk {
8
+ data: string; // Base64 encoded audio
9
+ timestamp: number;
10
+ }
11
+
12
+ @Injectable({
13
+ providedIn: 'root'
14
+ })
15
+ export class AudioStreamService implements OnDestroy {
16
+ private mediaRecorder: MediaRecorder | null = null;
17
+ private audioStream: MediaStream | null = null;
18
+ private audioChunkSubject = new Subject<AudioChunk>();
19
+ private recordingStateSubject = new Subject<boolean>();
20
+
21
+ public audioChunk$ = this.audioChunkSubject.asObservable();
22
+ public recordingState$ = this.recordingStateSubject.asObservable();
23
+
24
+ // Audio constraints
25
+ private constraints = {
26
+ audio: {
27
+ channelCount: 1,
28
+ sampleRate: 16000,
29
+ echoCancellation: true,
30
+ noiseSuppression: true,
31
+ autoGainControl: true
32
+ }
33
+ };
34
+
35
+ ngOnDestroy(): void {
36
+ this.stopRecording();
37
+ }
38
+
39
+ static checkBrowserSupport(): boolean {
40
+ return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
41
+ }
42
+
43
+ async startRecording(): Promise<void> {
44
+ try {
45
+ // Request microphone access
46
+ this.audioStream = await navigator.mediaDevices.getUserMedia(this.constraints);
47
+
48
+ // Create MediaRecorder with appropriate format
49
+ const options: MediaRecorderOptions = {
50
+ mimeType: this.getPreferredMimeType()
51
+ };
52
+
53
+ this.mediaRecorder = new MediaRecorder(this.audioStream, options);
54
+
55
+ // Handle data available
56
+ this.mediaRecorder.ondataavailable = async (event) => {
57
+ if (event.data.size > 0) {
58
+ const base64Data = await this.blobToBase64(event.data);
59
+ this.audioChunkSubject.next({
60
+ data: base64Data,
61
+ timestamp: Date.now()
62
+ });
63
+ }
64
+ };
65
+
66
+ // Handle errors
67
+ this.mediaRecorder.onerror = (error) => {
68
+ console.error('MediaRecorder error:', error);
69
+ this.stopRecording();
70
+ };
71
+
72
+ // Start recording with timeslice for real-time streaming
73
+ this.mediaRecorder.start(100); // Send chunks every 100ms
74
+ this.recordingStateSubject.next(true);
75
+
76
+ console.log('✅ Audio recording started');
77
+
78
+ } catch (error) {
79
+ console.error('Failed to start recording:', error);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ stopRecording(): void {
85
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
86
+ this.mediaRecorder.stop();
87
+ this.mediaRecorder = null;
88
+ }
89
+
90
+ if (this.audioStream) {
91
+ this.audioStream.getTracks().forEach(track => track.stop());
92
+ this.audioStream = null;
93
+ }
94
+
95
+ this.recordingStateSubject.next(false);
96
+ console.log('🛑 Audio recording stopped');
97
+ }
98
+
99
+ private getPreferredMimeType(): string {
100
+ const types = [
101
+ 'audio/webm;codecs=opus',
102
+ 'audio/webm',
103
+ 'audio/ogg;codecs=opus',
104
+ 'audio/ogg'
105
+ ];
106
+
107
+ for (const type of types) {
108
+ if (MediaRecorder.isTypeSupported(type)) {
109
+ console.log(`Using MIME type: ${type}`);
110
+ return type;
111
+ }
112
+ }
113
+
114
+ // Fallback to default
115
+ return '';
116
+ }
117
+
118
+ private async blobToBase64(blob: Blob): Promise<string> {
119
+ return new Promise((resolve, reject) => {
120
+ const reader = new FileReader();
121
+ reader.onloadend = () => {
122
+ if (reader.result) {
123
+ // Remove data URL prefix
124
+ const base64 = (reader.result as string).split(',')[1];
125
+ resolve(base64);
126
+ } else {
127
+ reject(new Error('Failed to convert blob to base64'));
128
+ }
129
+ };
130
+ reader.onerror = reject;
131
+ reader.readAsDataURL(blob);
132
+ });
133
+ }
134
+
135
+ // Volume level monitoring
136
+ async getVolumeLevel(): Promise<number> {
137
+ if (!this.audioStream) return 0;
138
+
139
+ const audioContext = new AudioContext();
140
+ const analyser = audioContext.createAnalyser();
141
+ const source = audioContext.createMediaStreamSource(this.audioStream);
142
+
143
+ source.connect(analyser);
144
+ analyser.fftSize = 256;
145
+
146
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
147
+ analyser.getByteFrequencyData(dataArray);
148
+
149
+ // Calculate average volume
150
+ const average = dataArray.reduce((sum, value) => sum + value, 0) / dataArray.length;
151
+
152
+ // Cleanup
153
+ source.disconnect();
154
+ audioContext.close();
155
+
156
+ return average / 255; // Normalize to 0-1
157
+ }
158
+
159
+ // Check microphone permissions
160
+ async checkMicrophonePermission(): Promise<PermissionState> {
161
+ try {
162
+ const result = await navigator.permissions.query({ name: 'microphone' as PermissionName });
163
+ return result.state;
164
+ } catch (error) {
165
+ console.warn('Permissions API not supported:', error);
166
+ // Assume granted if API not supported
167
+ return 'granted';
168
+ }
169
+ }
170
+ }
flare-ui/src/app/services/error-handler-service.ts ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // error-handler.service.ts (YENİ DOSYA)
2
+ // Path: /flare-ui/src/app/services/error-handler.service.ts
3
+
4
+ import { ErrorHandler, Injectable, Injector } from '@angular/core';
5
+ import { MatSnackBar } from '@angular/material/snack-bar';
6
+ import { Router } from '@angular/router';
7
+ import { HttpErrorResponse } from '@angular/common/http';
8
+
9
+ interface FlareError {
10
+ error: string;
11
+ message: string;
12
+ details?: any;
13
+ request_id?: string;
14
+ timestamp?: string;
15
+ user_action?: string;
16
+ }
17
+
18
+ @Injectable({
19
+ providedIn: 'root'
20
+ })
21
+ export class GlobalErrorHandler implements ErrorHandler {
22
+ constructor(private injector: Injector) {}
23
+
24
+ handleError(error: Error | HttpErrorResponse): void {
25
+ // Get services lazily to avoid circular dependency
26
+ const snackBar = this.injector.get(MatSnackBar);
27
+ const router = this.injector.get(Router);
28
+
29
+ console.error('Global error caught:', error);
30
+
31
+ // Handle HTTP errors
32
+ if (error instanceof HttpErrorResponse) {
33
+ this.handleHttpError(error, snackBar, router);
34
+ } else {
35
+ // Handle client-side errors
36
+ this.handleClientError(error, snackBar);
37
+ }
38
+ }
39
+
40
+ private handleHttpError(error: HttpErrorResponse, snackBar: MatSnackBar, router: Router): void {
41
+ const flareError = error.error as FlareError;
42
+
43
+ // Race condition error (409)
44
+ if (error.status === 409 && flareError.error === 'RaceConditionError') {
45
+ const snackBarRef = snackBar.open(
46
+ flareError.message || 'The data was modified by another user. Please refresh and try again.',
47
+ 'Refresh',
48
+ {
49
+ duration: 0,
50
+ panelClass: ['error-snackbar', 'race-condition-snackbar']
51
+ }
52
+ );
53
+
54
+ snackBarRef.onAction().subscribe(() => {
55
+ window.location.reload();
56
+ });
57
+
58
+ // Show additional info if available
59
+ if (flareError.details?.last_update_user) {
60
+ console.info(`Last updated by: ${flareError.details.last_update_user} at ${flareError.details.last_update_date}`);
61
+ }
62
+ return;
63
+ }
64
+
65
+ // Authentication error (401)
66
+ if (error.status === 401) {
67
+ snackBar.open(
68
+ 'Your session has expired. Please login again.',
69
+ 'Login',
70
+ {
71
+ duration: 5000,
72
+ panelClass: ['error-snackbar']
73
+ }
74
+ ).onAction().subscribe(() => {
75
+ router.navigate(['/login']);
76
+ });
77
+ return;
78
+ }
79
+
80
+ // Validation error (422)
81
+ if (error.status === 422 && flareError.details) {
82
+ const fieldErrors = flareError.details
83
+ .map((d: any) => `${d.field}: ${d.message}`)
84
+ .join('\n');
85
+
86
+ snackBar.open(
87
+ flareError.message || 'Validation failed. Please check your input.',
88
+ 'Close',
89
+ {
90
+ duration: 8000,
91
+ panelClass: ['error-snackbar', 'validation-snackbar']
92
+ }
93
+ );
94
+
95
+ console.error('Validation errors:', flareError.details);
96
+ return;
97
+ }
98
+
99
+ // Not found error (404)
100
+ if (error.status === 404) {
101
+ snackBar.open(
102
+ flareError.message || 'The requested resource was not found.',
103
+ 'Close',
104
+ {
105
+ duration: 5000,
106
+ panelClass: ['error-snackbar']
107
+ }
108
+ );
109
+ return;
110
+ }
111
+
112
+ // Server errors (5xx)
113
+ if (error.status >= 500) {
114
+ const message = flareError.message || 'A server error occurred. Please try again later.';
115
+ const requestId = flareError.request_id;
116
+
117
+ snackBar.open(
118
+ requestId ? `${message} (Request ID: ${requestId})` : message,
119
+ 'Close',
120
+ {
121
+ duration: 8000,
122
+ panelClass: ['error-snackbar', 'server-error-snackbar']
123
+ }
124
+ );
125
+ return;
126
+ }
127
+
128
+ // Generic HTTP error
129
+ snackBar.open(
130
+ flareError.message || `HTTP Error ${error.status}: ${error.statusText}`,
131
+ 'Close',
132
+ {
133
+ duration: 6000,
134
+ panelClass: ['error-snackbar']
135
+ }
136
+ );
137
+ }
138
+
139
+ private handleClientError(error: Error, snackBar: MatSnackBar): void {
140
+ // Check if it's a network error
141
+ if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
142
+ snackBar.open(
143
+ 'Network connection error. Please check your internet connection.',
144
+ 'Retry',
145
+ {
146
+ duration: 0,
147
+ panelClass: ['error-snackbar', 'network-error-snackbar']
148
+ }
149
+ ).onAction().subscribe(() => {
150
+ window.location.reload();
151
+ });
152
+ return;
153
+ }
154
+
155
+ // Generic client error
156
+ snackBar.open(
157
+ 'An unexpected error occurred. Please refresh the page.',
158
+ 'Refresh',
159
+ {
160
+ duration: 6000,
161
+ panelClass: ['error-snackbar']
162
+ }
163
+ ).onAction().subscribe(() => {
164
+ window.location.reload();
165
+ });
166
+ }
167
+ }
168
+
169
+ // Error interceptor for consistent error format
170
+ import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
171
+ import { Observable, throwError } from 'rxjs';
172
+ import { catchError } from 'rxjs/operators';
173
+
174
+ @Injectable()
175
+ export class ErrorInterceptor implements HttpInterceptor {
176
+ intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
177
+ return next.handle(req).pipe(
178
+ catchError((error: HttpErrorResponse) => {
179
+ // Log request details for debugging
180
+ console.error('HTTP Error:', {
181
+ url: req.url,
182
+ method: req.method,
183
+ status: error.status,
184
+ error: error.error,
185
+ requestId: error.headers.get('X-Request-ID')
186
+ });
187
+
188
+ // Re-throw to be handled by global error handler
189
+ return throwError(() => error);
190
+ })
191
+ );
192
+ }
193
+ }