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:
@@ -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 =
|
||||
'<div class="chat-header">' +
|
||||
'<h3>Asistente — Buscar partes</h3>' +
|
||||
'<button class="chat-header-close" id="chatClose" aria-label="Cerrar">×</button>' +
|
||||
'<div class="chat-header-actions">' +
|
||||
(hasTTS ? '<button class="chat-tts-toggle" id="chatTtsToggle" aria-label="Activar lectura de respuestas" title="Activar lectura de respuestas">🔊</button>' : '') +
|
||||
'<button class="chat-header-close" id="chatClose" aria-label="Cerrar">×</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="chat-messages" id="chatMessages">' +
|
||||
'<div class="chat-msg ai">Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.</div>' +
|
||||
@@ -32,6 +39,7 @@
|
||||
'</div>' +
|
||||
'<div class="chat-input-area">' +
|
||||
'<textarea class="chat-input" id="chatInput" placeholder="Ej: Balatas para Tsuru 2015..." rows="1"></textarea>' +
|
||||
(hasSpeechAPI ? '<button class="chat-mic-btn" id="chatMic" aria-label="Entrada por voz" title="Entrada por voz">🎤</button>' : '') +
|
||||
'<button class="chat-send-btn" id="chatSend" aria-label="Enviar">▶</button>' +
|
||||
'</div>';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user