// Variables globales let recognition = null; let isProcessingSpeech = false; let isPlayingAudio = false; let currentMode = 'soporte'; let currentModel = 'gemini'; let currentTTS = 'EDGE'; let isListening = false; let sessionId = null; let shouldRestartRecognition = true; // Variables globales para audio let currentAudio = null; let isPlaying = false; let audioQueue = []; let isProcessingQueue = false; // Variables para control de interrupción let lastInterruptTime = 0; const INTERRUPT_COOLDOWN = 1000; // 1 segundo entre interrupciones const ENERGY_THRESHOLD = 1.5; // Umbral de energía para detectar interrupción let audioContext = null; let analyser = null; let microphoneStream = null; const STOP_WORDS = ['alto', 'detente', 'permiteme', 'callate', 'silencio', 'calla', 'para', 'espera']; const GREETING_WORDS = ['hola', 'buenas', 'buenos días', 'buenas tardes', 'buenas noches']; const INTEREST_WORDS = ['acepto', 'me interesa', 'dime más', 'cuéntame más', 'quiero saber más']; // Elementos del DOM const chatBox = document.getElementById('chatBox'); const textInput = document.getElementById('textInput'); const sendTextButton = document.getElementById('sendText'); const modoSelect = document.getElementById('modoSelect'); const modeloSelect = document.getElementById('modeloSelect'); const vozSelect = document.getElementById('vozSelect'); const configForm = document.getElementById('configForm'); const statusLabel = document.getElementById('statusLabel'); const startRecordingButton = document.getElementById('startRecording'); // Comandos de voz const VOICE_COMMANDS = { stop: ['para', 'detente', 'silencio', 'cállate', 'espera', 'stop', 'alto', 'basta', 'suficiente', 'ya', 'shh', 'sh'], greet: ['hola', 'buenas', 'buenos días', 'buenas tardes', 'buenas noches'], interest: ['acepto', 'me interesa', 'dime más', 'cuéntame más', 'quiero saber más'] }; // Funciones de utilidad const Utils = { checkCommand(text, type) { return VOICE_COMMANDS[type].some(cmd => text.toLowerCase().includes(cmd)); }, updateStatus(text, type = 'info') { if (statusLabel) { statusLabel.textContent = text; statusLabel.classList.remove('error', 'success'); if (type === 'error' || type === 'success') { statusLabel.classList.add(type); } } } }; // Funciones de chat const ChatManager = { addMessage(text, sender) { if (!text || !sender || !chatBox) return; try { console.log('Agregando mensaje al chat:', { text, sender }); const messageDiv = document.createElement('div'); messageDiv.className = `message ${sender}-message`; const iconSpan = document.createElement('span'); iconSpan.className = 'message-icon'; iconSpan.textContent = sender === 'user' ? '👤' : '🤖'; const textSpan = document.createElement('span'); textSpan.className = 'message-text'; textSpan.textContent = text; messageDiv.appendChild(iconSpan); messageDiv.appendChild(textSpan); chatBox.appendChild(messageDiv); // Hacer scroll al último mensaje chatBox.scrollTop = chatBox.scrollHeight; console.log('Mensaje agregado exitosamente'); } catch (error) { console.error('Error al agregar mensaje:', error); } }, async sendMessage(text) { if (!text) return; try { Utils.updateStatus('Procesando...'); this.addMessage(text, 'user'); textInput.value = ''; const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mensaje: text, mode: currentMode, model: currentModel, tts: currentTTS }) }); if (!response.ok) { throw new Error(`Error HTTP: ${response.status}`); } const data = await response.json(); if (data.success && data.texto) { this.addMessage(data.texto, 'bot'); if (data.audio) { try { const audioType = data.audio_type || 'mp3'; await AudioManager.playAudio(data.audio, audioType); } catch (audioError) { console.error('Error reproduciendo audio:', audioError); handleAudioError(); } } } else { throw new Error(data.error || 'Error desconocido'); } } catch (error) { console.error('Error:', error); Utils.updateStatus('Error de conexión', 'error'); this.addMessage('Lo siento, hubo un error al procesar tu mensaje.', 'bot'); } finally { Utils.updateStatus('Escuchando...'); } } }; // Gestión de audio const AudioManager = { async playAudio(base64Audio, audioType = 'wav') { if (!base64Audio) return; try { if (isPlayingAudio) { audioQueue.push({ base64Audio, audioType }); return; } isPlayingAudio = true; Utils.updateStatus('Reproduciendo audio...'); const audioBlob = await fetch(`data:audio/${audioType};base64,${base64Audio}`) .then(res => res.blob()); currentAudio = new Audio(URL.createObjectURL(audioBlob)); currentAudio.onended = () => { isPlayingAudio = false; URL.revokeObjectURL(currentAudio.src); currentAudio = null; Utils.updateStatus('Escuchando...'); this.processQueue(); }; await currentAudio.play(); } catch (error) { console.error('Error reproduciendo audio:', error); this.handleError(); } }, async processQueue() { if (isProcessingQueue || audioQueue.length === 0) return; isProcessingQueue = true; const nextAudio = audioQueue.shift(); await this.playAudio(nextAudio.base64Audio, nextAudio.audioType); isProcessingQueue = false; }, handleError() { isPlayingAudio = false; if (currentAudio) { URL.revokeObjectURL(currentAudio.src); currentAudio = null; } Utils.updateStatus('Error de audio', 'error'); }, stop() { if (currentAudio) { currentAudio.pause(); URL.revokeObjectURL(currentAudio.src); currentAudio = null; } isPlayingAudio = false; audioQueue = []; Utils.updateStatus('Audio detenido'); } }; // Gestión de voz const VoiceManager = { async processCommand(text) { if (!text) return false; const currentTime = Date.now(); if (Utils.checkCommand(text, 'stop')) { if (currentTime - lastInterruptTime > INTERRUPT_COOLDOWN) { lastInterruptTime = currentTime; AudioManager.stop(); Utils.updateStatus('Audio detenido por comando de voz'); return true; } } if (Utils.checkCommand(text, 'greet') || Utils.checkCommand(text, 'interest')) { if (currentTime - lastInterruptTime > INTERRUPT_COOLDOWN) { lastInterruptTime = currentTime; if (isPlayingAudio) { AudioManager.stop(); await new Promise(resolve => setTimeout(resolve, 300)); } ChatManager.addMessage(text, 'user'); await this.processVoiceResponse(text); return true; } } return false; }, async processVoiceResponse(text) { try { shouldRestartRecognition = false; if (recognition && isListening) { recognition.stop(); } const response = await fetch('/procesar_voz', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ texto: text, mode: currentMode, model: currentModel, tts: currentTTS }) }); if (!response.ok) throw new Error(`Error HTTP: ${response.status}`); const data = await response.json(); if (data.success && data.texto) { ChatManager.addMessage(data.texto, 'bot'); if (data.audio) { await AudioManager.playAudio(data.audio, data.audio_type || 'wav'); } } } catch (error) { console.error('Error procesando voz:', error); Utils.updateStatus('Error procesando voz', 'error'); } finally { shouldRestartRecognition = true; if (!isListening) { await this.startRecognition(); } } }, async startRecognition() { if (!recognition || !isListening) { try { await this.initializeSpeechRecognition(); isListening = true; startRecordingButton.classList.add('active'); Utils.updateStatus('Escuchando...'); } catch (error) { console.error('Error iniciando reconocimiento:', error); Utils.updateStatus('Error de micrófono', 'error'); } } }, async pauseRecognition() { if (recognition && isListening) { recognition.stop(); isListening = false; startRecordingButton.classList.remove('active'); Utils.updateStatus('Reconocimiento pausado'); } }, async initializeSpeechRecognition() { if (!('webkitSpeechRecognition' in window)) { throw new Error('Reconocimiento de voz no soportado'); } recognition = new webkitSpeechRecognition(); recognition.continuous = true; recognition.interimResults = true; recognition.lang = 'es-ES'; recognition.onstart = () => { isListening = true; Utils.updateStatus('Escuchando...'); }; recognition.onend = () => { isListening = false; if (shouldRestartRecognition) { recognition.start(); } }; recognition.onresult = async (event) => { if (isProcessingSpeech) return; const results = Array.from(event.results); const lastResult = results[results.length - 1]; if (lastResult.isFinal) { const text = lastResult[0].transcript.trim(); if (text) { isProcessingSpeech = true; await this.processCommand(text); isProcessingSpeech = false; } } }; recognition.onerror = (event) => { console.error('Error de reconocimiento:', event.error); Utils.updateStatus('Error de reconocimiento', 'error'); }; recognition.start(); } }; // Funciones de modo y modelo function changeMode(mode) { currentMode = mode; fetch('/cambiar_modo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode }) }) .then(response => response.json()) .then(data => { if (data.success) { ChatManager.addMessage(`Modo cambiado a: ${mode}`, 'bot'); } else { console.error('Error cambiando modo:', data.error); } }) .catch(error => console.error('Error:', error)); } function changeModel(model) { currentModel = model; fetch('/cambiar_modelo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model }) }) .then(response => response.json()) .then(data => { if (data.success) { ChatManager.addMessage(`Modelo cambiado a: ${model}`, 'bot'); } else { console.error('Error cambiando modelo:', data.error); } }) .catch(error => console.error('Error:', error)); } async function changeTTS(model) { console.log("Cambiando modelo TTS a:", model); Utils.updateStatus("Cambiando voz..."); try { const response = await fetch('/cambiar_tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: model }) }); if (!response.ok) { throw new Error('Error en la respuesta del servidor'); } const data = await response.json(); let voiceName; switch(model) { case 'EDGE': voiceName = "Jorge (MX)"; break; case 'EDGE_ES': voiceName = "Álvaro (ES)"; break; case 'VITS': voiceName = "VITS (ES)"; break; default: voiceName = model; } Utils.updateStatus(`Voz cambiada a ${voiceName}`); console.log("Modelo TTS cambiado exitosamente"); } catch (error) { console.error("Error al cambiar el modelo de voz:", error); Utils.updateStatus("Error al cambiar la voz"); } } // Reconocimiento de voz async function checkMicrophonePermissions() { try { // Verificar si ya tenemos permisos const permissionStatus = await navigator.permissions.query({ name: 'microphone' }); if (permissionStatus.state === 'granted') { return true; } else if (permissionStatus.state === 'prompt') { // Solicitar permisos explícitamente const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach(track => track.stop()); return true; } else if (permissionStatus.state === 'denied') { Utils.updateStatus('Permisos de micrófono denegados. Por favor, habilítalos en la configuración del navegador.', 'error'); ChatManager.addMessage('❌ El micrófono está bloqueado. Para habilitarlo:\n1. Haz clic en el icono 🔒 o 🎤 en la barra de direcciones\n2. Selecciona "Permitir" para el micrófono\n3. Recarga la página', 'bot'); return false; } } catch (error) { console.error('Error verificando permisos:', error); return false; } } // Función para inicializar el análisis de audio async function setupAudioAnalysis() { try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); analyser.fftSize = 256; // Configurar filtros para reducir eco const lowpass = audioContext.createBiquadFilter(); lowpass.type = 'lowpass'; lowpass.frequency.value = 2000; const highpass = audioContext.createBiquadFilter(); highpass.type = 'highpass'; highpass.frequency.value = 85; // Obtener stream del micrófono microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: false } }); const source = audioContext.createMediaStreamSource(microphoneStream); source.connect(highpass); highpass.connect(lowpass); lowpass.connect(analyser); return true; } catch (error) { console.error('Error configurando análisis de audio:', error); return false; } } // Función para detectar interrupción basada en energía de audio function detectInterruption() { if (!analyser) return false; const dataArray = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(dataArray); // Calcular energía promedio const average = dataArray.reduce((a, b) => a + b) / dataArray.length; const normalizedEnergy = average / 128; // Normalizar a un rango de 0-1 return normalizedEnergy > ENERGY_THRESHOLD; } // Funciones de sesión async function initSession() { try { const response = await fetch('/get_session_data'); const data = await response.json(); sessionId = data.id; currentMode = data.mode; currentModel = data.model; currentTTS = data.tts; // Actualizar selectores con valores de la sesión if (modoSelect) modoSelect.value = currentMode; if (modeloSelect) modeloSelect.value = currentModel; if (vozSelect) vozSelect.value = currentTTS; // Cargar historial de chat if (data.chat_history && data.chat_history.length > 0) { data.chat_history.forEach(msg => { ChatManager.addMessage(msg.text, msg.sender); }); } return sessionId; } catch (error) { console.error('Error inicializando sesión:', error); return null; } } async function updateAudioState(isPlaying, currentAudio = null) { try { await fetch('/update_audio_state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_playing: isPlaying, current_audio: currentAudio }) }); } catch (error) { console.error('Error actualizando estado del audio:', error); } } // Función para obtener el mensaje de presentación según el modo function getMensajePresentacion(modo) { const presentaciones = { 'soporte': '¡Hola! Soy tu asistente de soporte técnico. Estoy aquí para ayudarte con cualquier problema de PC o Android. ¿En qué puedo ayudarte?', 'seguros': '¡Hola! Soy tu asesor de seguros personal. Te ayudaré a encontrar la mejor protección para ti y tu familia. ¿Qué tipo de seguro te interesa?', 'creditos': '¡Hola! Soy tu asesor financiero. Estoy aquí para ayudarte a obtener el crédito que necesitas con las mejores condiciones. ¿Cuánto necesitas?', 'cobranza': '¡Hola! Soy tu gestor de cobranza. Estoy aquí para ayudarte a regularizar tu situación y encontrar la mejor solución para ti. ¿Cómo puedo ayudarte?', 'encuestas': '¡Hola! Soy tu encuestador profesional. Me gustaría conocer tu opinión sobre temas importantes. ¿Estás listo para comenzar?' }; return presentaciones[modo] || '¡Hola! ¿En qué puedo ayudarte?'; } // Función para iniciar el modo con presentación async function iniciarModoConPresentacion(modo) { try { Utils.updateStatus('Iniciando modo...'); const mensajePresentacion = getMensajePresentacion(modo); const response = await fetch('/cambiar_modo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: modo }) }); const data = await response.json(); if (data.success) { currentMode = modo; ChatManager.addMessage(mensajePresentacion, 'bot'); if (data.audio) { try { const audioType = data.audio_type || 'wav'; await AudioManager.playAudio(data.audio, audioType); } catch (error) { console.error('Error reproduciendo audio:', error); handleAudioError(); } } } } catch (error) { console.error('Error iniciando modo:', error); Utils.updateStatus('Error al iniciar modo'); } } // Event Listeners document.addEventListener('DOMContentLoaded', async () => { console.log('Página cargada, inicializando...'); // Inicializar sesión await initSession(); // Inicializar reconocimiento de voz await VoiceManager.startRecognition(); // Iniciar modo inicial con presentación await iniciarModoConPresentacion(currentMode); // Iniciar reconocimiento automáticamente setTimeout(() => { if (startRecordingButton && !isListening && !isPlayingAudio) { console.log('Iniciando reconocimiento automático...'); VoiceManager.startRecognition(); Utils.updateStatus('Escuchando...'); } }, 1000); // Botón de envío de texto if (sendTextButton) { sendTextButton.addEventListener('click', () => { const text = textInput.value.trim(); if (text) { ChatManager.sendMessage(text); } }); } // Input de texto (Enter) if (textInput) { textInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const text = textInput.value.trim(); if (text) { ChatManager.sendMessage(text); } } }); } // Selectores if (modoSelect) { modoSelect.addEventListener('change', async function() { try { Utils.updateStatus('Cambiando modo...'); // Detener cualquier audio actual if (currentAudio && isPlaying) { currentAudio.pause(); currentAudio.currentTime = 0; currentAudio = null; isPlaying = false; } // Iniciar nuevo modo con presentación await iniciarModoConPresentacion(this.value); } catch (error) { console.error('Error en cambio de modo:', error); Utils.updateStatus('Error al cambiar modo'); this.value = currentMode; // Revertir al modo anterior } finally { setTimeout(() => Utils.updateStatus('Escuchando...'), 2000); } }); } if (modeloSelect) { modeloSelect.addEventListener('change', async (e) => { currentModel = e.target.value; changeModel(currentModel); }); } if (vozSelect) { vozSelect.addEventListener('change', async (e) => { const newVoice = e.target.value; console.log('Seleccionada nueva voz:', newVoice); Utils.updateStatus('Cambiando voz...'); currentTTS = newVoice; changeTTS(newVoice); }); } // Botón de grabación if (startRecordingButton) { startRecordingButton.addEventListener('click', async () => { if (!isListening) { await VoiceManager.startRecognition(); Utils.updateStatus('Escuchando...'); } else { await VoiceManager.pauseRecognition(); Utils.updateStatus('Reconocimiento pausado'); } }); } // Manejar visibilidad de la página document.addEventListener('visibilitychange', () => { if (document.hidden) { if (isListening) { VoiceManager.pauseRecognition(); } } else { if (!isListening && !isPlayingAudio) { setTimeout(VoiceManager.startRecognition, 500); } } }); }); // Agregar al inicio del archivo, después de las variables globales const permissionModal = `

Permisos de Micrófono

Para usar el chat por voz, necesitamos acceso a tu micrófono.

⚠️ Si el sitio no es seguro (HTTPS), el navegador puede bloquear el acceso al micrófono.
`; // Agregar después de document.addEventListener('DOMContentLoaded'... document.body.insertAdjacentHTML('beforeend', permissionModal); // Funciones para el modal de permisos function showPermissionModal() { document.getElementById('permissionModal').classList.add('show'); } function closePermissionModal() { document.getElementById('permissionModal').classList.remove('show'); } async function requestMicrophonePermission() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach(track => track.stop()); closePermissionModal(); await VoiceManager.initializeSpeechRecognition(); } catch (error) { console.error('Error al solicitar permisos:', error); Utils.updateStatus('Error: No se pudo acceder al micrófono', 'error'); ChatManager.addMessage('❌ No se pudo acceder al micrófono. Por favor, verifica los permisos en tu navegador.', 'bot'); } }