diff --git a/.gitignore b/.gitignore index 6a33621..3139bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,15 @@ vapid_public.pem .master_key secrets.db *.key + +# Test files +test_*.py +*_test_output.* +test-*.html +*-debug-script.py + +# Claude IDE +.claude/ + +# Standalone compiled JS (use dist/ instead) +static/js/app.js diff --git a/setup-script.sh b/setup-script.sh deleted file mode 100755 index 2331c45..0000000 --- a/setup-script.sh +++ /dev/null @@ -1,776 +0,0 @@ -#!/bin/bash - -# Create necessary directories -mkdir -p templates static/{css,js} - -# Move HTML template to templates directory -cat > templates/index.html << 'EOL' - - -
- - -Powered by Gemma 3, Whisper & Edge TTS
- -Your transcribed text will appear here...
-Translation will appear here...
-Click to start recording
-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 \ No newline at end of file diff --git a/test-cors.html b/test-cors.html deleted file mode 100644 index 9be8aa7..0000000 --- a/test-cors.html +++ /dev/null @@ -1,228 +0,0 @@ - - - - - -This page tests CORS configuration for the Talk2Me API. Open this file from a different origin (e.g., file:// or a different port) to test cross-origin requests.
- -