feat(voice): implementa voz y TTS en chats POS y dashboard

- 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
This commit is contained in:
2026-04-28 00:53:57 +00:00
parent 1f909f4c42
commit afb3b2405c
20 changed files with 443 additions and 10 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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) {

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contabilidad — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -495,5 +496,6 @@
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Configuración — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -688,5 +689,6 @@
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Clientes</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -623,5 +624,6 @@
<script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexus Autoparts — Dashboard</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -460,5 +461,6 @@
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,11 +5,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diagramas — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
<link rel="stylesheet" href="/pos/static/css/pos-glass.css" />
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" />
@@ -154,5 +154,6 @@
<script src="/pos/static/js/sidebar.js" defer></script>
<script src="/pos/static/js/diagrams.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flotillas — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -308,5 +309,6 @@
<script src="/pos/static/js/offline-banner.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inventario — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -818,5 +819,6 @@
<script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Facturación CFDI — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -1057,5 +1058,6 @@
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Marketplace B2B — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -512,5 +513,6 @@
}
})();
</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cotizaciones — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -147,5 +148,6 @@
loadQuotes();
})();
</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reportes — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -323,5 +324,6 @@
<script src="/pos/static/js/sync-engine.js" defer></script>
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsApp — Nexus Autoparts POS</title>
<link rel="stylesheet" href="/pos/static/css/chat.css" />
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
<link rel="stylesheet" href="/pos/static/css/common.css" />
<link rel="stylesheet" href="/pos/static/css/sidebar.css" />
@@ -130,5 +131,6 @@ function posLogout(){localStorage.removeItem('pos_token');window.location.href='
<script src="/pos/static/js/pos-utils.js" defer></script>
<script src="/pos/static/js/sidebar.js" defer></script>
<script src="/pos/static/js/chat.js" defer></script>
</body>
</html>