import { TranslationCache } from './translationCache'; import { RequestQueueManager } from './requestQueue'; import { ErrorBoundary } from './errorBoundary'; import { Validator } from './validator'; import { StreamingTranslation } from './streamingTranslation'; import { PerformanceMonitor } from './performanceMonitor'; import { SpeakerManager } from './speakerManager'; import { ConnectionManager } from './connectionManager'; import { ConnectionUI } from './connectionUI'; // import { apiClient } from './apiClient'; // Available for cross-origin requests // Initialize error boundary const errorBoundary = ErrorBoundary.getInstance(); // Initialize connection management ConnectionManager.getInstance(); // Initialize connection manager const connectionUI = ConnectionUI.getInstance(); // Configure API client if needed for cross-origin requests // import { apiClient } from './apiClient'; // apiClient.configure({ baseUrl: 'https://api.talk2me.com', credentials: 'include' }); document.addEventListener('DOMContentLoaded', function () { // Set up global error handler errorBoundary.setGlobalErrorHandler((error, errorInfo) => { console.error('Global error caught:', error); // Show user-friendly message based on component if (errorInfo.component === 'transcription') { const statusIndicator = document.getElementById('statusIndicator'); if (statusIndicator) { statusIndicator.textContent = 'Transcription failed. Please try again.'; statusIndicator.classList.add('text-danger'); } } else if (errorInfo.component === 'translation') { const translatedText = document.getElementById('translatedText'); if (translatedText) { translatedText.innerHTML = '
Translation failed. Please try again.
'; } } }); // Wrap initialization functions with error boundaries const safeRegisterServiceWorker = errorBoundary.wrapAsync(registerServiceWorker, 'service-worker', async () => console.warn('Service worker registration failed, continuing without PWA features')); const safeInitApp = errorBoundary.wrap(initApp, 'app-init', () => console.error('App initialization failed')); const safeInitInstallPrompt = errorBoundary.wrap(initInstallPrompt, 'install-prompt', () => console.warn('Install prompt initialization failed')); // Register service worker if ('serviceWorker' in navigator) { safeRegisterServiceWorker(); } // Initialize app safeInitApp(); // Check for PWA installation prompts safeInitInstallPrompt(); }); // 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 && registration.periodicSync) { // 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'); const loadingOverlay = document.getElementById('loadingOverlay'); const loadingText = document.getElementById('loadingText'); // Set initial values let isRecording = false; let mediaRecorder = null; let audioChunks = []; let currentSourceText = ''; let currentTranslationText = ''; let currentTtsServerUrl = ''; // Performance monitoring const performanceMonitor = PerformanceMonitor.getInstance(); // Speaker management const speakerManager = SpeakerManager.getInstance(); let multiSpeakerEnabled = false; // Check TTS server status on page load checkTtsServer(); // Check for saved translations in IndexedDB loadSavedTranslations(); // Initialize queue status updates initQueueStatus(); // Start health monitoring startHealthMonitoring(); // Initialize multi-speaker mode initMultiSpeakerMode(); // Multi-speaker mode implementation function initMultiSpeakerMode() { const multiSpeakerToggle = document.getElementById('toggleMultiSpeaker'); const multiSpeakerStatus = document.getElementById('multiSpeakerStatus'); const speakerToolbar = document.getElementById('speakerToolbar'); const conversationView = document.getElementById('conversationView'); const multiSpeakerModeCheckbox = document.getElementById('multiSpeakerMode'); // Load saved preference multiSpeakerEnabled = localStorage.getItem('multiSpeakerMode') === 'true'; if (multiSpeakerModeCheckbox) { multiSpeakerModeCheckbox.checked = multiSpeakerEnabled; } // Show/hide multi-speaker UI based on setting if (multiSpeakerEnabled) { speakerToolbar.style.display = 'block'; conversationView.style.display = 'block'; multiSpeakerStatus.textContent = 'ON'; } // Toggle multi-speaker mode multiSpeakerToggle?.addEventListener('click', () => { multiSpeakerEnabled = !multiSpeakerEnabled; multiSpeakerStatus.textContent = multiSpeakerEnabled ? 'ON' : 'OFF'; if (multiSpeakerEnabled) { speakerToolbar.style.display = 'block'; conversationView.style.display = 'block'; // Add default speaker if none exist if (speakerManager.getAllSpeakers().length === 0) { const defaultSpeaker = speakerManager.addSpeaker('Speaker 1', sourceLanguage.value); speakerManager.setActiveSpeaker(defaultSpeaker.id); updateSpeakerUI(); } } else { speakerToolbar.style.display = 'none'; conversationView.style.display = 'none'; } localStorage.setItem('multiSpeakerMode', multiSpeakerEnabled.toString()); if (multiSpeakerModeCheckbox) { multiSpeakerModeCheckbox.checked = multiSpeakerEnabled; } }); // Add speaker button document.getElementById('addSpeakerBtn')?.addEventListener('click', () => { const name = prompt('Enter speaker name:'); if (name) { const speaker = speakerManager.addSpeaker(name, sourceLanguage.value); speakerManager.setActiveSpeaker(speaker.id); updateSpeakerUI(); } }); // Update speaker UI function updateSpeakerUI() { const speakerList = document.getElementById('speakerList'); speakerList.innerHTML = ''; speakerManager.getAllSpeakers().forEach(speaker => { const btn = document.createElement('button'); btn.className = `speaker-button ${speaker.isActive ? 'active' : ''}`; btn.style.borderColor = speaker.color; btn.style.backgroundColor = speaker.isActive ? speaker.color : 'white'; btn.style.color = speaker.isActive ? 'white' : speaker.color; btn.innerHTML = ` ${speaker.avatar} ${speaker.name} `; btn.addEventListener('click', () => { speakerManager.setActiveSpeaker(speaker.id); updateSpeakerUI(); }); speakerList.appendChild(btn); }); } // Export conversation document.getElementById('exportConversation')?.addEventListener('click', () => { const text = speakerManager.exportConversation(targetLanguage.value); const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `conversation_${new Date().toISOString()}.txt`; a.click(); URL.revokeObjectURL(url); }); // Clear conversation document.getElementById('clearConversation')?.addEventListener('click', () => { if (confirm('Clear all conversation history?')) { speakerManager.clearConversation(); updateConversationView(); } }); // Update conversation view function updateConversationView() { const conversationContent = document.getElementById('conversationContent'); const entries = speakerManager.getConversationInLanguage(targetLanguage.value); conversationContent.innerHTML = entries.map(entry => `${Validator.sanitizeHTML(sanitizedText)}
Detected language: ${Validator.sanitizeHTML(data.detected_language)}`; statusIndicator.textContent = `Transcription complete (${data.detected_language} detected)`; } else { sourceText.innerHTML = `${Validator.sanitizeHTML(sanitizedText)}
`; statusIndicator.textContent = 'Transcription complete'; } playSource.disabled = false; translateBtn.disabled = false; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('success'); setTimeout(() => statusIndicator.classList.remove('success'), 2000); // Cache the transcription in IndexedDB saveToIndexedDB('transcriptions', { text: data.text, language: data.detected_language || sourceLanguage.value, timestamp: new Date().toISOString() }); } else { sourceText.innerHTML = `Error: ${data.error}
`; statusIndicator.textContent = 'Transcription failed'; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('error'); setTimeout(() => statusIndicator.classList.remove('error'), 2000); } } catch (error) { hideProgress(); console.error('Transcription error:', error); if (error.message?.includes('Rate limit')) { sourceText.innerHTML = `Too many requests. Please wait a moment.
`; statusIndicator.textContent = 'Rate limit - please wait'; } else if (error.message?.includes('connection') || error.message?.includes('network')) { sourceText.innerHTML = `Connection error. Your request will be processed when connection is restored.
`; statusIndicator.textContent = 'Connection error - queued'; connectionUI.showTemporaryMessage('Request queued for when connection returns', 'warning'); } else if (!navigator.onLine) { sourceText.innerHTML = `You're offline. Request will be sent when connection is restored.
`; statusIndicator.textContent = 'Offline - request queued'; } else { sourceText.innerHTML = `Failed to transcribe audio. Please try again.
`; statusIndicator.textContent = 'Transcription failed'; } } }; // Wrap transcribe function with error boundary const transcribeAudio = errorBoundary.wrapAsync(transcribeAudioBase, 'transcription', async () => { hideProgress(); hideLoadingOverlay(); sourceText.innerHTML = 'Transcription failed. Please try again.
'; statusIndicator.textContent = 'Transcription error - please retry'; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('error'); }); // Translate button click event translateBtn.addEventListener('click', errorBoundary.wrapAsync(async function () { if (!currentSourceText) { return; } // Check if streaming is enabled const streamingEnabled = localStorage.getItem('streamingTranslation') !== 'false'; // Check if offline mode is enabled const offlineModeEnabled = localStorage.getItem('offlineMode') !== 'false'; if (offlineModeEnabled) { statusIndicator.textContent = 'Checking cache...'; statusIndicator.classList.add('processing'); // Check cache first const cachedTranslation = await TranslationCache.getCachedTranslation(currentSourceText, sourceLanguage.value, targetLanguage.value); if (cachedTranslation) { // Use cached translation console.log('Using cached translation'); currentTranslationText = cachedTranslation; translatedText.innerHTML = `${cachedTranslation} (cached)
`; playTranslation.disabled = false; statusIndicator.textContent = 'Translation complete (from cache)'; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('success'); setTimeout(() => statusIndicator.classList.remove('success'), 2000); return; } } // No cache hit, proceed with API call statusIndicator.textContent = 'Translating...'; // Use streaming if enabled if (streamingEnabled && navigator.onLine) { // Clear previous translation translatedText.innerHTML = ''; const streamingTextElement = translatedText.querySelector('.streaming-text'); let accumulatedText = ''; // Show minimal loading indicator for streaming statusIndicator.classList.add('processing'); const streamingTranslation = new StreamingTranslation( // onChunk - append text as it arrives (chunk) => { accumulatedText += chunk; streamingTextElement.textContent = accumulatedText; streamingTextElement.classList.add('streaming-active'); }, // onComplete - finalize the translation async (fullText) => { const sanitizedTranslation = Validator.sanitizeText(fullText); currentTranslationText = sanitizedTranslation; streamingTextElement.textContent = sanitizedTranslation; streamingTextElement.classList.remove('streaming-active'); playTranslation.disabled = false; statusIndicator.textContent = 'Translation complete'; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('success'); setTimeout(() => statusIndicator.classList.remove('success'), 2000); // Cache the translation if (offlineModeEnabled) { await TranslationCache.cacheTranslation(currentSourceText, sourceLanguage.value, sanitizedTranslation, targetLanguage.value); } // Save to history saveToIndexedDB('translations', { sourceText: currentSourceText, sourceLanguage: sourceLanguage.value, targetText: sanitizedTranslation, targetLanguage: targetLanguage.value, timestamp: new Date().toISOString() }); }, // onError - handle streaming errors (error) => { translatedText.innerHTML = `Error: ${Validator.sanitizeHTML(error)}
`; statusIndicator.textContent = 'Translation failed'; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('error'); }, // onStart () => { console.log('Starting streaming translation'); }); try { await streamingTranslation.startStreaming(currentSourceText, sourceLanguage.value, targetLanguage.value, true // use streaming ); } catch (error) { console.error('Streaming translation failed:', error); // Fall back to regular translation is handled internally } return; // Exit early for streaming } // Regular non-streaming translation showProgress(); showLoadingOverlay('Translating to ' + targetLanguage.value + '...'); // Validate input text size if (!Validator.validateRequestSize({ text: currentSourceText }, 100)) { translatedText.innerHTML = 'Text is too long to translate. Please shorten it.
'; statusIndicator.textContent = 'Text too long'; hideProgress(); hideLoadingOverlay(); return; } // Validate language codes const validatedSourceLang = Validator.validateLanguageCode(sourceLanguage.value, Array.from(sourceLanguage.options).map(opt => opt.value)); const validatedTargetLang = Validator.validateLanguageCode(targetLanguage.value, Array.from(targetLanguage.options).map(opt => opt.value)); if (!validatedTargetLang) { translatedText.innerHTML = 'Invalid target language selected
'; statusIndicator.textContent = 'Invalid language'; hideProgress(); hideLoadingOverlay(); return; } const requestBody = { text: Validator.sanitizeText(currentSourceText), source_lang: validatedSourceLang || 'auto', target_lang: validatedTargetLang }; try { // Start performance timing for regular translation performanceMonitor.startTimer('regular_translation'); // Use request queue for throttling const queue = RequestQueueManager.getInstance(); const data = await queue.enqueue('translate', async () => { const response = await fetch('/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, 5 // Normal priority for translation ); hideProgress(); if (data.success && data.translation) { // End performance timing performanceMonitor.endTimer('regular_translation'); // Sanitize the translated text const sanitizedTranslation = Validator.sanitizeText(data.translation); currentTranslationText = sanitizedTranslation; translatedText.innerHTML = `${Validator.sanitizeHTML(sanitizedTranslation)}
`; playTranslation.disabled = false; statusIndicator.textContent = 'Translation complete'; statusIndicator.classList.remove('processing'); statusIndicator.classList.add('success'); setTimeout(() => statusIndicator.classList.remove('success'), 2000); // Cache the translation for offline use if enabled if (offlineModeEnabled) { await TranslationCache.cacheTranslation(currentSourceText, sourceLanguage.value, data.translation, targetLanguage.value); } // Also save to regular history saveToIndexedDB('translations', { sourceText: currentSourceText, sourceLanguage: sourceLanguage.value, targetText: data.translation, targetLanguage: targetLanguage.value, timestamp: new Date().toISOString() }); } else { translatedText.innerHTML = `Error: ${data.error}
`; statusIndicator.textContent = 'Translation failed'; } } catch (error) { hideProgress(); console.error('Translation error:', error); if (error.message?.includes('Rate limit')) { translatedText.innerHTML = `Too many requests. Please wait a moment.
`; statusIndicator.textContent = 'Rate limit - please wait'; } else if (error.message?.includes('connection') || error.message?.includes('network')) { translatedText.innerHTML = `Connection error. Your translation will be processed when connection is restored.
`; statusIndicator.textContent = 'Connection error - queued'; connectionUI.showTemporaryMessage('Translation queued for when connection returns', 'warning'); } else if (!navigator.onLine) { statusIndicator.textContent = 'Offline - checking cache...'; translatedText.innerHTML = `You're offline. Only cached translations are available.
`; } else { translatedText.innerHTML = `Failed to translate. Please try again.
`; statusIndicator.textContent = 'Translation failed'; } } }, 'translation', async () => { hideProgress(); hideLoadingOverlay(); translatedText.innerHTML = 'Translation failed. Please try again.
'; statusIndicator.textContent = 'Translation error - please retry'; })); // 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 const playAudioBase = async function (text, language) { showProgress(); showLoadingOverlay('Generating audio...'); const requestBody = { text: text, language: language }; try { // Use request queue for throttling const queue = RequestQueueManager.getInstance(); const data = await queue.enqueue('tts', async () => { const response = await fetch('/speak', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, 3 // Lower priority for TTS ); hideProgress(); if (data.success && data.audio_url) { audioPlayer.src = data.audio_url; audioPlayer.onloadeddata = function () { hideLoadingOverlay(); // Show audio playing animation const playingAnimation = ' '; statusIndicator.innerHTML = playingAnimation + ' Playing audio...'; }; audioPlayer.onended = function () { statusIndicator.innerHTML = ''; statusIndicator.textContent = 'Ready'; statusIndicator.classList.remove('processing'); }; 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 || 'TTS failed'; alert('Failed to play audio: ' + data.error); // Check TTS server status again checkTtsServer(); } } catch (error) { hideProgress(); console.error('TTS error:', error); if (error.message?.includes('Rate limit')) { statusIndicator.textContent = 'Too many requests - please wait'; alert('Too many requests. Please wait a moment before trying again.'); } else if (error.message?.includes('connection') || error.message?.includes('network')) { statusIndicator.textContent = 'Connection error - audio generation queued'; connectionUI.showTemporaryMessage('Audio generation queued for when connection returns', 'warning'); // Show TTS server alert ttsServerAlert.classList.remove('d-none'); ttsServerAlert.classList.remove('alert-success'); ttsServerAlert.classList.add('alert-warning'); ttsServerMessage.textContent = 'Connection error - request queued'; } else if (!navigator.onLine) { statusIndicator.textContent = 'Offline - audio generation unavailable'; alert('Audio generation requires an internet connection.'); } else { 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'; } } }; // Wrap playAudio function with error boundary const playAudio = errorBoundary.wrapAsync(playAudioBase, 'tts', async () => { hideProgress(); hideLoadingOverlay(); statusIndicator.textContent = 'Audio playback failed'; alert('Failed to generate audio. Please check your TTS server connection.'); }); // Clear buttons clearSource.addEventListener('click', function () { sourceText.innerHTML = 'Your transcribed text will appear here...
'; currentSourceText = ''; playSource.disabled = true; translateBtn.disabled = true; }); clearTranslation.addEventListener('click', function () { translatedText.innerHTML = 'Translation will appear here...
'; 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.toString(); } 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); hideLoadingOverlay(); } function showLoadingOverlay(text) { loadingText.textContent = text; loadingOverlay.classList.add('active'); } function hideLoadingOverlay() { loadingOverlay.classList.remove('active'); } // Initialize queue status display function initQueueStatus() { const queueStatus = document.getElementById('queueStatus'); const queueLength = document.getElementById('queueLength'); const activeRequests = document.getElementById('activeRequests'); const queue = RequestQueueManager.getInstance(); // Update queue status display function updateQueueDisplay() { const status = queue.getStatus(); if (status.queueLength > 0 || status.activeRequests > 0) { queueStatus.style.display = 'block'; queueLength.textContent = status.queueLength.toString(); activeRequests.textContent = status.activeRequests.toString(); } else { queueStatus.style.display = 'none'; } } // Poll for status updates setInterval(updateQueueDisplay, 500); // Initial update updateQueueDisplay(); } // Health monitoring and auto-recovery function startHealthMonitoring() { let consecutiveFailures = 0; const maxConsecutiveFailures = 3; async function checkHealth() { try { const response = await fetch('/health', { method: 'GET', signal: AbortSignal.timeout(5000) // 5 second timeout }); if (response.ok) { consecutiveFailures = 0; // Remove any health warning if shown const healthWarning = document.getElementById('healthWarning'); if (healthWarning) { healthWarning.style.display = 'none'; } } else { handleHealthCheckFailure(); } } catch (error) { handleHealthCheckFailure(); } } function handleHealthCheckFailure() { consecutiveFailures++; console.warn(`Health check failed (${consecutiveFailures}/${maxConsecutiveFailures})`); if (consecutiveFailures >= maxConsecutiveFailures) { showHealthWarning(); // Attempt auto-recovery attemptAutoRecovery(); } } function showHealthWarning() { let healthWarning = document.getElementById('healthWarning'); if (!healthWarning) { healthWarning = document.createElement('div'); healthWarning.id = 'healthWarning'; healthWarning.className = 'alert alert-warning alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3'; healthWarning.style.zIndex = '9999'; healthWarning.innerHTML = ` Service health check failed. Some features may be unavailable. `; document.body.appendChild(healthWarning); } healthWarning.style.display = 'block'; } async function attemptAutoRecovery() { console.log('Attempting auto-recovery...'); // Clear any stuck requests in the queue const queue = RequestQueueManager.getInstance(); queue.clearStuckRequests(); // Re-check TTS server checkTtsServer(); // Try to reload service worker if available if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.update(); console.log('Service worker updated'); } } catch (error) { console.error('Failed to update service worker:', error); } } // Reset failure counter after recovery attempt setTimeout(() => { consecutiveFailures = 0; }, 30000); // Wait 30 seconds before resetting } // Check health every 30 seconds setInterval(checkHealth, 30000); // Initial health check after 5 seconds setTimeout(checkHealth, 5000); } } // 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.error); }; }); } 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 = null; const installButton = document.createElement('button'); installButton.style.display = 'none'; installButton.classList.add('btn', 'btn-success', 'fixed-bottom', 'm-3'); installButton.innerHTML = 'Install Voice Translator '; 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', () => { // Hide our user interface that shows our install button installButton.style.display = 'none'; // Show the prompt if (deferredPrompt) { 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) { // Initialize notification UI initNotificationUI(swRegistration); // Check saved preference const notificationsEnabled = localStorage.getItem('notificationsEnabled'); if (notificationsEnabled === 'true' && Notification.permission === 'granted') { subscribeToPushManager(swRegistration); } } function initNotificationUI(swRegistration) { const notificationPrompt = document.getElementById('notificationPrompt'); const enableNotificationsBtn = document.getElementById('enableNotifications'); const notificationToggle = document.getElementById('notificationToggle'); const saveSettingsBtn = document.getElementById('saveSettings'); // Check if we should show the prompt const notificationsDismissed = localStorage.getItem('notificationsDismissed'); const notificationsEnabled = localStorage.getItem('notificationsEnabled'); if (!notificationsDismissed && !notificationsEnabled && Notification.permission === 'default') { // Show toast after 5 seconds setTimeout(() => { const toast = new window.bootstrap.Toast(notificationPrompt); toast.show(); }, 5000); } // Update toggle state notificationToggle.checked = notificationsEnabled === 'true'; // Enable notifications button enableNotificationsBtn?.addEventListener('click', async () => { const permission = await Notification.requestPermission(); if (permission === 'granted') { localStorage.setItem('notificationsEnabled', 'true'); notificationToggle.checked = true; await subscribeToPushManager(swRegistration); const toast = new window.bootstrap.Toast(notificationPrompt); toast.hide(); // Simple alert for mobile compatibility setTimeout(() => { alert('Notifications enabled successfully!'); }, 100); } }); // Notification toggle notificationToggle?.addEventListener('change', async () => { if (notificationToggle.checked) { if (Notification.permission === 'default') { const permission = await Notification.requestPermission(); if (permission !== 'granted') { notificationToggle.checked = false; return; } } localStorage.setItem('notificationsEnabled', 'true'); await subscribeToPushManager(swRegistration); } else { localStorage.setItem('notificationsEnabled', 'false'); await unsubscribeFromPushManager(swRegistration); } }); // Save settings saveSettingsBtn?.addEventListener('click', () => { const notifyTranscription = document.getElementById('notifyTranscription').checked; const notifyTranslation = document.getElementById('notifyTranslation').checked; const notifyErrors = document.getElementById('notifyErrors').checked; const streamingTranslation = document.getElementById('streamingTranslation').checked; const multiSpeakerMode = document.getElementById('multiSpeakerMode').checked; localStorage.setItem('notifyTranscription', notifyTranscription.toString()); localStorage.setItem('notifyTranslation', notifyTranslation.toString()); localStorage.setItem('notifyErrors', notifyErrors.toString()); localStorage.setItem('streamingTranslation', streamingTranslation.toString()); localStorage.setItem('multiSpeakerMode', multiSpeakerMode.toString()); // Update multi-speaker mode if changed const previousMultiSpeakerMode = localStorage.getItem('multiSpeakerMode') === 'true'; if (multiSpeakerMode !== previousMultiSpeakerMode) { window.location.reload(); // Reload to apply changes } // Show inline success message const saveStatus = document.getElementById('settingsSaveStatus'); if (saveStatus) { saveStatus.style.display = 'block'; // Hide after 2 seconds and close modal setTimeout(() => { saveStatus.style.display = 'none'; const modal = window.bootstrap.Modal.getInstance(document.getElementById('settingsModal')); modal.hide(); }, 1500); } }); // Load saved preferences const notifyTranscription = document.getElementById('notifyTranscription'); const notifyTranslation = document.getElementById('notifyTranslation'); const notifyErrors = document.getElementById('notifyErrors'); const streamingTranslation = document.getElementById('streamingTranslation'); notifyTranscription.checked = localStorage.getItem('notifyTranscription') !== 'false'; notifyTranslation.checked = localStorage.getItem('notifyTranslation') !== 'false'; notifyErrors.checked = localStorage.getItem('notifyErrors') === 'true'; streamingTranslation.checked = localStorage.getItem('streamingTranslation') !== 'false'; // Initialize cache management UI initCacheManagement(); } async function initCacheManagement() { const cacheCount = document.getElementById('cacheCount'); const cacheSize = document.getElementById('cacheSize'); const offlineMode = document.getElementById('offlineMode'); const clearCacheBtn = document.getElementById('clearCache'); // Load cache stats async function updateCacheStats() { const stats = await TranslationCache.getCacheStats(); cacheCount.textContent = stats.totalEntries.toString(); cacheSize.textContent = `${(stats.totalSize / 1024).toFixed(1)} KB`; } // Initial load updateCacheStats(); // Load offline mode preference offlineMode.checked = localStorage.getItem('offlineMode') !== 'false'; // Toggle offline mode offlineMode?.addEventListener('change', () => { localStorage.setItem('offlineMode', offlineMode.checked.toString()); }); // Clear cache button clearCacheBtn?.addEventListener('click', async () => { if (confirm('Are you sure you want to clear all cached translations?')) { await TranslationCache.clearCache(); await updateCacheStats(); alert('Translation cache cleared successfully!'); } }); // Update stats when modal is shown const settingsModal = document.getElementById('settingsModal'); settingsModal?.addEventListener('show.bs.modal', updateCacheStats); } 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); } } async function unsubscribeFromPushManager(swRegistration) { try { const subscription = await swRegistration.pushManager.getSubscription(); if (subscription) { // Unsubscribe from server await fetch('/api/push-unsubscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(subscription) }); // Unsubscribe locally await subscription.unsubscribe(); console.log('User is unsubscribed from push notifications'); } } catch (error) { console.error('Failed to unsubscribe from push notifications:', error); } } //# sourceMappingURL=app.js.map