diff --git a/pos/static/css/chat.css b/pos/static/css/chat.css index c7892fc..a0ced4f 100644 --- a/pos/static/css/chat.css +++ b/pos/static/css/chat.css @@ -268,6 +268,66 @@ .chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } .chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } +/* ─── Mic Button (Voice Input) ─── */ + +.chat-mic-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-base); + color: var(--color-text-secondary); + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background var(--duration-fast) var(--ease-in-out), + color var(--duration-fast) var(--ease-in-out), + border-color var(--duration-fast) var(--ease-in-out); +} + +.chat-mic-btn:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.chat-mic-btn.listening { + background: #f85149; + border-color: #f85149; + color: #fff; + animation: micPulse 1.4s infinite; +} + +@keyframes micPulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.4); } + 50% { box-shadow: 0 0 0 10px rgba(248, 81, 73, 0); } +} + +/* ─── Voice Toast ─── */ + +.chat-voice-toast { + position: fixed; + bottom: 160px; + left: 50%; + transform: translateX(-50%) translateY(10px); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 8px 18px; + border-radius: var(--radius-md, 8px); + font-size: 0.85rem; + z-index: 9999; + opacity: 0; + transition: opacity 0.3s, transform 0.3s; + pointer-events: none; +} + +.chat-voice-toast.visible { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + /* ─── Vehicle Info Banner ─── */ .chat-vehicle-banner { diff --git a/pos/static/js/chat.js b/pos/static/js/chat.js index 875bc29..de3f72c 100644 --- a/pos/static/js/chat.js +++ b/pos/static/js/chat.js @@ -7,7 +7,10 @@ // ─── State ─── let isOpen = false; let isSending = false; + let isListening = false; + let recognition = null; const history = []; // conversation history for AI context + const hasSpeechAPI = ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window); // ─── Build DOM ─── function init() { @@ -36,6 +39,7 @@
+ ${hasSpeechAPI ? '' : ''}
`; @@ -54,6 +58,11 @@ } }); + // Mic button (only if Speech API available) + if (hasSpeechAPI) { + document.getElementById('chatMic').addEventListener('click', toggleVoice); + } + // Auto-resize textarea document.getElementById('chatInput').addEventListener('input', function () { this.style.height = 'auto'; @@ -61,6 +70,98 @@ }); } + // ─── Voice Input (Web Speech API) ─── + function toggleVoice() { + if (isListening) { + stopVoice(); + return; + } + startVoice(); + } + + function startVoice() { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognition) return; + + recognition = new SpeechRecognition(); + recognition.lang = 'es-MX'; + recognition.continuous = false; + recognition.interimResults = true; + + const input = document.getElementById('chatInput'); + const micBtn = document.getElementById('chatMic'); + const savedPlaceholder = input.placeholder; + + recognition.onstart = function () { + isListening = true; + micBtn.classList.add('listening'); + input.placeholder = 'Escuchando...'; + input.value = ''; + }; + + recognition.onresult = function (e) { + let interim = ''; + let finalTranscript = ''; + for (let i = e.resultIndex; i < e.results.length; i++) { + if (e.results[i].isFinal) { + finalTranscript += e.results[i][0].transcript; + } else { + interim += e.results[i][0].transcript; + } + } + if (finalTranscript) { + input.value = finalTranscript; + } else { + input.value = interim; + } + }; + + recognition.onend = function () { + isListening = false; + micBtn.classList.remove('listening'); + input.placeholder = savedPlaceholder; + recognition = null; + // Auto-send if we got text + if (input.value.trim()) { + sendMessage(); + } + }; + + recognition.onerror = function (e) { + isListening = false; + micBtn.classList.remove('listening'); + input.placeholder = savedPlaceholder; + recognition = null; + if (e.error === 'no-speech' || e.error === 'audio-capture' || e.error === 'not-allowed') { + showVoiceToast('No se detecto voz'); + } + }; + + recognition.start(); + } + + function stopVoice() { + if (recognition) { + recognition.abort(); + recognition = null; + } + isListening = false; + const micBtn = document.getElementById('chatMic'); + if (micBtn) micBtn.classList.remove('listening'); + } + + function showVoiceToast(msg) { + const toast = document.createElement('div'); + toast.className = 'chat-voice-toast'; + toast.textContent = msg; + document.body.appendChild(toast); + setTimeout(function () { toast.classList.add('visible'); }, 10); + setTimeout(function () { + toast.classList.remove('visible'); + setTimeout(function () { toast.remove(); }, 300); + }, 2000); + } + function toggleChat() { isOpen = !isOpen; const panel = document.getElementById('chatPanel');