File size: 19,691 Bytes
e0c1694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d60045
e0c1694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0a0448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0c1694
 
 
 
 
 
 
 
 
 
 
a0a0448
e0c1694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// Wait for the DOM to be fully loaded before running any code
document.addEventListener('DOMContentLoaded', function() {
    // Global variables for WebRTC connection
    let peerConnection;      // Stores the WebRTC connection object for audio streaming
    let webrtc_id;           // A unique ID to identify this connection on the server
    let audioContext, analyser, audioSource;  // Audio processing objects for visualization
    let audioLevel = 0;      // Stores the current audio level (volume) from 0-1
    let animationFrame;      // Reference to the animation frame for audio visualization
    let isRecording = false; // Tracks whether we're currently recording or not
    let eventSource;         // Object that receives transcription results from the server

    // DOM element references
    const startButton = document.getElementById('start-button');    // The button to start/stop recording
    const transcriptDiv = document.getElementById('transcript');    // The container for transcription text

    // Log debug info at start
    console.log('DOM loaded. startButton:', startButton, 'transcriptDiv:', transcriptDiv);

    // Variables for managing the transcript display
    let currentParagraph = null;    // Reference to the current paragraph being updated
    let lastUpdateTime = Date.now(); // Timestamp of when we last updated the transcript

    // Show error messages to the user in a toast notification
    function showError(message) {
        console.error('Error:', message);
        const toast = document.getElementById('error-toast');   // Get the toast element
        toast.textContent = message;                           // Set the error message
        toast.style.display = 'block';                         // Make the toast visible

        // Hide toast after 5 seconds
        setTimeout(() => {
            toast.style.display = 'none';                      // Hide the toast
        }, 5000);
    }

    // Handle messages received from the server through WebRTC data channel
    function handleMessage(event) {
        // Parse JSON message
        const eventJson = JSON.parse(event.data);
        // Display errors to the user
        if (eventJson.type === "error") {
            showError(eventJson.message);
        }
        // Log all messages to console for debugging
        console.log('Received message:', event.data);
    }

    // Update button appearance based on connection state
    function updateButtonState() {
        // If connecting, show spinner
        if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
            startButton.innerHTML = `
                <div class="icon-with-spinner">
                    <div class="spinner"></div>
                    <span>Connecting...</span>
                </div>
            `;
            isRecording = false;  // Not recording while connecting
        // If connected, show pulsing recording indicator
        } else if (peerConnection && peerConnection.connectionState === 'connected') {
            startButton.innerHTML = `
                <div class="pulse-container">
                    <div class="pulse-circle"></div>
                    <span>Stop Recording</span>
                </div>
            `;
            isRecording = true;   // Set recording state to true
        // Default state - ready to start
        } else {
            startButton.innerHTML = 'Start Recording';
            isRecording = false;  // Not recording when not connected
        }
        console.log('Button state updated. isRecording:', isRecording);
    }

    // Set up audio visualization to show when the user is speaking
    function setupAudioVisualization(stream) {
        // Create or resume the audio context
        if (!audioContext) {
            // Create new audio context with browser compatibility handling
            audioContext = new (window.AudioContext || window.webkitAudioContext)();
        } else {
            // Resume context if it was suspended
            if (audioContext.state === 'suspended') {
                audioContext.resume();
            }
        }
        
        // Create audio analyzer for processing audio data
        analyser = audioContext.createAnalyser();
        // Create media source from microphone stream
        audioSource = audioContext.createMediaStreamSource(stream);
        // Connect source to analyzer
        audioSource.connect(analyser);
        // Set FFT size (controls frequency data resolution)
        analyser.fftSize = 64;
        // Create array to store frequency data
        const dataArray = new Uint8Array(analyser.frequencyBinCount);

        // Function to continuously update audio level visualization
        function updateAudioLevel() {
            // Get audio frequency data
            analyser.getByteFrequencyData(dataArray);
            // Calculate average volume across all frequencies
            const average = Array.from(dataArray).reduce((a, b) => a + b, 0) / dataArray.length;
            // Convert to 0-1 scale
            audioLevel = average / 255;

            // Update pulse circle size based on audio level
            const pulseCircle = document.querySelector('.pulse-circle');
            if (pulseCircle) {
                pulseCircle.style.setProperty('--audio-level', 1 + audioLevel);
            }

            // Continue animation loop
            animationFrame = requestAnimationFrame(updateAudioLevel);
        }
        // Start audio visualization loop
        updateAudioLevel();
    }

    // Set up WebRTC connection for streaming audio to server
    async function setupWebRTC() {
        console.log('Setting up WebRTC connection...');
        
        try {
            // Get WebRTC configuration from global variable
            const config = window.__RTC_CONFIGURATION__;
            console.log('WebRTC configuration:', config);
            
            // Create new peer connection
            peerConnection = new RTCPeerConnection(config);
            console.log('Created peer connection:', peerConnection);

            // Set connection timeout (15 seconds)
            const connectionTimeout = setTimeout(() => {
                if (peerConnection && peerConnection.connectionState !== 'connected') {
                    showError('Connection timeout. Please check your network and try again.');
                    stop(); // Stop connection attempt
                }
            }, 15000);

            // Set warning for slow connection (5 seconds)
            const timeoutId = setTimeout(() => {
                const toast = document.getElementById('error-toast');
                toast.textContent = "Connection is taking longer than usual. Are you on a VPN?";
                toast.className = 'toast warning';
                toast.style.display = 'block';

                // Hide warning after 5 seconds
                setTimeout(() => {
                    toast.style.display = 'none';
                }, 5000);
            }, 5000);

            // Update button to show connecting state
            updateButtonState();

            // Request access to user's microphone
            console.log('Requesting microphone access...');
            const stream = await navigator.mediaDevices.getUserMedia({
                audio: true // Only request audio access
            });
            console.log('Microphone access granted:', stream);

            // Set up audio visualization
            setupAudioVisualization(stream);

            // Add audio tracks to WebRTC connection
            stream.getTracks().forEach(track => {
                peerConnection.addTrack(track, stream);
            });
            console.log('Added audio tracks to connection');

            // Monitor connection state changes
            peerConnection.addEventListener('connectionstatechange', () => {
                // Log state changes
                console.log('connectionstatechange', peerConnection.connectionState);
                
                // Handle successful connection
                if (peerConnection.connectionState === 'connected') {
                    clearTimeout(timeoutId);
                    clearTimeout(connectionTimeout);
                    const toast = document.getElementById('error-toast');
                    toast.style.display = 'none';
                    console.log('Connection established successfully');
                // Handle connection failures
                } else if (peerConnection.connectionState === 'failed' || 
                          peerConnection.connectionState === 'disconnected' || 
                          peerConnection.connectionState === 'closed') {
                    showError('Connection lost. Please try again.');
                    stop();
                }
                // Update button appearance
                updateButtonState();
            });

            // Create data channel for server messages
            const dataChannel = peerConnection.createDataChannel('text');
            dataChannel.onmessage = handleMessage;  // Set message handler
            console.log('Created data channel');

            // Add ICE candidate handler to send candidates as they're discovered
            peerConnection.onicecandidate = ({ candidate }) => {
                if (candidate) {
                    console.log("Sending ICE candidate", candidate);
                    fetch('/webrtc/offer', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            candidate: candidate.toJSON(),
                            webrtc_id: webrtc_id,
                            type: "ice-candidate",
                        })
                    });
                }
            };

            // Create connection offer
            console.log('Creating connection offer...');
            const offer = await peerConnection.createOffer();
            // Set local description (our end of connection)
            await peerConnection.setLocalDescription(offer);
            console.log('Local description set');

            // Generate random ID for this connection
            webrtc_id = Math.random().toString(36).substring(7);
            console.log('Generated webrtc_id:', webrtc_id);

            // Send connection offer to server immediately
            console.log('Sending offer to server...');
            const response = await fetch('/webrtc/offer', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    sdp: peerConnection.localDescription.sdp,        // Session description
                    type: peerConnection.localDescription.type,      // Offer type
                    webrtc_id: webrtc_id                             // Unique connection ID
                })
            });
            console.log('Server responded to offer');

            // Parse server response
            const serverResponse = await response.json();
            console.log('Server response:', serverResponse);

            // Handle server errors
            if (serverResponse.status === 'failed') {
                showError(serverResponse.meta.error === 'concurrency_limit_reached'
                    ? `Too many connections. Maximum limit is ${serverResponse.meta.limit}`
                    : serverResponse.meta.error);
                stop();
                startButton.textContent = 'Start Recording';
                return;
            }

            // Complete connection with server's description
            console.log('Setting remote description...');
            await peerConnection.setRemoteDescription(serverResponse);
            console.log('Remote description set');

            // Create event source for receiving transcription results
            console.log('Creating event source for transcription...');
            eventSource = new EventSource('/transcript?webrtc_id=' + webrtc_id);
            // Handle event source errors
            eventSource.onerror = (event) => {
                console.error("EventSource error:", event);
                showError("Transcription connection lost. Please try again.");
            };
            // Process transcription results as they arrive
            eventSource.addEventListener("output", (event) => {
                console.log("Received transcript chunk:", event.data);
                // Add text to display
                appendTranscript(event.data);
            });
            
            console.log('WebRTC setup complete, waiting for connection...');
        } catch (err) {
            // Handle any setup errors
            console.error('Error setting up WebRTC:', err);
            showError('Failed to establish connection: ' + err.message);
            stop();
            startButton.textContent = 'Start Recording';
        }
    }

    function appendTranscriptSimple(text) {
        const p = document.createElement('p');
        p.textContent = text;
        transcriptDiv.appendChild(p);
        transcriptDiv.scrollTop = transcriptDiv.scrollHeight;
    }

    // Add transcription text to display
    function appendTranscript(text) {
        // Clean up text
        const formattedText = text.trim();
        if (!formattedText) return;
        
        const now = Date.now();
        const timeSinceLastUpdate = now - lastUpdateTime;
        lastUpdateTime = now;
        
        // Handle transcript display
        if (!currentParagraph) {
            // Create new paragraph
            currentParagraph = document.createElement('p');
            currentParagraph.classList.add('current');
            transcriptDiv.appendChild(currentParagraph);
            currentParagraph.textContent = formattedText;
        } else {
            // Get current text
            const currentText = currentParagraph.textContent;
            
            // Fix spacing issues by normalizing
            let cleanedText = formattedText;
            
            // 1. Check for simple word repetition - last word repeated
            const words = currentText.split(/\s+/);
            const lastWord = words[words.length - 1].replace(/[^\w]/g, '').toLowerCase();
            
            if (lastWord && lastWord.length > 2) {
                // Check if new text starts with the same word
                const regex = new RegExp(`^${lastWord}`, 'i');
                if (regex.test(cleanedText.replace(/[^\w]/g, ''))) {
                    // Remove the first word if it's a duplicate
                    cleanedText = cleanedText.replace(regex, '').trim();
                }
            }
            
            // 2. Add proper spacing
            let finalText = currentText;
            
            // Only add space if current text doesn't end with space or punctuation
            // and new text doesn't start with punctuation
            if (!/[\s.,!?]$/.test(finalText) && !/^[.,!?]/.test(cleanedText) && cleanedText) {
                finalText += ' ';
            }
            
            // 3. Add the cleaned text
            finalText += cleanedText;
            
            // 4. Fix any run-together words by adding spaces after punctuation
            finalText = finalText.replace(/([.,!?])([a-zA-Z])/g, '$1 $2');
            
            // Update the paragraph text
            currentParagraph.textContent = finalText;
        }
        
        // Create new paragraph on sentence end or pause
        if (/[.!?]$/.test(formattedText) || timeSinceLastUpdate > 5000) {
            // End current paragraph
            if (currentParagraph) {
                currentParagraph.classList.remove('current');
            }
            
            // Prepare for next paragraph
            currentParagraph = null;
        }
        
        // Limit number of displayed paragraphs
        const paragraphs = transcriptDiv.getElementsByTagName('p');
        while (paragraphs.length > 10) { // Keep last 10 paragraphs
            transcriptDiv.removeChild(paragraphs[0]);
        }
        
        // Scroll to show newest text
        requestAnimationFrame(() => {
            transcriptDiv.scrollTop = transcriptDiv.scrollHeight;
        });
    }

    // Stop recording and clean up resources
    function stop() {
        console.log('Stopping recording...');
        // Stop audio visualization
        if (animationFrame) {
            cancelAnimationFrame(animationFrame);
            animationFrame = null;
        }
        
        // Pause audio processing
        if (audioContext) {
            audioContext.suspend();
        }
        
        // Stop all media tracks
        if (peerConnection) {
            const senders = peerConnection.getSenders();
            if (senders) {
                senders.forEach(sender => {
                    if (sender.track) {
                        sender.track.stop();  // Release microphone
                    }
                });
            }
            
            // Close WebRTC connection
            peerConnection.close();
            peerConnection = null;
        }
        
        // Close transcription connection
        if (eventSource) {
            eventSource.close();
            eventSource = null;
        }
        
        // Reset audio level
        audioLevel = 0;
        // Update button display
        updateButtonState();
        
        // Ask about clearing transcript
        if (window.confirm('Clear transcript?')) {
            // Clear all transcript text
            transcriptDiv.innerHTML = '';
            currentParagraph = null;
        } else {
            // Just end current paragraph
            if (currentParagraph) {
                currentParagraph.classList.remove('current');
                currentParagraph = null;
            }
        }
        
        // Reset timestamp
        lastUpdateTime = Date.now();
        console.log('Recording stopped');
    }

    // Clean up resources when page is closed
    window.addEventListener('beforeunload', () => {
        stop();  // Stop recording and release resources
    });

    // Handle start/stop button clicks
    startButton.addEventListener('click', () => {
        console.log('Start button clicked. isRecording:', isRecording);
        if (!isRecording) {
            // Start recording if not already recording
            setupWebRTC();
        } else {
            // Stop recording if currently recording
            stop();
        }
    });

    // Initialize UI when page loads
    console.log('Initializing UI...');
    // Ensure all UI elements are visible
    const elementsToCheck = [
        transcriptDiv,
        startButton,
        document.getElementById('error-toast')
    ];
    
    // Set appropriate display for each element
    elementsToCheck.forEach(el => {
        if (el) {
            // Set appropriate display style based on element type
            el.style.display = el.tagName.toLowerCase() === 'button' ? 'block' : 
                              (el.id === 'transcript' ? 'block' : 'none');
        }
    });
    
    // Apply CSS variables to ensure theme is working
    document.body.style.backgroundColor = 'var(--background-dark)';
    document.body.style.color = 'var(--text-light)';
    
    // Force button colors for consistency
    startButton.style.backgroundColor = 'rgba(249, 164, 92, 1.0)';
    startButton.style.color = 'black';
    
    console.log('UI initialization complete');
});