/** * Accessibility Mode UI - Touch Keyboard & STT Display */ class AccessibilityUI { constructor() { this.keyboardBuffer = ''; this.displayData = { history: [], keyboard_buffer: '' }; this.wsConnected = false; this.initializeElements(); this.attachEventListeners(); this.startPolling(); } initializeElements() { this.keyboardBufferEl = document.getElementById('keyboard-buffer'); this.transcriptDisplay = document.getElementById('transcript-display'); this.transcriptHistoryEl = null; this.sttStatusEl = document.getElementById('stt-status'); this.ttsStatusEl = document.getElementById('tts-status'); this.modeIndicatorEl = document.getElementById('mode-indicator'); // Keyboard buttons this.keyButtons = document.querySelectorAll('.key[data-char]'); this.backspaceBtn = document.getElementById('backspace-btn'); this.spaceBtn = document.getElementById('space-btn'); this.clearBtn = document.getElementById('clear-btn'); this.sendBtn = document.getElementById('send-btn'); } attachEventListeners() { // Character keys this.keyButtons.forEach(btn => { btn.addEventListener('click', () => this.inputCharacter(btn.dataset.char)); btn.addEventListener('touch start', (e) => e.preventDefault()); }); // Special keys this.backspaceBtn.addEventListener('click', () => this.backspace()); this.spaceBtn.addEventListener('click', () => this.inputCharacter(' ')); this.clearBtn.addEventListener('click', () => this.clearBuffer()); this.sendBtn.addEventListener('click', () => this.sendInput()); // Physical keyboard support document.addEventListener('keydown', (e) => this.handlePhysicalKey(e)); } inputCharacter(char) { this.keyboardBuffer += char.toUpperCase(); this.updateDisplay(); this.sendToROS('', false); // Update display on ROS } backspace() { this.keyboardBuffer = this.keyboardBuffer.slice(0, -1); this.updateDisplay(); this.sendToROS('', false); } clearBuffer() { this.keyboardBuffer = ''; this.updateDisplay(); this.sendToROS('[CLEAR]', false); } sendInput() { if (this.keyboardBuffer.trim()) { this.sendToROS('[SEND]', true); this.keyboardBuffer = ''; this.updateDisplay(); } } handlePhysicalKey(e) { if (e.target !== document.body) return; const char = e.key.toUpperCase(); if (e.key === 'Backspace') { e.preventDefault(); this.backspace(); } else if (e.key === 'Enter') { e.preventDefault(); this.sendInput(); } else if (char.match(/^[A-Z0-9 .,!?]$/)) { this.inputCharacter(char); } } updateDisplay() { this.keyboardBufferEl.textContent = this.keyboardBuffer || '(empty)'; this.renderTranscriptHistory(); } renderTranscriptHistory() { if (!this.displayData.history) return; let html = ''; this.displayData.history.forEach(entry => { const cls = entry.type === 'stt' ? 'transcript-stt' : 'transcript-keyboard'; const icon = entry.type === 'stt' ? '🎤' : '⌨️'; const text = entry.text || ''; html += `
${icon} ${this.escapeHtml(text)}
`; }); if (!html) { html = '
Waiting for speech...
'; } this.transcriptDisplay.innerHTML = html; // Auto-scroll to bottom this.transcriptDisplay.scrollTop = this.transcriptDisplay.scrollHeight; } sendToROS(command, isFinal) { // This will be called when we have a WebSocket connection to the ROS2 bridge const data = { command: command, buffer: this.keyboardBuffer, timestamp: Date.now(), final: isFinal }; console.log('Sending to ROS:', data); // Send via fetch API to UI server fetch('/api/keyboard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).catch(err => console.error('ROS send error:', err)); } startPolling() { // Poll for display updates from ROS2 setInterval(() => this.pollDisplayUpdate(), 500); } pollDisplayUpdate() { fetch('/api/display') .then(res => res.json()) .then(data => { if (data && data.history) { this.displayData = data; this.renderTranscriptHistory(); this.updateStatusIndicators(); } }) .catch(err => console.error('Display poll error:', err)); } updateStatusIndicators() { // Update STT/TTS status based on display data if (this.displayData.history && this.displayData.history.length > 0) { const lastEntry = this.displayData.history[this.displayData.history.length - 1]; if (lastEntry.type === 'stt') { this.sttStatusEl.textContent = lastEntry.final ? 'Complete' : 'Listening...'; } } } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.accessibilityUI = new AccessibilityUI(); });