quasi-final
This commit is contained in:
		
							
								
								
									
										600
									
								
								static/js/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										600
									
								
								static/js/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,600 @@ | ||||
| // Main application JavaScript with PWA support | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     // Register service worker | ||||
|     if ('serviceWorker' in navigator) { | ||||
|         registerServiceWorker(); | ||||
|     } | ||||
|  | ||||
|     // Initialize app | ||||
|     initApp(); | ||||
|  | ||||
|     // Check for PWA installation prompts | ||||
|     initInstallPrompt(); | ||||
| }); | ||||
|  | ||||
| // Service Worker Registration | ||||
| async function registerServiceWorker() { | ||||
|     try { | ||||
|         const registration = await navigator.serviceWorker.register('/service-worker.js'); | ||||
|         console.log('Service Worker registered with scope:', registration.scope); | ||||
|  | ||||
|         // Setup periodic sync if available | ||||
|         if ('periodicSync' in registration) { | ||||
|             // Request permission for background sync | ||||
|             const status = await navigator.permissions.query({ | ||||
|                 name: 'periodic-background-sync', | ||||
|             }); | ||||
|  | ||||
|             if (status.state === 'granted') { | ||||
|                 try { | ||||
|                     // Register for background sync to check for updates | ||||
|                     await registration.periodicSync.register('translation-updates', { | ||||
|                         minInterval: 24 * 60 * 60 * 1000, // once per day | ||||
|                     }); | ||||
|                     console.log('Periodic background sync registered'); | ||||
|                 } catch (error) { | ||||
|                     console.error('Periodic background sync could not be registered:', error); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Setup push notification if available | ||||
|         if ('PushManager' in window) { | ||||
|             setupPushNotifications(registration); | ||||
|         } | ||||
|     } catch (error) { | ||||
|         console.error('Service Worker registration failed:', error); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Initialize the main application | ||||
| function initApp() { | ||||
|     // DOM elements | ||||
|     const recordBtn = document.getElementById('recordBtn'); | ||||
|     const translateBtn = document.getElementById('translateBtn'); | ||||
|     const sourceText = document.getElementById('sourceText'); | ||||
|     const translatedText = document.getElementById('translatedText'); | ||||
|     const sourceLanguage = document.getElementById('sourceLanguage'); | ||||
|     const targetLanguage = document.getElementById('targetLanguage'); | ||||
|     const playSource = document.getElementById('playSource'); | ||||
|     const playTranslation = document.getElementById('playTranslation'); | ||||
|     const clearSource = document.getElementById('clearSource'); | ||||
|     const clearTranslation = document.getElementById('clearTranslation'); | ||||
|     const statusIndicator = document.getElementById('statusIndicator'); | ||||
|     const progressContainer = document.getElementById('progressContainer'); | ||||
|     const progressBar = document.getElementById('progressBar'); | ||||
|     const audioPlayer = document.getElementById('audioPlayer'); | ||||
|     const ttsServerAlert = document.getElementById('ttsServerAlert'); | ||||
|     const ttsServerMessage = document.getElementById('ttsServerMessage'); | ||||
|     const ttsServerUrl = document.getElementById('ttsServerUrl'); | ||||
|     const ttsApiKey = document.getElementById('ttsApiKey'); | ||||
|     const updateTtsServer = document.getElementById('updateTtsServer'); | ||||
|  | ||||
|     // Set initial values | ||||
|     let isRecording = false; | ||||
|     let mediaRecorder = null; | ||||
|     let audioChunks = []; | ||||
|     let currentSourceText = ''; | ||||
|     let currentTranslationText = ''; | ||||
|     let currentTtsServerUrl = ''; | ||||
|  | ||||
|     // Check TTS server status on page load | ||||
|     checkTtsServer(); | ||||
|  | ||||
|     // Check for saved translations in IndexedDB | ||||
|     loadSavedTranslations(); | ||||
|  | ||||
|     // Update TTS server URL and API key | ||||
|     updateTtsServer.addEventListener('click', function() { | ||||
|         const newUrl = ttsServerUrl.value.trim(); | ||||
|         const newApiKey = ttsApiKey.value.trim(); | ||||
|          | ||||
|         if (!newUrl && !newApiKey) { | ||||
|             alert('Please provide at least one value to update'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         const updateData = {}; | ||||
|         if (newUrl) updateData.server_url = newUrl; | ||||
|         if (newApiKey) updateData.api_key = newApiKey; | ||||
|          | ||||
|         fetch('/update_tts_config', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify(updateData) | ||||
|         }) | ||||
|         .then(response => response.json()) | ||||
|         .then(data => { | ||||
|             if (data.success) { | ||||
|                 statusIndicator.textContent = 'TTS configuration updated'; | ||||
|                 // Save URL to localStorage but not the API key for security | ||||
|                 if (newUrl) localStorage.setItem('ttsServerUrl', newUrl); | ||||
|                 // Check TTS server with new configuration | ||||
|                 checkTtsServer(); | ||||
|             } else { | ||||
|                 alert('Failed to update TTS configuration: ' + data.error); | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             console.error('Failed to update TTS config:', error); | ||||
|             alert('Failed to update TTS configuration. See console for details.'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     // Make sure target language is different from source | ||||
|     if (targetLanguage.options[0].value === sourceLanguage.value) { | ||||
|         targetLanguage.selectedIndex = 1; | ||||
|     } | ||||
|  | ||||
|     // Event listeners for language selection | ||||
|     sourceLanguage.addEventListener('change', function() { | ||||
|         if (targetLanguage.value === sourceLanguage.value) { | ||||
|             for (let i = 0; i < targetLanguage.options.length; i++) { | ||||
|                 if (targetLanguage.options[i].value !== sourceLanguage.value) { | ||||
|                     targetLanguage.selectedIndex = i; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     targetLanguage.addEventListener('change', function() { | ||||
|         if (targetLanguage.value === sourceLanguage.value) { | ||||
|             for (let i = 0; i < sourceLanguage.options.length; i++) { | ||||
|                 if (sourceLanguage.options[i].value !== targetLanguage.value) { | ||||
|                     sourceLanguage.selectedIndex = i; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Record button click event | ||||
|     recordBtn.addEventListener('click', function() { | ||||
|         if (isRecording) { | ||||
|             stopRecording(); | ||||
|         } else { | ||||
|             startRecording(); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Function to start recording | ||||
|     function startRecording() { | ||||
|         navigator.mediaDevices.getUserMedia({ audio: true }) | ||||
|             .then(stream => { | ||||
|                 mediaRecorder = new MediaRecorder(stream); | ||||
|                 audioChunks = []; | ||||
|  | ||||
|                 mediaRecorder.addEventListener('dataavailable', event => { | ||||
|                     audioChunks.push(event.data); | ||||
|                 }); | ||||
|  | ||||
|                 mediaRecorder.addEventListener('stop', () => { | ||||
|                     const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); | ||||
|                     transcribeAudio(audioBlob); | ||||
|                 }); | ||||
|  | ||||
|                 mediaRecorder.start(); | ||||
|                 isRecording = true; | ||||
|                 recordBtn.classList.add('recording'); | ||||
|                 recordBtn.classList.replace('btn-primary', 'btn-danger'); | ||||
|                 recordBtn.innerHTML = '<i class="fas fa-stop"></i>'; | ||||
|                 statusIndicator.textContent = 'Recording... Click to stop'; | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error('Error accessing microphone:', error); | ||||
|                 alert('Error accessing microphone. Please make sure you have given permission for microphone access.'); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     // Function to stop recording | ||||
|     function stopRecording() { | ||||
|         mediaRecorder.stop(); | ||||
|         isRecording = false; | ||||
|         recordBtn.classList.remove('recording'); | ||||
|         recordBtn.classList.replace('btn-danger', 'btn-primary'); | ||||
|         recordBtn.innerHTML = '<i class="fas fa-microphone"></i>'; | ||||
|         statusIndicator.textContent = 'Processing audio...'; | ||||
|          | ||||
|         // Stop all audio tracks | ||||
|         mediaRecorder.stream.getTracks().forEach(track => track.stop()); | ||||
|     } | ||||
|  | ||||
|     // Function to transcribe audio | ||||
|     function transcribeAudio(audioBlob) { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('audio', audioBlob); | ||||
|         formData.append('source_lang', sourceLanguage.value); | ||||
|  | ||||
|         showProgress(); | ||||
|          | ||||
|         fetch('/transcribe', { | ||||
|             method: 'POST', | ||||
|             body: formData | ||||
|         }) | ||||
|         .then(response => response.json()) | ||||
|         .then(data => { | ||||
|             hideProgress(); | ||||
|              | ||||
|             if (data.success) { | ||||
|                 currentSourceText = data.text; | ||||
|                 sourceText.innerHTML = `<p>${data.text}</p>`; | ||||
|                 playSource.disabled = false; | ||||
|                 translateBtn.disabled = false; | ||||
|                 statusIndicator.textContent = 'Transcription complete'; | ||||
|                  | ||||
|                 // Cache the transcription in IndexedDB | ||||
|                 saveToIndexedDB('transcriptions', { | ||||
|                     text: data.text, | ||||
|                     language: sourceLanguage.value, | ||||
|                     timestamp: new Date().toISOString() | ||||
|                 }); | ||||
|             } else { | ||||
|                 sourceText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`; | ||||
|                 statusIndicator.textContent = 'Transcription failed'; | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             hideProgress(); | ||||
|             console.error('Transcription error:', error); | ||||
|             sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`; | ||||
|             statusIndicator.textContent = 'Transcription failed'; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Translate button click event | ||||
|     translateBtn.addEventListener('click', function() { | ||||
|         if (!currentSourceText) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         statusIndicator.textContent = 'Translating...'; | ||||
|         showProgress(); | ||||
|          | ||||
|         fetch('/translate', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify({ | ||||
|                 text: currentSourceText, | ||||
|                 source_lang: sourceLanguage.value, | ||||
|                 target_lang: targetLanguage.value | ||||
|             }) | ||||
|         }) | ||||
|         .then(response => response.json()) | ||||
|         .then(data => { | ||||
|             hideProgress(); | ||||
|              | ||||
|             if (data.success) { | ||||
|                 currentTranslationText = data.translation; | ||||
|                 translatedText.innerHTML = `<p>${data.translation}</p>`; | ||||
|                 playTranslation.disabled = false; | ||||
|                 statusIndicator.textContent = 'Translation complete'; | ||||
|                  | ||||
|                 // Cache the translation in IndexedDB | ||||
|                 saveToIndexedDB('translations', { | ||||
|                     sourceText: currentSourceText, | ||||
|                     sourceLanguage: sourceLanguage.value, | ||||
|                     targetText: data.translation, | ||||
|                     targetLanguage: targetLanguage.value, | ||||
|                     timestamp: new Date().toISOString() | ||||
|                 }); | ||||
|             } else { | ||||
|                 translatedText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`; | ||||
|                 statusIndicator.textContent = 'Translation failed'; | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             hideProgress(); | ||||
|             console.error('Translation error:', error); | ||||
|             translatedText.innerHTML = `<p class="text-danger">Failed to translate. Please try again.</p>`; | ||||
|             statusIndicator.textContent = 'Translation failed'; | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     // Play source text | ||||
|     playSource.addEventListener('click', function() { | ||||
|         if (!currentSourceText) return; | ||||
|          | ||||
|         playAudio(currentSourceText, sourceLanguage.value); | ||||
|         statusIndicator.textContent = 'Playing source audio...'; | ||||
|     }); | ||||
|  | ||||
|     // Play translation | ||||
|     playTranslation.addEventListener('click', function() { | ||||
|         if (!currentTranslationText) return; | ||||
|          | ||||
|         playAudio(currentTranslationText, targetLanguage.value); | ||||
|         statusIndicator.textContent = 'Playing translation audio...'; | ||||
|     }); | ||||
|  | ||||
|     // Function to play audio via TTS | ||||
|     function playAudio(text, language) { | ||||
|         showProgress(); | ||||
|          | ||||
|         fetch('/speak', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify({ | ||||
|                 text: text, | ||||
|                 language: language | ||||
|             }) | ||||
|         }) | ||||
|         .then(response => response.json()) | ||||
|         .then(data => { | ||||
|             hideProgress(); | ||||
|              | ||||
|             if (data.success) { | ||||
|                 audioPlayer.src = data.audio_url; | ||||
|                 audioPlayer.onended = function() { | ||||
|                     statusIndicator.textContent = 'Ready'; | ||||
|                 }; | ||||
|                 audioPlayer.play(); | ||||
|             } else { | ||||
|                 statusIndicator.textContent = 'TTS failed'; | ||||
|                  | ||||
|                 // Show TTS server alert with error message | ||||
|                 ttsServerAlert.classList.remove('d-none'); | ||||
|                 ttsServerAlert.classList.remove('alert-success'); | ||||
|                 ttsServerAlert.classList.add('alert-warning'); | ||||
|                 ttsServerMessage.textContent = data.error; | ||||
|                  | ||||
|                 alert('Failed to play audio: ' + data.error); | ||||
|                  | ||||
|                 // Check TTS server status again | ||||
|                 checkTtsServer(); | ||||
|             } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|             hideProgress(); | ||||
|             console.error('TTS error:', error); | ||||
|             statusIndicator.textContent = 'TTS failed'; | ||||
|              | ||||
|             // Show TTS server alert | ||||
|             ttsServerAlert.classList.remove('d-none'); | ||||
|             ttsServerAlert.classList.remove('alert-success'); | ||||
|             ttsServerAlert.classList.add('alert-warning'); | ||||
|             ttsServerMessage.textContent = 'Failed to connect to TTS server'; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Clear buttons | ||||
|     clearSource.addEventListener('click', function() { | ||||
|         sourceText.innerHTML = '<p class="text-muted">Your transcribed text will appear here...</p>'; | ||||
|         currentSourceText = ''; | ||||
|         playSource.disabled = true; | ||||
|         translateBtn.disabled = true; | ||||
|     }); | ||||
|  | ||||
|     clearTranslation.addEventListener('click', function() { | ||||
|         translatedText.innerHTML = '<p class="text-muted">Translation will appear here...</p>'; | ||||
|         currentTranslationText = ''; | ||||
|         playTranslation.disabled = true; | ||||
|     }); | ||||
|  | ||||
|     // Function to check TTS server status | ||||
|     function checkTtsServer() { | ||||
|         fetch('/check_tts_server') | ||||
|             .then(response => response.json()) | ||||
|             .then(data => { | ||||
|                 currentTtsServerUrl = data.url; | ||||
|                 ttsServerUrl.value = currentTtsServerUrl; | ||||
|                  | ||||
|                 // Load saved API key if available | ||||
|                 const savedApiKey = localStorage.getItem('ttsApiKeySet'); | ||||
|                 if (savedApiKey === 'true') { | ||||
|                     ttsApiKey.placeholder = '••••••• (API key saved)'; | ||||
|                 } | ||||
|                  | ||||
|                 if (data.status === 'error' || data.status === 'auth_error') { | ||||
|                     ttsServerAlert.classList.remove('d-none'); | ||||
|                     ttsServerAlert.classList.remove('alert-success'); | ||||
|                     ttsServerAlert.classList.add('alert-warning'); | ||||
|                     ttsServerMessage.textContent = data.message; | ||||
|                      | ||||
|                     if (data.status === 'auth_error') { | ||||
|                         ttsServerMessage.textContent = 'Authentication error with TTS server. Please check your API key.'; | ||||
|                     } | ||||
|                 } else { | ||||
|                     ttsServerAlert.classList.remove('d-none'); | ||||
|                     ttsServerAlert.classList.remove('alert-warning'); | ||||
|                     ttsServerAlert.classList.add('alert-success'); | ||||
|                     ttsServerMessage.textContent = 'TTS server is online and ready.'; | ||||
|                     setTimeout(() => { | ||||
|                         ttsServerAlert.classList.add('d-none'); | ||||
|                     }, 3000); | ||||
|                 } | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error('Failed to check TTS server:', error); | ||||
|                 ttsServerAlert.classList.remove('d-none'); | ||||
|                 ttsServerAlert.classList.remove('alert-success'); | ||||
|                 ttsServerAlert.classList.add('alert-warning'); | ||||
|                 ttsServerMessage.textContent = 'Failed to check TTS server status.'; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     // Progress indicator functions | ||||
|     function showProgress() { | ||||
|         progressContainer.classList.remove('d-none'); | ||||
|         let progress = 0; | ||||
|         const interval = setInterval(() => { | ||||
|             progress += 5; | ||||
|             if (progress > 90) { | ||||
|                 clearInterval(interval); | ||||
|             } | ||||
|             progressBar.style.width = `${progress}%`; | ||||
|         }, 100); | ||||
|         progressBar.dataset.interval = interval; | ||||
|     } | ||||
|  | ||||
|     function hideProgress() { | ||||
|         const interval = progressBar.dataset.interval; | ||||
|         if (interval) { | ||||
|             clearInterval(Number(interval)); | ||||
|         } | ||||
|         progressBar.style.width = '100%'; | ||||
|         setTimeout(() => { | ||||
|             progressContainer.classList.add('d-none'); | ||||
|             progressBar.style.width = '0%'; | ||||
|         }, 500); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // IndexedDB functions for offline data storage | ||||
| function openIndexedDB() { | ||||
|     return new Promise((resolve, reject) => { | ||||
|         const request = indexedDB.open('VoiceTranslatorDB', 1); | ||||
|          | ||||
|         request.onupgradeneeded = (event) => { | ||||
|             const db = event.target.result; | ||||
|              | ||||
|             // Create stores for transcriptions and translations | ||||
|             if (!db.objectStoreNames.contains('transcriptions')) { | ||||
|                 db.createObjectStore('transcriptions', { keyPath: 'timestamp' }); | ||||
|             } | ||||
|              | ||||
|             if (!db.objectStoreNames.contains('translations')) { | ||||
|                 db.createObjectStore('translations', { keyPath: 'timestamp' }); | ||||
|             } | ||||
|         }; | ||||
|          | ||||
|         request.onsuccess = (event) => { | ||||
|             resolve(event.target.result); | ||||
|         }; | ||||
|          | ||||
|         request.onerror = (event) => { | ||||
|             reject('IndexedDB error: ' + event.target.errorCode); | ||||
|         }; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function saveToIndexedDB(storeName, data) { | ||||
|     openIndexedDB().then(db => { | ||||
|         const transaction = db.transaction([storeName], 'readwrite'); | ||||
|         const store = transaction.objectStore(storeName); | ||||
|         store.add(data); | ||||
|     }).catch(error => { | ||||
|         console.error('Error saving to IndexedDB:', error); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function loadSavedTranslations() { | ||||
|     openIndexedDB().then(db => { | ||||
|         const transaction = db.transaction(['translations'], 'readonly'); | ||||
|         const store = transaction.objectStore('translations'); | ||||
|         const request = store.getAll(); | ||||
|          | ||||
|         request.onsuccess = (event) => { | ||||
|             const translations = event.target.result; | ||||
|             if (translations && translations.length > 0) { | ||||
|                 // Could add a history section or recently used translations | ||||
|                 console.log('Loaded saved translations:', translations.length); | ||||
|             } | ||||
|         }; | ||||
|     }).catch(error => { | ||||
|         console.error('Error loading from IndexedDB:', error); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // PWA installation prompt | ||||
| function initInstallPrompt() { | ||||
|     let deferredPrompt; | ||||
|     const installButton = document.createElement('button'); | ||||
|     installButton.style.display = 'none'; | ||||
|     installButton.classList.add('btn', 'btn-success', 'fixed-bottom', 'm-3'); | ||||
|     installButton.innerHTML = 'Install Voice Translator <i class="fas fa-download ml-2"></i>'; | ||||
|     document.body.appendChild(installButton); | ||||
|  | ||||
|     window.addEventListener('beforeinstallprompt', (e) => { | ||||
|         // Prevent Chrome 67 and earlier from automatically showing the prompt | ||||
|         e.preventDefault(); | ||||
|         // Stash the event so it can be triggered later | ||||
|         deferredPrompt = e; | ||||
|         // Update UI to notify the user they can add to home screen | ||||
|         installButton.style.display = 'block'; | ||||
|  | ||||
|         installButton.addEventListener('click', (e) => { | ||||
|             // Hide our user interface that shows our install button | ||||
|             installButton.style.display = 'none'; | ||||
|             // Show the prompt | ||||
|             deferredPrompt.prompt(); | ||||
|             // Wait for the user to respond to the prompt | ||||
|             deferredPrompt.userChoice.then((choiceResult) => { | ||||
|                 if (choiceResult.outcome === 'accepted') { | ||||
|                     console.log('User accepted the install prompt'); | ||||
|                 } else { | ||||
|                     console.log('User dismissed the install prompt'); | ||||
|                 } | ||||
|                 deferredPrompt = null; | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Push notification setup | ||||
| function setupPushNotifications(swRegistration) { | ||||
|     // First check if we already have permission | ||||
|     if (Notification.permission === 'granted') { | ||||
|         console.log('Notification permission already granted'); | ||||
|         subscribeToPushManager(swRegistration); | ||||
|     } else if (Notification.permission !== 'denied') { | ||||
|         // Otherwise, ask for permission | ||||
|         Notification.requestPermission().then(function(permission) { | ||||
|             if (permission === 'granted') { | ||||
|                 console.log('Notification permission granted'); | ||||
|                 subscribeToPushManager(swRegistration); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function subscribeToPushManager(swRegistration) { | ||||
|     try { | ||||
|         // Get the server's public key | ||||
|         const response = await fetch('/api/push-public-key'); | ||||
|         const data = await response.json(); | ||||
|          | ||||
|         // Convert the base64 string to Uint8Array | ||||
|         function urlBase64ToUint8Array(base64String) { | ||||
|             const padding = '='.repeat((4 - base64String.length % 4) % 4); | ||||
|             const base64 = (base64String + padding) | ||||
|                 .replace(/-/g, '+') | ||||
|                 .replace(/_/g, '/'); | ||||
|              | ||||
|             const rawData = window.atob(base64); | ||||
|             const outputArray = new Uint8Array(rawData.length); | ||||
|              | ||||
|             for (let i = 0; i < rawData.length; ++i) { | ||||
|                 outputArray[i] = rawData.charCodeAt(i); | ||||
|             } | ||||
|             return outputArray; | ||||
|         } | ||||
|          | ||||
|         const convertedVapidKey = urlBase64ToUint8Array(data.publicKey); | ||||
|          | ||||
|         // Subscribe to push notifications | ||||
|         const subscription = await swRegistration.pushManager.subscribe({ | ||||
|             userVisibleOnly: true, | ||||
|             applicationServerKey: convertedVapidKey | ||||
|         }); | ||||
|          | ||||
|         // Send the subscription details to the server | ||||
|         await fetch('/api/push-subscribe', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json', | ||||
|             }, | ||||
|             body: JSON.stringify(subscription) | ||||
|         }); | ||||
|          | ||||
|         console.log('User is subscribed to push notifications'); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to subscribe to push notifications:', error); | ||||
|     } | ||||
| } | ||||
| @@ -1,255 +0,0 @@ | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     // DOM elements | ||||
|     const sourceLanguage = document.getElementById('sourceLanguage'); | ||||
|     const targetLanguage = document.getElementById('targetLanguage'); | ||||
|     const swapButton = document.getElementById('swapLanguages'); | ||||
|     const sourceText = document.getElementById('sourceText'); | ||||
|     const translatedText = document.getElementById('translatedText'); | ||||
|     const recordSourceButton = document.getElementById('recordSource'); | ||||
|     const speakButton = document.getElementById('speak'); | ||||
|     const clearSourceButton = document.getElementById('clearSource'); | ||||
|     const copyTranslationButton = document.getElementById('copyTranslation'); | ||||
|     const translateButton = document.getElementById('translateButton'); | ||||
|     const statusMessage = document.getElementById('status'); | ||||
|  | ||||
|     // Audio recording variables | ||||
|     let mediaRecorder; | ||||
|     let audioChunks = []; | ||||
|     let isRecording = false; | ||||
|  | ||||
|     // Speech recognition setup | ||||
|     const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | ||||
|     const recognition = SpeechRecognition ? new SpeechRecognition() : null; | ||||
|      | ||||
|     if (recognition) { | ||||
|         recognition.continuous = false; | ||||
|         recognition.interimResults = false; | ||||
|     } | ||||
|  | ||||
|     // Event listeners | ||||
|     swapButton.addEventListener('click', swapLanguages); | ||||
|     translateButton.addEventListener('click', translateText); | ||||
|     clearSourceButton.addEventListener('click', clearSource); | ||||
|     copyTranslationButton.addEventListener('click', copyTranslation); | ||||
|      | ||||
|     if (recognition) { | ||||
|         recordSourceButton.addEventListener('click', toggleRecording); | ||||
|     } else { | ||||
|         recordSourceButton.textContent = "Speech API not supported"; | ||||
|         recordSourceButton.disabled = true; | ||||
|     } | ||||
|      | ||||
|     speakButton.addEventListener('click', speakTranslation); | ||||
|  | ||||
|     // Functions (continued) | ||||
|     function swapLanguages() { | ||||
|         const tempLang = sourceLanguage.value; | ||||
|         sourceLanguage.value = targetLanguage.value; | ||||
|         targetLanguage.value = tempLang; | ||||
|          | ||||
|         // Also swap the text if both fields have content | ||||
|         if (sourceText.value && translatedText.value) { | ||||
|             const tempText = sourceText.value; | ||||
|             sourceText.value = translatedText.value; | ||||
|             translatedText.value = tempText; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     function clearSource() { | ||||
|         sourceText.value = ''; | ||||
|         updateStatus(''); | ||||
|     } | ||||
|      | ||||
|     function copyTranslation() { | ||||
|         if (!translatedText.value) { | ||||
|             updateStatus('Nothing to copy', 'error'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         navigator.clipboard.writeText(translatedText.value) | ||||
|             .then(() => { | ||||
|                 updateStatus('Copied to clipboard!', 'success'); | ||||
|                 setTimeout(() => updateStatus(''), 2000); | ||||
|             }) | ||||
|             .catch(err => { | ||||
|                 updateStatus('Failed to copy: ' + err, 'error'); | ||||
|             }); | ||||
|     } | ||||
|      | ||||
|     async function translateText() { | ||||
|         const source = sourceText.value.trim(); | ||||
|         if (!source) { | ||||
|             updateStatus('Please enter or speak some text to translate', 'error'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         updateStatus('Translating...'); | ||||
|         translatedText.value = ''; | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/translate', { | ||||
|                 method: 'POST', | ||||
|                 headers: { | ||||
|                     'Content-Type': 'application/json', | ||||
|                 }, | ||||
|                 body: JSON.stringify({ | ||||
|                     sourceLanguage: sourceLanguage.value, | ||||
|                     targetLanguage: targetLanguage.value, | ||||
|                     text: source | ||||
|                 }) | ||||
|             }); | ||||
|              | ||||
|             const data = await response.json(); | ||||
|              | ||||
|             if (response.ok) { | ||||
|                 translatedText.value = data.translation; | ||||
|                 updateStatus('Translation complete', 'success'); | ||||
|                 setTimeout(() => updateStatus(''), 2000); | ||||
|             } else { | ||||
|                 updateStatus(data.error || 'Translation failed', 'error'); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             updateStatus('Network error: ' + error.message, 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     function toggleRecording() { | ||||
|         if (!recognition) { | ||||
|             updateStatus('Speech recognition not supported in this browser', 'error'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (isRecording) { | ||||
|             stopRecording(); | ||||
|         } else { | ||||
|             startRecording(); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     function startRecording() { | ||||
|         sourceText.value = ''; | ||||
|         updateStatus('Listening...'); | ||||
|          | ||||
|         recognition.lang = getLanguageCode(sourceLanguage.value); | ||||
|         recognition.onresult = function(event) { | ||||
|             const transcript = event.results[0][0].transcript; | ||||
|             sourceText.value = transcript; | ||||
|             updateStatus('Recording completed', 'success'); | ||||
|             setTimeout(() => updateStatus(''), 2000); | ||||
|         }; | ||||
|          | ||||
|         recognition.onerror = function(event) { | ||||
|             updateStatus('Error in speech recognition: ' + event.error, 'error'); | ||||
|             stopRecording(); | ||||
|         }; | ||||
|          | ||||
|         recognition.onend = function() { | ||||
|             stopRecording(); | ||||
|         }; | ||||
|          | ||||
|         try { | ||||
|             recognition.start(); | ||||
|             isRecording = true; | ||||
|             recordSourceButton.classList.add('recording'); | ||||
|             recordSourceButton.querySelector('.button-text').textContent = 'Stop'; | ||||
|         } catch (error) { | ||||
|             updateStatus('Failed to start recording: ' + error.message, 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     function stopRecording() { | ||||
|         if (isRecording) { | ||||
|             try { | ||||
|                 recognition.stop(); | ||||
|             } catch (error) { | ||||
|                 console.error('Error stopping recognition:', error); | ||||
|             } | ||||
|              | ||||
|             isRecording = false; | ||||
|             recordSourceButton.classList.remove('recording'); | ||||
|             recordSourceButton.querySelector('.button-text').textContent = 'Record'; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     function speakTranslation() { | ||||
|         const text = translatedText.value.trim(); | ||||
|         if (!text) { | ||||
|             updateStatus('No translation to speak', 'error'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         // Use the browser's speech synthesis API | ||||
|         const speech = new SpeechSynthesisUtterance(text); | ||||
|         speech.lang = getLanguageCode(targetLanguage.value); | ||||
|         speech.volume = 1; | ||||
|         speech.rate = 1; | ||||
|         speech.pitch = 1; | ||||
|          | ||||
|         speech.onstart = function() { | ||||
|             updateStatus('Speaking...'); | ||||
|             speakButton.disabled = true; | ||||
|         }; | ||||
|          | ||||
|         speech.onend = function() { | ||||
|             updateStatus(''); | ||||
|             speakButton.disabled = false; | ||||
|         }; | ||||
|          | ||||
|         speech.onerror = function(event) { | ||||
|             updateStatus('Speech synthesis error: ' + event.error, 'error'); | ||||
|             speakButton.disabled = false; | ||||
|         }; | ||||
|          | ||||
|         window.speechSynthesis.speak(speech); | ||||
|     } | ||||
|      | ||||
|     function getLanguageCode(language) { | ||||
|         // Map language names to BCP 47 language tags for speech recognition/synthesis | ||||
|         const languageMap = { | ||||
|             "arabic": "ar-SA", | ||||
|             "armenian": "hy-AM", | ||||
|             "azerbaijani": "az-AZ", | ||||
|             "english": "en-US", | ||||
|             "french": "fr-FR", | ||||
|             "georgian": "ka-GE", | ||||
|             "kazakh": "kk-KZ", | ||||
|             "mandarin": "zh-CN", | ||||
|             "persian": "fa-IR", | ||||
|             "portuguese": "pt-PT", | ||||
|             "russian": "ru-RU", | ||||
|             "turkish": "tr-TR", | ||||
|             "uzbek": "uz-UZ" | ||||
|         }; | ||||
|          | ||||
|         return languageMap[language] || 'en-US'; | ||||
|     } | ||||
|      | ||||
|     function updateStatus(message, type = '') { | ||||
|         statusMessage.textContent = message; | ||||
|         statusMessage.className = 'status-message'; | ||||
|          | ||||
|         if (type) { | ||||
|             statusMessage.classList.add(type); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Check for microphone and speech support when page loads | ||||
|     function checkSupportedFeatures() { | ||||
|         if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { | ||||
|             updateStatus('Microphone access is not supported in this browser', 'error'); | ||||
|             recordSourceButton.disabled = true; | ||||
|         } | ||||
|          | ||||
|         if (!window.SpeechRecognition && !window.webkitSpeechRecognition) { | ||||
|             updateStatus('Speech recognition is not supported in this browser', 'error'); | ||||
|             recordSourceButton.disabled = true; | ||||
|         } | ||||
|          | ||||
|         if (!window.speechSynthesis) { | ||||
|             updateStatus('Speech synthesis is not supported in this browser', 'error'); | ||||
|             speakButton.disabled = true; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     checkSupportedFeatures(); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user