From afb3b2405c5bcd15db0afc9203bb6061bcd86132 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 28 Apr 2026 00:53:57 +0000 Subject: [PATCH] feat(voice): implementa voz y TTS en chats POS y dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agrega TTS (speechSynthesis) a chat.js del POS para leer respuestas IA - Copia lógica de voz completa (STT + TTS) a dashboard/chat-public.js - Extiende estilos TTS en chat.css y chat-public.css - Agrega chat widget a 13 templates POS que no lo tenían - Corrige duplicado de chat.css en diagrams.html - Minifica assets actualizados - 73/73 tests pasan --- dashboard/chat-public.css | 95 ++++++++++++++++++++- dashboard/chat-public.js | 150 +++++++++++++++++++++++++++++++-- dashboard/chat-public.min.css | 95 ++++++++++++++++++++- dashboard/chat-public.min.js | 2 +- pos/static/css/chat.css | 21 +++++ pos/static/css/chat.min.css | 21 +++++ pos/static/js/chat.js | 42 +++++++++ pos/static/js/chat.min.js | 2 +- pos/templates/accounting.html | 2 + pos/templates/config.html | 2 + pos/templates/customers.html | 2 + pos/templates/dashboard.html | 2 + pos/templates/diagrams.html | 3 +- pos/templates/fleet.html | 2 + pos/templates/inventory.html | 2 + pos/templates/invoicing.html | 2 + pos/templates/marketplace.html | 2 + pos/templates/quotations.html | 2 + pos/templates/reports.html | 2 + pos/templates/whatsapp.html | 2 + 20 files changed, 443 insertions(+), 10 deletions(-) diff --git a/dashboard/chat-public.css b/dashboard/chat-public.css index 35381ac..523f75a 100644 --- a/dashboard/chat-public.css +++ b/dashboard/chat-public.css @@ -1,5 +1,5 @@ /* ========================================================================== - NEXUS — Public Catalog Chat Widget + NEXUS — Public Catalog Chat Widget (Voice + TTS enabled) Reuses design tokens from tokens.css ========================================================================== */ @@ -228,6 +228,99 @@ .chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } .chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } +/* ─── Header Actions (TTS toggle + close) ─── */ +.chat-header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.chat-tts-toggle { + background: none; + border: none; + color: #fff; + font-size: 1rem; + cursor: pointer; + padding: 4px; + line-height: 1; + opacity: 0.9; + transition: opacity 0.15s ease; +} + +.chat-tts-toggle:hover { opacity: 1; } +.chat-tts-toggle.off { opacity: 0.4; } + +/* ─── Mic Button (Voice Input) ─── */ +.chat-mic-btn { + width: 38px; + height: 38px; + border-radius: 8px; + border: 1px solid var(--color-border, #333); + background: var(--color-bg-base, #111); + color: var(--color-text-secondary, #aaa); + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.chat-mic-btn:hover { + border-color: var(--color-accent, #F5A623); + color: var(--color-accent, #F5A623); +} + +.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); } +} + +/* ─── TTS Button ─── */ +.chat-tts-btn { + background: none; + border: none; + color: #8b949e; + font-size: 0.85rem; + cursor: pointer; + padding: 2px 4px; + margin-left: 6px; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} +.chat-tts-btn:hover { color: #58a6ff; background: rgba(88,166,255,0.1); } +.chat-tts-btn.tts-active { color: #58a6ff; } + +/* ─── 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: 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); +} + @media (max-width: 480px) { .chat-panel { width: calc(100vw - 16px); diff --git a/dashboard/chat-public.js b/dashboard/chat-public.js index 6d9cb98..c3052e3 100644 --- a/dashboard/chat-public.js +++ b/dashboard/chat-public.js @@ -1,15 +1,20 @@ // /home/Autopartes/dashboard/chat-public.js -// Public catalog chatbot — no auth required, calls /api/chat +// Public catalog chatbot — voice + TTS enabled (function () { 'use strict'; var isOpen = false; var isSending = false; + var isListening = false; + var recognition = null; var history = []; + var ttsEnabled = true; + var ttsUtterance = null; + var hasSpeechAPI = ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window); + var hasTTS = ('speechSynthesis' in window); function init() { - // FAB button var fab = document.createElement('button'); fab.className = 'chat-fab'; fab.id = 'chatFab'; @@ -17,14 +22,16 @@ fab.innerHTML = '💬'; fab.setAttribute('aria-label', 'Abrir asistente IA'); - // Chat panel var panel = document.createElement('div'); panel.className = 'chat-panel'; panel.id = 'chatPanel'; panel.innerHTML = '
' + '

Asistente — Buscar partes

' + - '' + + '
' + + (hasTTS ? '' : '') + + '' + + '
' + '
' + '
' + '
Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.
' + @@ -32,6 +39,7 @@ '
' + '
' + '' + + (hasSpeechAPI ? '' : '') + '' + '
'; @@ -52,8 +60,139 @@ this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 80) + 'px'; }); + + if (hasSpeechAPI) { + document.getElementById('chatMic').addEventListener('click', toggleVoice); + } + + if (hasTTS) { + document.getElementById('chatTtsToggle').addEventListener('click', toggleTTS); + } + + // Stop TTS when closing chat + document.getElementById('chatClose').addEventListener('click', function () { + if (hasTTS) stopSpeaking(); + }); } + // ─── TTS ─── + function toggleTTS() { + ttsEnabled = !ttsEnabled; + var btn = document.getElementById('chatTtsToggle'); + if (btn) { + btn.classList.toggle('off', !ttsEnabled); + btn.setAttribute('title', ttsEnabled ? 'Desactivar lectura de respuestas' : 'Activar lectura de respuestas'); + } + if (!ttsEnabled) stopSpeaking(); + } + + function speak(text) { + if (!hasTTS || !ttsEnabled || !text) return; + stopSpeaking(); + ttsUtterance = new SpeechSynthesisUtterance(text); + ttsUtterance.lang = 'es-MX'; + ttsUtterance.rate = 1.1; + ttsUtterance.pitch = 1; + window.speechSynthesis.speak(ttsUtterance); + } + + function stopSpeaking() { + if (hasTTS && window.speechSynthesis.speaking) { + window.speechSynthesis.cancel(); + } + ttsUtterance = null; + } + + // ─── Voice Input ─── + function toggleVoice() { + if (isListening) { stopVoice(); return; } + startVoice(); + } + + function startVoice() { + var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognition) return; + + recognition = new SpeechRecognition(); + recognition.lang = 'es-MX'; + recognition.continuous = false; + recognition.interimResults = true; + + var input = document.getElementById('chatInput'); + var micBtn = document.getElementById('chatMic'); + var savedPlaceholder = input.placeholder; + + recognition.onstart = function () { + isListening = true; + micBtn.classList.add('listening'); + input.placeholder = 'Escuchando...'; + input.value = ''; + stopSpeaking(); + }; + + recognition.onresult = function (e) { + var interim = ''; + var finalTranscript = ''; + for (var 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; + 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; + var micBtn = document.getElementById('chatMic'); + if (micBtn) micBtn.classList.remove('listening'); + } + + function showVoiceToast(msg) { + var 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); + } + + // ─── Chat UI ─── function toggleChat() { isOpen = !isOpen; var panel = document.getElementById('chatPanel'); @@ -104,6 +243,8 @@ addBubble(aiMsg, 'ai'); history.push({ role: 'assistant', content: aiMsg }); + if (ttsEnabled) speak(aiMsg); + if (data.search_results && data.search_results.length > 0) { addPartResults(data.search_results); } @@ -149,7 +290,6 @@ card.style.cursor = 'pointer'; card.addEventListener('click', function () { - // Search in catalog var searchInput = document.getElementById('searchInput'); if (searchInput && partNum) { searchInput.value = partNum; diff --git a/dashboard/chat-public.min.css b/dashboard/chat-public.min.css index 35381ac..523f75a 100644 --- a/dashboard/chat-public.min.css +++ b/dashboard/chat-public.min.css @@ -1,5 +1,5 @@ /* ========================================================================== - NEXUS — Public Catalog Chat Widget + NEXUS — Public Catalog Chat Widget (Voice + TTS enabled) Reuses design tokens from tokens.css ========================================================================== */ @@ -228,6 +228,99 @@ .chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } .chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } +/* ─── Header Actions (TTS toggle + close) ─── */ +.chat-header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.chat-tts-toggle { + background: none; + border: none; + color: #fff; + font-size: 1rem; + cursor: pointer; + padding: 4px; + line-height: 1; + opacity: 0.9; + transition: opacity 0.15s ease; +} + +.chat-tts-toggle:hover { opacity: 1; } +.chat-tts-toggle.off { opacity: 0.4; } + +/* ─── Mic Button (Voice Input) ─── */ +.chat-mic-btn { + width: 38px; + height: 38px; + border-radius: 8px; + border: 1px solid var(--color-border, #333); + background: var(--color-bg-base, #111); + color: var(--color-text-secondary, #aaa); + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.chat-mic-btn:hover { + border-color: var(--color-accent, #F5A623); + color: var(--color-accent, #F5A623); +} + +.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); } +} + +/* ─── TTS Button ─── */ +.chat-tts-btn { + background: none; + border: none; + color: #8b949e; + font-size: 0.85rem; + cursor: pointer; + padding: 2px 4px; + margin-left: 6px; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} +.chat-tts-btn:hover { color: #58a6ff; background: rgba(88,166,255,0.1); } +.chat-tts-btn.tts-active { color: #58a6ff; } + +/* ─── 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: 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); +} + @media (max-width: 480px) { .chat-panel { width: calc(100vw - 16px); diff --git a/dashboard/chat-public.min.js b/dashboard/chat-public.min.js index 7fa9053..e65147e 100644 --- a/dashboard/chat-public.min.js +++ b/dashboard/chat-public.min.js @@ -1 +1 @@ -!function(){"use strict";var e=!1,t=!1,n=[];function a(){var e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");var t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML='

Asistente — Buscar partes

Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.
',document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",s),document.getElementById("chatClose").addEventListener("click",s),document.getElementById("chatSend").addEventListener("click",c),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),c())})),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function s(){e=!e;var t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function c(){if(!t){var e=document.getElementById("chatInput"),a=e.value.trim();a&&(e.value="",e.style.height="auto",i(a,"user"),n.push({role:"user",content:a}),n.length>20&&n.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,d(!0),fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:a,history:n.slice(-10)})}).then((function(e){return e.json()})).then((function(e){if(e.error)i("Error: "+e.error,"ai");else{var t,a,c,d,l=e.response||"Sin respuesta.";i(l,"ai"),n.push({role:"assistant",content:l}),e.search_results&&e.search_results.length>0&&(t=e.search_results,a=document.getElementById("chatMessages"),c=document.getElementById("chatTyping"),(d=document.createElement("div")).className="chat-parts",t.slice(0,8).forEach((function(e){var t=document.createElement("div");t.className="chat-part-card";var n=e.name_es||e.name_part||"",a=e.oem_part_number||e.part_number||"",c=e.brand||"";t.innerHTML='
'+o(a)+'
'+o(n)+(c?' ('+o(c)+")":"")+"
",t.style.cursor="pointer",t.addEventListener("click",(function(){var e=document.getElementById("searchInput");e&&a&&(e.value=a,"function"==typeof window.doSearch&&window.doSearch(),s())})),d.appendChild(t)})),a.insertBefore(d,c),r())}})).catch((function(e){i("Error de conexion: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,d(!1)})))}}function i(e,t){var n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),r()}function d(e){var t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&r()}function r(){var e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function o(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",a):a()}(); \ No newline at end of file +!function(){"use strict";var e=!1,t=!1,n=!1,a=null,s=[],c=!0,i=null,o="webkitSpeechRecognition"in window||"SpeechRecognition"in window,r="speechSynthesis"in window;function l(){var e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");var t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML='

Asistente — Buscar partes

'+(r?'':"")+'
Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.
'+(o?'':"")+'
',document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",m),document.getElementById("chatClose").addEventListener("click",m),document.getElementById("chatSend").addEventListener("click",p),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),p())})),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"})),o&&document.getElementById("chatMic").addEventListener("click",h),r&&document.getElementById("chatTtsToggle").addEventListener("click",d),document.getElementById("chatClose").addEventListener("click",(function(){r&&u()}))}function d(){c=!c;var e=document.getElementById("chatTtsToggle");e&&(e.classList.toggle("off",!c),e.setAttribute("title",c?"Desactivar lectura de respuestas":"Activar lectura de respuestas")),c||u()}function u(){r&&window.speechSynthesis.speaking&&window.speechSynthesis.cancel(),i=null}function h(){n?function(){a&&(a.abort(),a=null);n=!1;var e=document.getElementById("chatMic");e&&e.classList.remove("listening")}():function(){var e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e)return;(a=new e).lang="es-MX",a.continuous=!1,a.interimResults=!0;var t=document.getElementById("chatInput"),s=document.getElementById("chatMic"),c=t.placeholder;a.onstart=function(){n=!0,s.classList.add("listening"),t.placeholder="Escuchando...",t.value="",u()},a.onresult=function(e){for(var n="",a="",s=e.resultIndex;s20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0),fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:n,history:s.slice(-10)})}).then((function(e){return e.json()})).then((function(e){if(e.error)v("Error: "+e.error,"ai");else{var t,n,a,o,l=e.response||"Sin respuesta.";v(l,"ai"),s.push({role:"assistant",content:l}),c&&function(e){r&&c&&e&&(u(),(i=new SpeechSynthesisUtterance(e)).lang="es-MX",i.rate=1.1,i.pitch=1,window.speechSynthesis.speak(i))}(l),e.search_results&&e.search_results.length>0&&(t=e.search_results,n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),(o=document.createElement("div")).className="chat-parts",t.slice(0,8).forEach((function(e){var t=document.createElement("div");t.className="chat-part-card";var n=e.name_es||e.name_part||"",a=e.oem_part_number||e.part_number||"",s=e.brand||"";t.innerHTML='
'+f(a)+'
'+f(n)+(s?' ('+f(s)+")":"")+"
",t.style.cursor="pointer",t.addEventListener("click",(function(){var e=document.getElementById("searchInput");e&&a&&(e.value=a,"function"==typeof window.doSearch&&window.doSearch(),m())})),o.appendChild(t)})),n.insertBefore(o,a),y())}})).catch((function(e){v("Error de conexion: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,g(!1)})))}}function v(e,t){var n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),y()}function g(e){var t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&y()}function y(){var e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function f(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):l()}(); \ No newline at end of file diff --git a/pos/static/css/chat.css b/pos/static/css/chat.css index ec6a47e..a02936d 100644 --- a/pos/static/css/chat.css +++ b/pos/static/css/chat.css @@ -104,6 +104,27 @@ .chat-header-close:hover { opacity: 1; } +.chat-header-actions { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.chat-tts-toggle { + background: none; + border: none; + color: #fff; + font-size: 1rem; + cursor: pointer; + padding: var(--space-1); + line-height: 1; + opacity: 0.9; + transition: opacity var(--duration-fast) var(--ease-in-out); +} + +.chat-tts-toggle:hover { opacity: 1; } +.chat-tts-toggle.off { opacity: 0.35; } + /* ─── Messages Area ─── */ .chat-messages { diff --git a/pos/static/css/chat.min.css b/pos/static/css/chat.min.css index ec6a47e..a02936d 100644 --- a/pos/static/css/chat.min.css +++ b/pos/static/css/chat.min.css @@ -104,6 +104,27 @@ .chat-header-close:hover { opacity: 1; } +.chat-header-actions { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.chat-tts-toggle { + background: none; + border: none; + color: #fff; + font-size: 1rem; + cursor: pointer; + padding: var(--space-1); + line-height: 1; + opacity: 0.9; + transition: opacity var(--duration-fast) var(--ease-in-out); +} + +.chat-tts-toggle:hover { opacity: 1; } +.chat-tts-toggle.off { opacity: 0.35; } + /* ─── Messages Area ─── */ .chat-messages { diff --git a/pos/static/js/chat.js b/pos/static/js/chat.js index d7bb979..ddcaac7 100644 --- a/pos/static/js/chat.js +++ b/pos/static/js/chat.js @@ -9,8 +9,11 @@ let isSending = false; let isListening = false; let recognition = null; + let ttsEnabled = true; + let ttsUtterance = null; const history = []; // conversation history for AI context const hasSpeechAPI = ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window); + const hasTTS = ('speechSynthesis' in window); // ─── Build DOM ─── function init() { @@ -71,6 +74,16 @@ document.getElementById('chatMic').addEventListener('click', toggleVoice); } + // TTS toggle + if (hasTTS) { + document.getElementById('chatTtsToggle').addEventListener('click', toggleTTS); + } + + // Stop TTS when closing + document.getElementById('chatClose').addEventListener('click', function () { + if (hasTTS) stopSpeaking(); + }); + // Auto-resize textarea document.getElementById('chatInput').addEventListener('input', function () { this.style.height = 'auto'; @@ -170,6 +183,34 @@ }, 2000); } + // ─── TTS (Text-to-Speech) ─── + function toggleTTS() { + ttsEnabled = !ttsEnabled; + const btn = document.getElementById('chatTtsToggle'); + if (btn) { + btn.classList.toggle('off', !ttsEnabled); + btn.setAttribute('title', ttsEnabled ? 'Desactivar lectura de respuestas' : 'Activar lectura de respuestas'); + } + if (!ttsEnabled) stopSpeaking(); + } + + function speak(text) { + if (!hasTTS || !ttsEnabled || !text) return; + stopSpeaking(); + ttsUtterance = new SpeechSynthesisUtterance(text); + ttsUtterance.lang = 'es-MX'; + ttsUtterance.rate = 1.1; + ttsUtterance.pitch = 1; + window.speechSynthesis.speak(ttsUtterance); + } + + function stopSpeaking() { + if (hasTTS && window.speechSynthesis.speaking) { + window.speechSynthesis.cancel(); + } + ttsUtterance = null; + } + // ─── Image Upload (Part identification placeholder) ─── function handleImageUpload(e) { const file = e.target.files && e.target.files[0]; @@ -314,6 +355,7 @@ const aiMsg = data.response || 'Sin respuesta.'; addBubble(aiMsg, 'ai'); history.push({ role: 'assistant', content: aiMsg }); + speak(aiMsg); // Vehicle info if (data.vehicle && data.vehicle.brand_id) { diff --git a/pos/static/js/chat.min.js b/pos/static/js/chat.min.js index 04f8242..f11e8cf 100644 --- a/pos/static/js/chat.min.js +++ b/pos/static/js/chat.min.js @@ -1 +1 @@ -!function(){"use strict";let e=!1,t=!1,n=!1,a=null;const s=[],i="webkitSpeechRecognition"in window||"SpeechRecognition"in window;function c(){const e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");const t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML=`\n
\n

Asistente IA — Buscar partes

\n \n
\n
\n
Hola, soy el asistente de Nexus. Dime que refaccion necesitas y te ayudo a encontrarla.
\n
\n \n
\n
\n
\n \n \n \n ${i?'':""}\n \n
\n `,document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",l),document.getElementById("chatClose").addEventListener("click",l),document.getElementById("chatSend").addEventListener("click",u),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),u())})),document.getElementById("chatCam").addEventListener("click",(function(){document.getElementById("chatImageInput").click()})),document.getElementById("chatImageInput").addEventListener("change",r),i&&document.getElementById("chatMic").addEventListener("click",o),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function o(){n?function(){a&&(a.abort(),a=null);n=!1;const e=document.getElementById("chatMic");e&&e.classList.remove("listening")}():function(){const e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e)return;a=new e,a.lang="es-MX",a.continuous=!1,a.interimResults=!0;const t=document.getElementById("chatInput"),s=document.getElementById("chatMic"),i=t.placeholder;a.onstart=function(){n=!0,s.classList.add("listening"),t.placeholder="Escuchando...",t.value=""},a.onresult=function(e){let n="",a="";for(let t=e.resultIndex;t5242880)return void m("La imagen es muy grande (max 5MB).","ai");const a=new FileReader;a.onload=function(e){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),i=document.createElement("div");i.className="chat-msg user chat-msg-image",i.innerHTML='Foto de parteIdentificar esta parte',n.insertBefore(i,a),y();var c=e.target.result,o="Identifica esta parte automotriz y sugiere terminos de busqueda.";s.push({role:"user",content:o}),s.length>20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0);var r=d();fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+r},body:JSON.stringify({message:o,image:c,history:s.slice(-10)})}).then((function(e){return e.json()})).then((function(e){const t=e.response||"No pude identificar la parte. Intenta describirla con texto.";m(t,"ai"),s.push({role:"assistant",content:t}),e.vehicle&&e.vehicle.brand_id&&p(e.vehicle),e.search_results&&e.search_results.length>0&&h(e.search_results)})).catch((function(e){m("Error al procesar imagen: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,g(!1)}))},a.readAsDataURL(n)}function l(){e=!e;const t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function d(){return window.__pos&&window.__pos.token?window.__pos.token:localStorage.getItem("pos_token")||""}async function u(){if(t)return;const e=document.getElementById("chatInput"),n=e.value.trim();if(n){e.value="",e.style.height="auto",m(n,"user"),s.push({role:"user",content:n}),s.length>20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0);try{const e=d(),t=await fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+e},body:JSON.stringify({message:n,history:s.slice(-10)})}),a=await t.json();if(!t.ok)return void m("Error: "+(a.error||t.statusText),"ai");const i=a.response||"Sin respuesta.";m(i,"ai"),s.push({role:"assistant",content:i}),a.vehicle&&a.vehicle.brand_id&&p(a.vehicle),a.search_results&&a.search_results.length>0&&h(a.search_results)}catch(e){m("Error de conexion: "+e.message,"ai")}finally{t=!1,document.getElementById("chatSend").disabled=!1,g(!1)}}}function m(e,t){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),y()}function p(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-vehicle-banner";let s=""+f(e.brand||"")+" "+f(e.model||"")+"";e.year&&(s+=" "+e.year),e.mye_options&&e.mye_options.length>0&&(s+="
Motorizaciones encontradas:",e.mye_options.forEach((function(e){s+="
• "+f(e.engine),e.trim&&(s+=" ("+f(e.trim)+")")}))),a.innerHTML=s,t.insertBefore(a,n),y()}function h(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-parts",e.slice(0,8).forEach((function(e){const t=document.createElement("div");t.className="chat-part-card";const n="local"===e.source,s=e.local_stock||0,i=s>0?"in-stock":"",c=s>0?s+" en stock":"Sin stock local",o=e.name_es||e.name_part||"",r=e.oem_part_number||e.part_number||"",d=e.brand||"",u=e.price_1?"$"+parseFloat(e.price_1).toFixed(2):"",m=n?'MI INVENTARIO':'CATÁLOGO';t.innerHTML='
'+f(r)+m+(u?" — "+u:"")+'
'+f(o)+(d?' ('+f(d)+")":"")+'
'+f(c)+"
",t.addEventListener("click",(function(){e.id_part&&"function"==typeof window.openPartDetail&&(window.openPartDetail(e.id_part),l())})),a.appendChild(t)})),t.insertBefore(a,n),y()}function g(e){const t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&y()}function y(){const e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function f(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()}(); \ No newline at end of file +!function(){"use strict";let e=!1,t=!1,n=!1,a=null,s=!0,i=null;const c=[],o="webkitSpeechRecognition"in window||"SpeechRecognition"in window,r="speechSynthesis"in window;function l(){const e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");const t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML=`\n
\n

Asistente IA — Buscar partes

\n \n
\n
\n
Hola, soy el asistente de Nexus. Dime que refaccion necesitas y te ayudo a encontrarla.
\n
\n \n
\n
\n
\n \n \n \n ${o?'':""}\n \n
\n `,document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",p),document.getElementById("chatClose").addEventListener("click",p),document.getElementById("chatSend").addEventListener("click",y),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),y())})),document.getElementById("chatCam").addEventListener("click",(function(){document.getElementById("chatImageInput").click()})),document.getElementById("chatImageInput").addEventListener("change",h),o&&document.getElementById("chatMic").addEventListener("click",d),r&&document.getElementById("chatTtsToggle").addEventListener("click",u),document.getElementById("chatClose").addEventListener("click",(function(){r&&m()})),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function d(){n?function(){a&&(a.abort(),a=null);n=!1;const e=document.getElementById("chatMic");e&&e.classList.remove("listening")}():function(){const e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e)return;a=new e,a.lang="es-MX",a.continuous=!1,a.interimResults=!0;const t=document.getElementById("chatInput"),s=document.getElementById("chatMic"),i=t.placeholder;a.onstart=function(){n=!0,s.classList.add("listening"),t.placeholder="Escuchando...",t.value=""},a.onresult=function(e){let n="",a="";for(let t=e.resultIndex;t5242880)return void f("La imagen es muy grande (max 5MB).","ai");const a=new FileReader;a.onload=function(e){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg user chat-msg-image",s.innerHTML='Foto de parteIdentificar esta parte',n.insertBefore(s,a),I();var i=e.target.result,o="Identifica esta parte automotriz y sugiere terminos de busqueda.";c.push({role:"user",content:o}),c.length>20&&c.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,b(!0);var r=g();fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+r},body:JSON.stringify({message:o,image:i,history:c.slice(-10)})}).then((function(e){return e.json()})).then((function(e){const t=e.response||"No pude identificar la parte. Intenta describirla con texto.";f(t,"ai"),c.push({role:"assistant",content:t}),e.vehicle&&e.vehicle.brand_id&&v(e.vehicle),e.search_results&&e.search_results.length>0&&E(e.search_results)})).catch((function(e){f("Error al procesar imagen: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,b(!1)}))},a.readAsDataURL(n)}function p(){e=!e;const t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function g(){return window.__pos&&window.__pos.token?window.__pos.token:localStorage.getItem("pos_token")||""}async function y(){if(t)return;const e=document.getElementById("chatInput"),n=e.value.trim();if(n){e.value="",e.style.height="auto",f(n,"user"),c.push({role:"user",content:n}),c.length>20&&c.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,b(!0);try{const e=g(),t=await fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+e},body:JSON.stringify({message:n,history:c.slice(-10)})}),a=await t.json();if(!t.ok)return void f("Error: "+(a.error||t.statusText),"ai");const o=a.response||"Sin respuesta.";f(o,"ai"),c.push({role:"assistant",content:o}),function(e){r&&s&&e&&(m(),i=new SpeechSynthesisUtterance(e),i.lang="es-MX",i.rate=1.1,i.pitch=1,window.speechSynthesis.speak(i))}(o),a.vehicle&&a.vehicle.brand_id&&v(a.vehicle),a.search_results&&a.search_results.length>0&&E(a.search_results)}catch(e){f("Error de conexion: "+e.message,"ai")}finally{t=!1,document.getElementById("chatSend").disabled=!1,b(!1)}}}function f(e,t){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),I()}function v(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-vehicle-banner";let s=""+B(e.brand||"")+" "+B(e.model||"")+"";e.year&&(s+=" "+e.year),e.mye_options&&e.mye_options.length>0&&(s+="
Motorizaciones encontradas:",e.mye_options.forEach((function(e){s+="
• "+B(e.engine),e.trim&&(s+=" ("+B(e.trim)+")")}))),a.innerHTML=s,t.insertBefore(a,n),I()}function E(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-parts",e.slice(0,8).forEach((function(e){const t=document.createElement("div");t.className="chat-part-card";const n="local"===e.source,s=e.local_stock||0,i=s>0?"in-stock":"",c=s>0?s+" en stock":"Sin stock local",o=e.name_es||e.name_part||"",r=e.oem_part_number||e.part_number||"",l=e.brand||"",d=e.price_1?"$"+parseFloat(e.price_1).toFixed(2):"",u=n?'MI INVENTARIO':'CATÁLOGO';t.innerHTML='
'+B(r)+u+(d?" — "+d:"")+'
'+B(o)+(l?' ('+B(l)+")":"")+'
'+B(c)+"
",t.addEventListener("click",(function(){e.id_part&&"function"==typeof window.openPartDetail&&(window.openPartDetail(e.id_part),p())})),a.appendChild(t)})),t.insertBefore(a,n),I()}function b(e){const t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&I()}function I(){const e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function B(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",l):l()}(); \ No newline at end of file diff --git a/pos/templates/accounting.html b/pos/templates/accounting.html index 7058d42..5f792a0 100644 --- a/pos/templates/accounting.html +++ b/pos/templates/accounting.html @@ -5,6 +5,7 @@ Contabilidad — Nexus Autoparts POS + @@ -495,5 +496,6 @@ + diff --git a/pos/templates/config.html b/pos/templates/config.html index b6e0e43..71f80f6 100644 --- a/pos/templates/config.html +++ b/pos/templates/config.html @@ -5,6 +5,7 @@ Configuración — Nexus Autoparts POS + @@ -688,5 +689,6 @@ + diff --git a/pos/templates/customers.html b/pos/templates/customers.html index 64314f0..9d98b6b 100644 --- a/pos/templates/customers.html +++ b/pos/templates/customers.html @@ -5,6 +5,7 @@ Nexus Autoparts — Clientes + @@ -623,5 +624,6 @@ + diff --git a/pos/templates/dashboard.html b/pos/templates/dashboard.html index 36c0d8d..0f58059 100644 --- a/pos/templates/dashboard.html +++ b/pos/templates/dashboard.html @@ -5,6 +5,7 @@ Nexus Autoparts — Dashboard + @@ -460,5 +461,6 @@ + diff --git a/pos/templates/diagrams.html b/pos/templates/diagrams.html index 0df137d..4c9d1a8 100644 --- a/pos/templates/diagrams.html +++ b/pos/templates/diagrams.html @@ -5,11 +5,11 @@ Diagramas — Nexus Autoparts POS + - @@ -154,5 +154,6 @@ + diff --git a/pos/templates/fleet.html b/pos/templates/fleet.html index 740974d..1ce78aa 100644 --- a/pos/templates/fleet.html +++ b/pos/templates/fleet.html @@ -5,6 +5,7 @@ Flotillas — Nexus Autoparts POS + @@ -308,5 +309,6 @@ + diff --git a/pos/templates/inventory.html b/pos/templates/inventory.html index 9438269..b70d91a 100644 --- a/pos/templates/inventory.html +++ b/pos/templates/inventory.html @@ -5,6 +5,7 @@ Inventario — Nexus Autoparts POS + @@ -818,5 +819,6 @@ + diff --git a/pos/templates/invoicing.html b/pos/templates/invoicing.html index 7882216..2565e91 100644 --- a/pos/templates/invoicing.html +++ b/pos/templates/invoicing.html @@ -5,6 +5,7 @@ Facturación CFDI — Nexus Autoparts POS + @@ -1057,5 +1058,6 @@ + diff --git a/pos/templates/marketplace.html b/pos/templates/marketplace.html index 1455d41..c712570 100644 --- a/pos/templates/marketplace.html +++ b/pos/templates/marketplace.html @@ -5,6 +5,7 @@ Marketplace B2B — Nexus Autoparts POS + @@ -512,5 +513,6 @@ } })(); + diff --git a/pos/templates/quotations.html b/pos/templates/quotations.html index 5688f9e..bba0c86 100644 --- a/pos/templates/quotations.html +++ b/pos/templates/quotations.html @@ -5,6 +5,7 @@ Cotizaciones — Nexus Autoparts POS + @@ -147,5 +148,6 @@ loadQuotes(); })(); + diff --git a/pos/templates/reports.html b/pos/templates/reports.html index 5cdb5ea..82891e6 100644 --- a/pos/templates/reports.html +++ b/pos/templates/reports.html @@ -5,6 +5,7 @@ Reportes — Nexus Autoparts POS + @@ -323,5 +324,6 @@ + diff --git a/pos/templates/whatsapp.html b/pos/templates/whatsapp.html index 974f193..2bc7120 100644 --- a/pos/templates/whatsapp.html +++ b/pos/templates/whatsapp.html @@ -5,6 +5,7 @@ WhatsApp — Nexus Autoparts POS + @@ -130,5 +131,6 @@ function posLogout(){localStorage.removeItem('pos_token');window.location.href=' +