chatbot-web-app / main.js
salomonsky's picture
Upload main.js with huggingface_hub
4b16238 verified
raw
history blame
26.3 kB
// 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 = `
<div class="permission-modal" id="permissionModal">
<div class="permission-content">
<h4>Permisos de Micrófono</h4>
<p>Para usar el chat por voz, necesitamos acceso a tu micrófono.</p>
<div class="alert alert-warning">
<small>⚠️ Si el sitio no es seguro (HTTPS), el navegador puede bloquear el acceso al micrófono.</small>
</div>
<div class="permission-buttons">
<button class="btn btn-primary" onclick="requestMicrophonePermission()">Permitir Micrófono</button>
<button class="btn btn-secondary" onclick="closePermissionModal()">Cancelar</button>
</div>
</div>
</div>`;
// 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');
}
}