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 => `
${entry.speakerName.substr(0, 2).toUpperCase()} ${entry.speakerName} ${new Date(entry.timestamp).toLocaleTimeString()}
${Validator.sanitizeHTML(entry.text)}
`).join(''); // Scroll to bottom conversationContent.scrollTop = conversationContent.scrollHeight; } // Store reference to update function for use in transcription window.updateConversationView = updateConversationView; window.updateSpeakerUI = updateSpeakerUI; } // 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 = {}; // Validate URL if (newUrl) { const validatedUrl = Validator.validateURL(newUrl); if (!validatedUrl) { alert('Invalid server URL. Please enter a valid HTTP/HTTPS URL.'); return; } updateData.server_url = validatedUrl; } // Validate API key if (newApiKey) { const validatedKey = Validator.validateAPIKey(newApiKey); if (!validatedKey) { alert('Invalid API key format. API keys should be 20-128 characters and contain only letters, numbers, dashes, and underscores.'); return; } updateData.api_key = validatedKey; } 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 () { // Skip conflict check for auto-detect if (sourceLanguage.value === 'auto') { return; } 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() { // Request audio with specific constraints for better compression const audioConstraints = { audio: { channelCount: 1, // Mono audio (reduces size by 50%) sampleRate: 16000, // Lower sample rate for speech (16kHz is enough for speech) echoCancellation: true, noiseSuppression: true, autoGainControl: true } }; navigator.mediaDevices.getUserMedia(audioConstraints) .then(stream => { // Use webm/opus for better compression (if supported) const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; const options = { mimeType: mimeType, audioBitsPerSecond: 32000 // Low bitrate for speech (32 kbps) }; try { mediaRecorder = new MediaRecorder(stream, options); } catch (e) { // Fallback to default if options not supported console.warn('Compression options not supported, using defaults'); mediaRecorder = new MediaRecorder(stream); } audioChunks = []; mediaRecorder.addEventListener('dataavailable', event => { audioChunks.push(event.data); }); mediaRecorder.addEventListener('stop', async () => { // Create blob with appropriate MIME type const mimeType = mediaRecorder?.mimeType || 'audio/webm'; const audioBlob = new Blob(audioChunks, { type: mimeType }); // Log compression results const sizeInKB = (audioBlob.size / 1024).toFixed(2); console.log(`Audio compressed to ${sizeInKB} KB (${mimeType})`); // If the audio is still too large, we can compress it further if (audioBlob.size > 500 * 1024) { // If larger than 500KB statusIndicator.textContent = 'Compressing audio...'; const compressedBlob = await compressAudioBlob(audioBlob); transcribeAudio(compressedBlob); } else { transcribeAudio(audioBlob); } }); mediaRecorder.start(); isRecording = true; recordBtn.classList.add('recording'); recordBtn.classList.replace('btn-primary', 'btn-danger'); recordBtn.innerHTML = '
'; statusIndicator.textContent = 'Recording... Click to stop'; statusIndicator.classList.add('processing'); }) .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() { if (!mediaRecorder) return; mediaRecorder.stop(); isRecording = false; recordBtn.classList.remove('recording'); recordBtn.classList.replace('btn-danger', 'btn-primary'); recordBtn.innerHTML = ''; statusIndicator.textContent = 'Processing audio...'; statusIndicator.classList.add('processing'); showLoadingOverlay('Transcribing your speech...'); // Stop all audio tracks mediaRecorder.stream.getTracks().forEach(track => track.stop()); } // Function to compress audio blob if needed async function compressAudioBlob(blob) { return new Promise((resolve) => { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const reader = new FileReader(); reader.onload = async (e) => { try { const arrayBuffer = e.target?.result; const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // Downsample to 16kHz mono const offlineContext = new OfflineAudioContext(1, audioBuffer.duration * 16000, 16000); const source = offlineContext.createBufferSource(); source.buffer = audioBuffer; source.connect(offlineContext.destination); source.start(); const compressedBuffer = await offlineContext.startRendering(); // Convert to WAV format const wavBlob = audioBufferToWav(compressedBuffer); const compressedSizeKB = (wavBlob.size / 1024).toFixed(2); console.log(`Further compressed to ${compressedSizeKB} KB`); resolve(wavBlob); } catch (error) { console.error('Compression failed, using original:', error); resolve(blob); // Return original if compression fails } }; reader.readAsArrayBuffer(blob); }); } // Convert AudioBuffer to WAV format function audioBufferToWav(buffer) { const length = buffer.length * buffer.numberOfChannels * 2; const arrayBuffer = new ArrayBuffer(44 + length); const view = new DataView(arrayBuffer); // WAV header const writeString = (offset, string) => { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }; writeString(0, 'RIFF'); view.setUint32(4, 36 + length, true); writeString(8, 'WAVE'); writeString(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, buffer.numberOfChannels, true); view.setUint32(24, buffer.sampleRate, true); view.setUint32(28, buffer.sampleRate * buffer.numberOfChannels * 2, true); view.setUint16(32, buffer.numberOfChannels * 2, true); view.setUint16(34, 16, true); writeString(36, 'data'); view.setUint32(40, length, true); // Convert float samples to 16-bit PCM let offset = 44; for (let i = 0; i < buffer.length; i++) { for (let channel = 0; channel < buffer.numberOfChannels; channel++) { const sample = Math.max(-1, Math.min(1, buffer.getChannelData(channel)[i])); view.setInt16(offset, sample * 0x7FFF, true); offset += 2; } } return new Blob([arrayBuffer], { type: 'audio/wav' }); } // Function to transcribe audio const transcribeAudioBase = async function (audioBlob) { // Validate audio file const validation = Validator.validateAudioFile(new File([audioBlob], 'audio.webm', { type: audioBlob.type })); if (!validation.valid) { statusIndicator.textContent = validation.error || 'Invalid audio file'; statusIndicator.classList.add('text-danger'); hideProgress(); hideLoadingOverlay(); return; } // Validate language code const validatedLang = Validator.validateLanguageCode(sourceLanguage.value, Array.from(sourceLanguage.options).map(opt => opt.value)); if (!validatedLang && sourceLanguage.value !== 'auto') { statusIndicator.textContent = 'Invalid source language selected'; statusIndicator.classList.add('text-danger'); hideProgress(); hideLoadingOverlay(); return; } const formData = new FormData(); formData.append('audio', audioBlob, Validator.sanitizeFilename('audio.webm')); formData.append('source_lang', validatedLang || 'auto'); // Log upload size const sizeInKB = (audioBlob.size / 1024).toFixed(2); console.log(`Uploading ${sizeInKB} KB of audio data`); showProgress(); try { // Use request queue for throttling const queue = RequestQueueManager.getInstance(); const data = await queue.enqueue('transcribe', async () => { const response = await fetch('/transcribe', { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }, 8 // Higher priority for transcription ); hideProgress(); if (data.success && data.text) { // Sanitize the transcribed text const sanitizedText = Validator.sanitizeText(data.text); currentSourceText = sanitizedText; // Handle multi-speaker mode if (multiSpeakerEnabled) { const activeSpeaker = speakerManager.getActiveSpeaker(); if (activeSpeaker) { const entry = speakerManager.addConversationEntry(activeSpeaker.id, sanitizedText, data.detected_language || sourceLanguage.value); // Auto-translate for all other speakers' languages const allLanguages = new Set(speakerManager.getAllSpeakers().map(s => s.language)); allLanguages.delete(entry.originalLanguage); allLanguages.forEach(async (lang) => { try { const response = await fetch('/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: sanitizedText, source_lang: entry.originalLanguage, target_lang: lang }) }); if (response.ok) { const result = await response.json(); if (result.success && result.translation) { speakerManager.addTranslation(entry.id, lang, result.translation); if (window.updateConversationView) { window.updateConversationView(); } } } } catch (error) { console.error(`Failed to translate to ${lang}:`, error); } }); // Update conversation view if (window.updateConversationView) { window.updateConversationView(); } } } // Handle auto-detected language if (data.detected_language && sourceLanguage.value === 'auto') { // Update the source language selector sourceLanguage.value = data.detected_language; // Show detected language info with sanitized HTML sourceText.innerHTML = `

${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