// Main application TypeScript with PWA support import { TranscriptionResponse, TranslationResponse, TTSResponse, TTSServerStatus, TTSConfigUpdate, TTSConfigResponse, TranslationRequest, TTSRequest, PushPublicKeyResponse, TranscriptionRecord, TranslationRecord, ServiceWorkerRegistrationExtended, BeforeInstallPromptEvent } from './types'; 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'; // Initialize error boundary const errorBoundary = ErrorBoundary.getInstance(); 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(): Promise${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() } as TranscriptionRecord); } 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: any) { 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 { 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') as HTMLParagraphElement; let accumulatedText = ''; // Show minimal loading indicator for streaming statusIndicator.classList.add('processing'); const streamingTranslation = new StreamingTranslation( // onChunk - append text as it arrives (chunk: string) => { accumulatedText += chunk; streamingTextElement.textContent = accumulatedText; streamingTextElement.classList.add('streaming-active'); }, // onComplete - finalize the translation async (fullText: string) => { 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() } as TranslationRecord); }, // onError - handle streaming errors (error: string) => { 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: TranslationRequest = { 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${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() } as TranslationRecord); } else { translatedText.innerHTML = `Error: ${data.error}
`; statusIndicator.textContent = 'Translation failed'; } } catch (error: any) { 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 (!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: string, language: string): PromiseYour 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(): void { fetch('/check_tts_server') .then(response => response.json() as Promise