Spaces:
Runtime error
Runtime error
| // T5 Text Simplifier - JavaScript Functionality | |
| class TextSimplifier { | |
| constructor() { | |
| // Since we're running the Flask server locally, we'll use the local URL | |
| this.apiUrl = 'http://localhost:5000'; | |
| this.isDyslexiaMode = false; | |
| this.isProcessing = false; | |
| this.speechSynthesis = window.speechSynthesis; | |
| this.currentUtterance = null; | |
| this.isManuallyStopping = false; | |
| this.setupElements(); | |
| this.setupEventListeners(); | |
| this.initializeTooltip(); | |
| this.checkSpeechSupport(); | |
| } | |
| setupElements() { | |
| // Input elements | |
| this.inputText = document.getElementById('inputText'); | |
| this.clearInput = document.getElementById('clearInput'); | |
| this.copyInput = document.getElementById('copyInput'); | |
| // Output elements | |
| this.outputText = document.getElementById('outputText'); | |
| this.clearOutput = document.getElementById('clearOutput'); | |
| this.copyOutput = document.getElementById('copyOutput'); | |
| this.downloadOutput = document.getElementById('downloadOutput'); | |
| // Control elements | |
| this.simplifyBtn = document.getElementById('simplifyBtn'); | |
| this.dyslexiaToggle = document.getElementById('dyslexiaToggle'); | |
| this.ttsInput = document.getElementById('ttsInput'); | |
| this.ttsOutput = document.getElementById('ttsOutput'); | |
| // Info elements | |
| this.infoBtn = document.querySelector('.info-btn'); | |
| this.infoTooltip = document.getElementById('infoTooltip'); | |
| this.closeTooltip = document.querySelector('.close-tooltip'); | |
| // Status message | |
| this.statusMessage = document.getElementById('statusMessage'); | |
| } | |
| setupEventListeners() { | |
| // Main functionality | |
| this.simplifyBtn.addEventListener('click', () => this.simplifyText()); | |
| // Temporarily disable keydown listener to test | |
| // this.inputText.addEventListener('keydown', (e) => this.handleInputKeydown(e)); | |
| // Remove the input event listeners that might be interfering | |
| // this.inputText.addEventListener('input', (e) => { | |
| // // Allow normal text input without any interference | |
| // return true; | |
| // }); | |
| // this.outputText.addEventListener('input', (e) => { | |
| // // Allow normal text input without any interference | |
| // return true; | |
| // }); | |
| // Dyslexia toggle | |
| this.dyslexiaToggle.addEventListener('click', () => this.toggleDyslexiaMode()); | |
| // Text-to-Speech | |
| this.ttsInput.addEventListener('click', () => this.handleTTSButtonClick('input')); | |
| this.ttsOutput.addEventListener('click', () => this.handleTTSButtonClick('output')); | |
| // Action buttons | |
| this.clearInput.addEventListener('click', () => this.clearText(this.inputText)); | |
| this.clearOutput.addEventListener('click', () => this.clearText(this.outputText)); | |
| this.copyInput.addEventListener('click', () => this.copyToClipboard(this.inputText.value, 'Input text')); | |
| this.copyOutput.addEventListener('click', () => this.copyToClipboard(this.outputText.value, 'Simplified text')); | |
| this.downloadOutput.addEventListener('click', () => this.downloadText()); | |
| // Info tooltip | |
| this.infoBtn.addEventListener('click', () => this.showTooltip()); | |
| this.closeTooltip.addEventListener('click', () => this.hideTooltip()); | |
| // Close tooltip on outside click - temporarily disabled to test | |
| // document.addEventListener('click', (e) => { | |
| // if (!this.infoTooltip.contains(e.target) && !this.infoBtn.contains(e.target)) { | |
| // this.hideTooltip(); | |
| // } | |
| // }); | |
| // Keyboard shortcuts - only enable speech stop shortcut | |
| document.addEventListener('keydown', (e) => this.handleGlobalKeydown(e)); | |
| // Stop speech when page is hidden | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) { | |
| this.stopSpeech(); | |
| } | |
| }); | |
| } | |
| initializeTooltip() { | |
| // Add backdrop for tooltip | |
| const backdrop = document.createElement('div'); | |
| backdrop.className = 'tooltip-backdrop'; | |
| backdrop.style.cssText = ` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| z-index: 999; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| pointer-events: none; | |
| `; | |
| document.body.appendChild(backdrop); | |
| this.tooltipBackdrop = backdrop; | |
| } | |
| checkSpeechSupport() { | |
| if (!this.speechSynthesis) { | |
| this.ttsInput.disabled = true; | |
| this.ttsOutput.disabled = true; | |
| this.ttsInput.title = 'Text-to-speech not supported in this browser'; | |
| this.ttsOutput.title = 'Text-to-speech not supported in this browser'; | |
| } else { | |
| // Log available voices for debugging | |
| this.logAvailableVoices(); | |
| } | |
| } | |
| logAvailableVoices() { | |
| // Wait for voices to load (they might not be available immediately) | |
| const loadVoices = () => { | |
| const voices = this.speechSynthesis.getVoices(); | |
| if (voices.length > 0) { | |
| console.log('Available TTS voices:'); | |
| voices.forEach((voice, index) => { | |
| console.log(`${index + 1}. ${voice.name} (${voice.lang}) - ${voice.default ? 'Default' : 'Available'}`); | |
| }); | |
| } else { | |
| // Try again after a short delay | |
| setTimeout(loadVoices, 100); | |
| } | |
| }; | |
| loadVoices(); | |
| } | |
| async simplifyText() { | |
| const text = this.inputText.value.trim(); | |
| if (!text) { | |
| this.showStatus('Please enter some text to simplify.', 'warning'); | |
| return; | |
| } | |
| if (this.isProcessing) { | |
| return; | |
| } | |
| this.setProcessingState(true); | |
| this.showStatus('Simplifying your text...', 'info'); | |
| try { | |
| const response = await fetch(`${this.apiUrl}/simplify`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.error) { | |
| throw new Error(data.error); | |
| } | |
| if (data.simplified_text) { | |
| this.outputText.value = data.simplified_text; | |
| this.showStatus('Text simplified successfully!', 'success'); | |
| this.outputText.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } else { | |
| throw new Error('No simplified text received from server'); | |
| } | |
| } catch (error) { | |
| console.error('Error simplifying text:', error); | |
| this.showStatus(`Failed to simplify text: ${error.message}`, 'error'); | |
| } finally { | |
| this.setProcessingState(false); | |
| } | |
| } | |
| setProcessingState(isProcessing) { | |
| this.isProcessing = isProcessing; | |
| this.simplifyBtn.disabled = isProcessing; | |
| if (isProcessing) { | |
| this.simplifyBtn.classList.add('loading'); | |
| } else { | |
| this.simplifyBtn.classList.remove('loading'); | |
| } | |
| } | |
| toggleDyslexiaMode() { | |
| this.isDyslexiaMode = !this.isDyslexiaMode; | |
| document.body.classList.toggle('dyslexia-friendly', this.isDyslexiaMode); | |
| this.dyslexiaToggle.setAttribute('aria-pressed', this.isDyslexiaMode); | |
| const status = this.isDyslexiaMode ? 'enabled' : 'disabled'; | |
| this.showStatus(`Dyslexia-friendly formatting ${status}`, 'info'); | |
| // Update button text | |
| const buttonText = this.dyslexiaToggle.querySelector('span'); | |
| buttonText.textContent = this.isDyslexiaMode ? 'Dyslexia-Friendly ✓' : 'Dyslexia-Friendly'; | |
| } | |
| handleTTSButtonClick(type) { | |
| // Check if speech is currently playing | |
| if (this.speechSynthesis.speaking) { | |
| this.stopSpeech(); | |
| } else { | |
| const text = type === 'input' ? this.inputText.value : this.outputText.value; | |
| this.speakText(text, type); | |
| } | |
| } | |
| speakText(text, type) { | |
| if (!this.speechSynthesis) { | |
| this.showStatus('Text-to-speech is not supported in this browser', 'warning'); | |
| return; | |
| } | |
| if (!text.trim()) { | |
| this.showStatus(`No ${type} text to read`, 'warning'); | |
| return; | |
| } | |
| // Stop any current speech | |
| this.stopSpeech(); | |
| this.currentUtterance = new SpeechSynthesisUtterance(text); | |
| // Configure speech settings for more natural sound | |
| this.currentUtterance.rate = 0.85; // Slightly slower for better comprehension | |
| this.currentUtterance.pitch = 0.95; // Slightly lower pitch for warmth | |
| this.currentUtterance.volume = 0.9; // Higher volume for clarity | |
| // Try to use the most natural voice available | |
| const voices = this.speechSynthesis.getVoices(); | |
| let selectedVoice = null; | |
| // Priority order for voice selection (most natural first) | |
| const voicePreferences = [ | |
| // Google Cloud voices (most natural) | |
| voice => voice.name.includes('Google') && voice.name.includes('Neural'), | |
| voice => voice.name.includes('Google') && voice.name.includes('Wavenet'), | |
| voice => voice.name.includes('Google'), | |
| // Microsoft voices | |
| voice => voice.name.includes('Microsoft') && voice.name.includes('Neural'), | |
| voice => voice.name.includes('Microsoft'), | |
| // Amazon Polly voices | |
| voice => voice.name.includes('Amazon') && voice.name.includes('Neural'), | |
| voice => voice.name.includes('Amazon'), | |
| // Apple voices | |
| voice => voice.name.includes('Samantha') || voice.name.includes('Alex'), | |
| voice => voice.name.includes('Apple'), | |
| // Other natural voices | |
| voice => voice.name.includes('Natural') || voice.name.includes('Enhanced'), | |
| voice => voice.name.includes('Premium'), | |
| voice => voice.name.includes('Neural'), | |
| // Fallback to any English voice | |
| voice => voice.lang.startsWith('en') | |
| ]; | |
| // Find the best available voice | |
| for (const preference of voicePreferences) { | |
| selectedVoice = voices.find(voice => | |
| voice.lang.startsWith('en') && preference(voice) | |
| ); | |
| if (selectedVoice) break; | |
| } | |
| if (selectedVoice) { | |
| this.currentUtterance.voice = selectedVoice; | |
| console.log('Using voice:', selectedVoice.name); | |
| } else { | |
| console.log('Using default voice'); | |
| } | |
| // Event handlers | |
| this.currentUtterance.onstart = () => { | |
| this.showStatus(`Reading ${type} text...`, 'info'); | |
| this.updateTTSButton(type, true); | |
| }; | |
| this.currentUtterance.onend = () => { | |
| this.showStatus(`Finished reading ${type} text`, 'success'); | |
| this.updateTTSButton(type, false); | |
| this.currentUtterance = null; | |
| this.isManuallyStopping = false; // Reset the flag | |
| }; | |
| this.currentUtterance.onerror = (event) => { | |
| console.error('Speech synthesis error:', event); | |
| // Only show error message if we're not manually stopping | |
| if (!this.isManuallyStopping) { | |
| this.showStatus('Error reading text', 'error'); | |
| } | |
| this.updateTTSButton(type, false); | |
| this.currentUtterance = null; | |
| this.isManuallyStopping = false; // Reset the flag | |
| }; | |
| this.speechSynthesis.speak(this.currentUtterance); | |
| } | |
| stopSpeech() { | |
| if (this.speechSynthesis.speaking) { | |
| this.isManuallyStopping = true; | |
| this.speechSynthesis.cancel(); | |
| this.showStatus('Stopped reading aloud', 'info'); | |
| } | |
| this.currentUtterance = null; | |
| this.updateTTSButton('input', false); | |
| this.updateTTSButton('output', false); | |
| } | |
| updateTTSButton(type, isSpeaking) { | |
| const button = type === 'input' ? this.ttsInput : this.ttsOutput; | |
| const icon = button.querySelector('i'); | |
| const text = button.querySelector('span'); | |
| if (isSpeaking) { | |
| icon.className = 'fas fa-stop'; | |
| text.textContent = 'Stop'; | |
| button.style.backgroundColor = 'var(--error-color)'; | |
| button.style.color = 'white'; | |
| } else { | |
| icon.className = 'fas fa-volume-up'; | |
| text.textContent = `Read ${type === 'input' ? 'Input' : 'Output'}`; | |
| button.style.backgroundColor = ''; | |
| button.style.color = ''; | |
| } | |
| } | |
| clearText(textElement) { | |
| textElement.value = ''; | |
| // Don't automatically focus - let user decide where to focus | |
| this.showStatus('Text cleared', 'info'); | |
| } | |
| async copyToClipboard(text, type) { | |
| if (!text.trim()) { | |
| this.showStatus(`No ${type.toLowerCase()} to copy`, 'warning'); | |
| return; | |
| } | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| this.showStatus(`${type} copied to clipboard!`, 'success'); | |
| } catch (error) { | |
| console.error('Failed to copy text:', error); | |
| // Fallback for older browsers | |
| this.fallbackCopyToClipboard(text, type); | |
| } | |
| } | |
| fallbackCopyToClipboard(text, type) { | |
| const textArea = document.createElement('textarea'); | |
| textArea.value = text; | |
| textArea.style.position = 'fixed'; | |
| textArea.style.left = '-999999px'; | |
| textArea.style.top = '-999999px'; | |
| document.body.appendChild(textArea); | |
| textArea.focus(); | |
| textArea.select(); | |
| try { | |
| document.execCommand('copy'); | |
| this.showStatus(`${type} copied to clipboard!`, 'success'); | |
| } catch (error) { | |
| console.error('Fallback copy failed:', error); | |
| this.showStatus('Failed to copy text', 'error'); | |
| } | |
| document.body.removeChild(textArea); | |
| } | |
| downloadText() { | |
| const text = this.outputText.value.trim(); | |
| if (!text) { | |
| this.showStatus('No simplified text to download', 'warning'); | |
| return; | |
| } | |
| const blob = new Blob([text], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `simplified-text-${new Date().toISOString().split('T')[0]}.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| this.showStatus('Text downloaded successfully!', 'success'); | |
| } | |
| showTooltip() { | |
| this.infoTooltip.classList.add('show'); | |
| this.tooltipBackdrop.style.pointerEvents = 'auto'; | |
| this.tooltipBackdrop.style.opacity = '1'; | |
| this.infoTooltip.setAttribute('aria-hidden', 'false'); | |
| // Focus management | |
| this.closeTooltip.focus(); | |
| } | |
| hideTooltip() { | |
| this.infoTooltip.classList.remove('show'); | |
| this.tooltipBackdrop.style.pointerEvents = 'none'; | |
| this.tooltipBackdrop.style.opacity = '0'; | |
| this.infoTooltip.setAttribute('aria-hidden', 'true'); | |
| // Return focus to info button | |
| this.infoBtn.focus(); | |
| } | |
| showStatus(message, type = 'info') { | |
| this.statusMessage.textContent = message; | |
| this.statusMessage.className = `status-message ${type} show`; | |
| // Auto-hide after 4 seconds | |
| setTimeout(() => { | |
| this.statusMessage.classList.remove('show'); | |
| }, 4000); | |
| } | |
| handleInputKeydown(e) { | |
| // Ctrl/Cmd + Enter to simplify | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { | |
| e.preventDefault(); | |
| this.simplifyText(); | |
| } | |
| // Don't interfere with normal typing | |
| return true; | |
| } | |
| handleGlobalKeydown(e) { | |
| // Only handle speech stop shortcut to avoid interfering with text input | |
| // Ctrl/Cmd + Shift + S to stop speech | |
| if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'S') { | |
| e.preventDefault(); | |
| this.stopSpeech(); | |
| } | |
| // Escape to close tooltip (only if tooltip is open) | |
| if (e.key === 'Escape' && this.infoTooltip.classList.contains('show')) { | |
| this.hideTooltip(); | |
| } | |
| } | |
| } | |
| // Initialize the application when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new TextSimplifier(); | |
| // Show welcome message | |
| setTimeout(() => { | |
| const statusMessage = document.getElementById('statusMessage'); | |
| statusMessage.textContent = 'Welcome! Enter your text and click "Simplify Text" to get started.'; | |
| statusMessage.className = 'status-message success show'; | |
| setTimeout(() => { | |
| statusMessage.classList.remove('show'); | |
| }, 5000); | |
| }, 1000); | |
| }); | |
| // Service Worker registration for offline functionality (optional) | |
| if ('serviceWorker' in navigator) { | |
| window.addEventListener('load', () => { | |
| navigator.serviceWorker.register('/static/js/sw.js') | |
| .then(registration => { | |
| console.log('SW registered: ', registration); | |
| }) | |
| .catch(registrationError => { | |
| console.log('SW registration failed: ', registrationError); | |
| }); | |
| }); | |
| } | |