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');