talk2me/static/js/app.js
2025-04-05 11:50:31 -06:00

601 lines
22 KiB
JavaScript

// Main application JavaScript with PWA support
document.addEventListener('DOMContentLoaded', function() {
// Register service worker
if ('serviceWorker' in navigator) {
registerServiceWorker();
}
// Initialize app
initApp();
// Check for PWA installation prompts
initInstallPrompt();
});
// Service Worker Registration
async function registerServiceWorker() {
try {
const registration = await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker registered with scope:', registration.scope);
// Setup periodic sync if available
if ('periodicSync' in registration) {
// Request permission for background sync
const status = await navigator.permissions.query({
name: 'periodic-background-sync',
});
if (status.state === 'granted') {
try {
// Register for background sync to check for updates
await registration.periodicSync.register('translation-updates', {
minInterval: 24 * 60 * 60 * 1000, // once per day
});
console.log('Periodic background sync registered');
} catch (error) {
console.error('Periodic background sync could not be registered:', error);
}
}
}
// Setup push notification if available
if ('PushManager' in window) {
setupPushNotifications(registration);
}
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
// Initialize the main application
function initApp() {
// DOM elements
const recordBtn = document.getElementById('recordBtn');
const translateBtn = document.getElementById('translateBtn');
const sourceText = document.getElementById('sourceText');
const translatedText = document.getElementById('translatedText');
const sourceLanguage = document.getElementById('sourceLanguage');
const targetLanguage = document.getElementById('targetLanguage');
const playSource = document.getElementById('playSource');
const playTranslation = document.getElementById('playTranslation');
const clearSource = document.getElementById('clearSource');
const clearTranslation = document.getElementById('clearTranslation');
const statusIndicator = document.getElementById('statusIndicator');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const audioPlayer = document.getElementById('audioPlayer');
const ttsServerAlert = document.getElementById('ttsServerAlert');
const ttsServerMessage = document.getElementById('ttsServerMessage');
const ttsServerUrl = document.getElementById('ttsServerUrl');
const ttsApiKey = document.getElementById('ttsApiKey');
const updateTtsServer = document.getElementById('updateTtsServer');
// Set initial values
let isRecording = false;
let mediaRecorder = null;
let audioChunks = [];
let currentSourceText = '';
let currentTranslationText = '';
let currentTtsServerUrl = '';
// Check TTS server status on page load
checkTtsServer();
// Check for saved translations in IndexedDB
loadSavedTranslations();
// Update TTS server URL and API key
updateTtsServer.addEventListener('click', function() {
const newUrl = ttsServerUrl.value.trim();
const newApiKey = ttsApiKey.value.trim();
if (!newUrl && !newApiKey) {
alert('Please provide at least one value to update');
return;
}
const updateData = {};
if (newUrl) updateData.server_url = newUrl;
if (newApiKey) updateData.api_key = newApiKey;
fetch('/update_tts_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusIndicator.textContent = 'TTS configuration updated';
// Save URL to localStorage but not the API key for security
if (newUrl) localStorage.setItem('ttsServerUrl', newUrl);
// Check TTS server with new configuration
checkTtsServer();
} else {
alert('Failed to update TTS configuration: ' + data.error);
}
})
.catch(error => {
console.error('Failed to update TTS config:', error);
alert('Failed to update TTS configuration. See console for details.');
});
});
// Make sure target language is different from source
if (targetLanguage.options[0].value === sourceLanguage.value) {
targetLanguage.selectedIndex = 1;
}
// Event listeners for language selection
sourceLanguage.addEventListener('change', function() {
if (targetLanguage.value === sourceLanguage.value) {
for (let i = 0; i < targetLanguage.options.length; i++) {
if (targetLanguage.options[i].value !== sourceLanguage.value) {
targetLanguage.selectedIndex = i;
break;
}
}
}
});
targetLanguage.addEventListener('change', function() {
if (targetLanguage.value === sourceLanguage.value) {
for (let i = 0; i < sourceLanguage.options.length; i++) {
if (sourceLanguage.options[i].value !== targetLanguage.value) {
sourceLanguage.selectedIndex = i;
break;
}
}
}
});
// Record button click event
recordBtn.addEventListener('click', function() {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
});
// Function to start recording
function startRecording() {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.addEventListener('dataavailable', event => {
audioChunks.push(event.data);
});
mediaRecorder.addEventListener('stop', () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
transcribeAudio(audioBlob);
});
mediaRecorder.start();
isRecording = true;
recordBtn.classList.add('recording');
recordBtn.classList.replace('btn-primary', 'btn-danger');
recordBtn.innerHTML = '<i class="fas fa-stop"></i>';
statusIndicator.textContent = 'Recording... Click to stop';
})
.catch(error => {
console.error('Error accessing microphone:', error);
alert('Error accessing microphone. Please make sure you have given permission for microphone access.');
});
}
// Function to stop recording
function stopRecording() {
mediaRecorder.stop();
isRecording = false;
recordBtn.classList.remove('recording');
recordBtn.classList.replace('btn-danger', 'btn-primary');
recordBtn.innerHTML = '<i class="fas fa-microphone"></i>';
statusIndicator.textContent = 'Processing audio...';
// Stop all audio tracks
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
// Function to transcribe audio
function transcribeAudio(audioBlob) {
const formData = new FormData();
formData.append('audio', audioBlob);
formData.append('source_lang', sourceLanguage.value);
showProgress();
fetch('/transcribe', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
hideProgress();
if (data.success) {
currentSourceText = data.text;
sourceText.innerHTML = `<p>${data.text}</p>`;
playSource.disabled = false;
translateBtn.disabled = false;
statusIndicator.textContent = 'Transcription complete';
// Cache the transcription in IndexedDB
saveToIndexedDB('transcriptions', {
text: data.text,
language: sourceLanguage.value,
timestamp: new Date().toISOString()
});
} else {
sourceText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
statusIndicator.textContent = 'Transcription failed';
}
})
.catch(error => {
hideProgress();
console.error('Transcription error:', error);
sourceText.innerHTML = `<p class="text-danger">Failed to transcribe audio. Please try again.</p>`;
statusIndicator.textContent = 'Transcription failed';
});
}
// Translate button click event
translateBtn.addEventListener('click', function() {
if (!currentSourceText) {
return;
}
statusIndicator.textContent = 'Translating...';
showProgress();
fetch('/translate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: currentSourceText,
source_lang: sourceLanguage.value,
target_lang: targetLanguage.value
})
})
.then(response => response.json())
.then(data => {
hideProgress();
if (data.success) {
currentTranslationText = data.translation;
translatedText.innerHTML = `<p>${data.translation}</p>`;
playTranslation.disabled = false;
statusIndicator.textContent = 'Translation complete';
// Cache the translation in IndexedDB
saveToIndexedDB('translations', {
sourceText: currentSourceText,
sourceLanguage: sourceLanguage.value,
targetText: data.translation,
targetLanguage: targetLanguage.value,
timestamp: new Date().toISOString()
});
} else {
translatedText.innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
statusIndicator.textContent = 'Translation failed';
}
})
.catch(error => {
hideProgress();
console.error('Translation error:', error);
translatedText.innerHTML = `<p class="text-danger">Failed to translate. Please try again.</p>`;
statusIndicator.textContent = 'Translation failed';
});
});
// Play source text
playSource.addEventListener('click', function() {
if (!currentSourceText) return;
playAudio(currentSourceText, sourceLanguage.value);
statusIndicator.textContent = 'Playing source audio...';
});
// Play translation
playTranslation.addEventListener('click', function() {
if (!currentTranslationText) return;
playAudio(currentTranslationText, targetLanguage.value);
statusIndicator.textContent = 'Playing translation audio...';
});
// Function to play audio via TTS
function playAudio(text, language) {
showProgress();
fetch('/speak', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: text,
language: language
})
})
.then(response => response.json())
.then(data => {
hideProgress();
if (data.success) {
audioPlayer.src = data.audio_url;
audioPlayer.onended = function() {
statusIndicator.textContent = 'Ready';
};
audioPlayer.play();
} else {
statusIndicator.textContent = 'TTS failed';
// Show TTS server alert with error message
ttsServerAlert.classList.remove('d-none');
ttsServerAlert.classList.remove('alert-success');
ttsServerAlert.classList.add('alert-warning');
ttsServerMessage.textContent = data.error;
alert('Failed to play audio: ' + data.error);
// Check TTS server status again
checkTtsServer();
}
})
.catch(error => {
hideProgress();
console.error('TTS error:', error);
statusIndicator.textContent = 'TTS failed';
// Show TTS server alert
ttsServerAlert.classList.remove('d-none');
ttsServerAlert.classList.remove('alert-success');
ttsServerAlert.classList.add('alert-warning');
ttsServerMessage.textContent = 'Failed to connect to TTS server';
});
}
// Clear buttons
clearSource.addEventListener('click', function() {
sourceText.innerHTML = '<p class="text-muted">Your transcribed text will appear here...</p>';
currentSourceText = '';
playSource.disabled = true;
translateBtn.disabled = true;
});
clearTranslation.addEventListener('click', function() {
translatedText.innerHTML = '<p class="text-muted">Translation will appear here...</p>';
currentTranslationText = '';
playTranslation.disabled = true;
});
// Function to check TTS server status
function checkTtsServer() {
fetch('/check_tts_server')
.then(response => response.json())
.then(data => {
currentTtsServerUrl = data.url;
ttsServerUrl.value = currentTtsServerUrl;
// Load saved API key if available
const savedApiKey = localStorage.getItem('ttsApiKeySet');
if (savedApiKey === 'true') {
ttsApiKey.placeholder = '••••••• (API key saved)';
}
if (data.status === 'error' || data.status === 'auth_error') {
ttsServerAlert.classList.remove('d-none');
ttsServerAlert.classList.remove('alert-success');
ttsServerAlert.classList.add('alert-warning');
ttsServerMessage.textContent = data.message;
if (data.status === 'auth_error') {
ttsServerMessage.textContent = 'Authentication error with TTS server. Please check your API key.';
}
} else {
ttsServerAlert.classList.remove('d-none');
ttsServerAlert.classList.remove('alert-warning');
ttsServerAlert.classList.add('alert-success');
ttsServerMessage.textContent = 'TTS server is online and ready.';
setTimeout(() => {
ttsServerAlert.classList.add('d-none');
}, 3000);
}
})
.catch(error => {
console.error('Failed to check TTS server:', error);
ttsServerAlert.classList.remove('d-none');
ttsServerAlert.classList.remove('alert-success');
ttsServerAlert.classList.add('alert-warning');
ttsServerMessage.textContent = 'Failed to check TTS server status.';
});
}
// Progress indicator functions
function showProgress() {
progressContainer.classList.remove('d-none');
let progress = 0;
const interval = setInterval(() => {
progress += 5;
if (progress > 90) {
clearInterval(interval);
}
progressBar.style.width = `${progress}%`;
}, 100);
progressBar.dataset.interval = interval;
}
function hideProgress() {
const interval = progressBar.dataset.interval;
if (interval) {
clearInterval(Number(interval));
}
progressBar.style.width = '100%';
setTimeout(() => {
progressContainer.classList.add('d-none');
progressBar.style.width = '0%';
}, 500);
}
}
// IndexedDB functions for offline data storage
function openIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('VoiceTranslatorDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create stores for transcriptions and translations
if (!db.objectStoreNames.contains('transcriptions')) {
db.createObjectStore('transcriptions', { keyPath: 'timestamp' });
}
if (!db.objectStoreNames.contains('translations')) {
db.createObjectStore('translations', { keyPath: 'timestamp' });
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject('IndexedDB error: ' + event.target.errorCode);
};
});
}
function saveToIndexedDB(storeName, data) {
openIndexedDB().then(db => {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
store.add(data);
}).catch(error => {
console.error('Error saving to IndexedDB:', error);
});
}
function loadSavedTranslations() {
openIndexedDB().then(db => {
const transaction = db.transaction(['translations'], 'readonly');
const store = transaction.objectStore('translations');
const request = store.getAll();
request.onsuccess = (event) => {
const translations = event.target.result;
if (translations && translations.length > 0) {
// Could add a history section or recently used translations
console.log('Loaded saved translations:', translations.length);
}
};
}).catch(error => {
console.error('Error loading from IndexedDB:', error);
});
}
// PWA installation prompt
function initInstallPrompt() {
let deferredPrompt;
const installButton = document.createElement('button');
installButton.style.display = 'none';
installButton.classList.add('btn', 'btn-success', 'fixed-bottom', 'm-3');
installButton.innerHTML = 'Install Voice Translator <i class="fas fa-download ml-2"></i>';
document.body.appendChild(installButton);
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later
deferredPrompt = e;
// Update UI to notify the user they can add to home screen
installButton.style.display = 'block';
installButton.addEventListener('click', (e) => {
// Hide our user interface that shows our install button
installButton.style.display = 'none';
// Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
deferredPrompt = null;
});
});
});
}
// Push notification setup
function setupPushNotifications(swRegistration) {
// First check if we already have permission
if (Notification.permission === 'granted') {
console.log('Notification permission already granted');
subscribeToPushManager(swRegistration);
} else if (Notification.permission !== 'denied') {
// Otherwise, ask for permission
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Notification permission granted');
subscribeToPushManager(swRegistration);
}
});
}
}
async function subscribeToPushManager(swRegistration) {
try {
// Get the server's public key
const response = await fetch('/api/push-public-key');
const data = await response.json();
// Convert the base64 string to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
const convertedVapidKey = urlBase64ToUint8Array(data.publicKey);
// Subscribe to push notifications
const subscription = await swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
// Send the subscription details to the server
await fetch('/api/push-subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription)
});
console.log('User is subscribed to push notifications');
} catch (error) {
console.error('Failed to subscribe to push notifications:', error);
}
}