// Conectar con Socket.IO const socket = io(); // Elementos del DOM const messagesContainer = document.getElementById('messagesContainer'); const messagesWrapper = document.getElementById('messagesWrapper'); const chatForm = document.getElementById('chatForm'); const messageInput = document.getElementById('messageInput'); const sendBtn = document.getElementById('sendBtn'); const newChatBtn = document.getElementById('newChatBtn'); const newChatBtnMobile = document.getElementById('newChatBtnMobile'); const sidebar = document.getElementById('sidebar'); const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const sidebarToggle = document.getElementById('sidebarToggle'); const welcomeMessage = document.getElementById('welcomeMessage'); // Estado let isTyping = false; let conversationId = null; // Inicialización function init() { // Event listeners chatForm.addEventListener('submit', handleSubmit); messageInput.addEventListener('input', handleInputChange); messageInput.addEventListener('keydown', handleKeyDown); newChatBtn.addEventListener('click', handleNewChat); if (newChatBtnMobile) { newChatBtnMobile.addEventListener('click', handleNewChat); } if (mobileMenuBtn) { mobileMenuBtn.addEventListener('click', toggleSidebar); } if (sidebarToggle) { sidebarToggle.addEventListener('click', toggleSidebar); } // Cerrar sidebar al hacer click fuera (mobile) document.addEventListener('click', handleClickOutside); // Suggestion cards setupSuggestionCards(); // Escuchar mensajes del servidor socket.on('message', handleIncomingMessage); socket.on('ai_response', handleAIResponse); socket.on('error', handleError); // Escuchar errores de conexión socket.on('connect_error', (error) => { console.error('Error de conexión:', error); showErrorMessage('Error de conexión con el servidor'); }); // Conectado exitosamente socket.on('connect', () => { console.log('Conectado al servidor'); }); // Auto-focus en el input messageInput.focus(); } // Toggle sidebar (mobile) function toggleSidebar() { sidebar.classList.toggle('open'); } // Cerrar sidebar al hacer click fuera function handleClickOutside(e) { if (window.innerWidth <= 768 && sidebar.classList.contains('open')) { if (!sidebar.contains(e.target) && !mobileMenuBtn.contains(e.target)) { sidebar.classList.remove('open'); } } } // Setup suggestion cards function setupSuggestionCards() { const suggestionCards = document.querySelectorAll('.suggestion-card'); suggestionCards.forEach(card => { card.addEventListener('click', () => { const suggestion = card.querySelector('h3').textContent; messageInput.value = suggestion; messageInput.focus(); handleInputChange(); }); }); } // Manejar envío de mensaje function handleSubmit(e) { e.preventDefault(); const message = messageInput.value.trim(); if (!message || isTyping) return; // Ocultar mensaje de bienvenida if (welcomeMessage) { welcomeMessage.style.display = 'none'; } // Mostrar mensaje del usuario inmediatamente addMessage('user', message); // Limpiar input messageInput.value = ''; adjustTextareaHeight(); sendBtn.disabled = true; // Mostrar indicador de escritura showTypingIndicator(); // Enviar mensaje al servidor socket.emit('user_message', { message, conversationId }); } // Manejar cambios en el input function handleInputChange() { adjustTextareaHeight(); const hasText = messageInput.value.trim() !== ''; sendBtn.disabled = !hasText || isTyping; } // Ajustar altura del textarea function adjustTextareaHeight() { messageInput.style.height = 'auto'; const newHeight = Math.min(messageInput.scrollHeight, 200); messageInput.style.height = newHeight + 'px'; } // Manejar teclas en el input function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(e); } } // Manejar nuevo chat function handleNewChat() { // Limpiar mensajes messagesContainer.innerHTML = ''; // Mostrar mensaje de bienvenida messagesContainer.innerHTML = `

¿Cómo puedo ayudarte hoy?

`; // Re-setup suggestion cards setupSuggestionCards(); // Reset estado conversationId = null; isTyping = false; messageInput.value = ''; adjustTextareaHeight(); messageInput.focus(); // Cerrar sidebar en mobile if (window.innerWidth <= 768) { sidebar.classList.remove('open'); } } // Manejar mensaje entrante del servidor function handleIncomingMessage(data) { if (data.role === 'user') { addMessage('user', data.content); showTypingIndicator(); } } // Manejar respuesta de AI function handleAIResponse(data) { removeTypingIndicator(); addMessage('ai', data.content); if (data.conversationId) { conversationId = data.conversationId; } isTyping = false; handleInputChange(); } // Manejar errores function handleError(data) { removeTypingIndicator(); showErrorMessage(data.message || 'Ha ocurrido un error'); isTyping = false; handleInputChange(); } // Agregar mensaje al chat function addMessage(role, content) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; const avatar = role === 'user' ? ` ` : ` `; messageDiv.innerHTML = `
${avatar}
${formatMessage(content)}
`; messagesContainer.appendChild(messageDiv); scrollToBottom(); } // Mostrar indicador de escritura function showTypingIndicator() { isTyping = true; const typingDiv = document.createElement('div'); typingDiv.className = 'typing-indicator'; typingDiv.id = 'typingIndicator'; typingDiv.innerHTML = `
`; messagesContainer.appendChild(typingDiv); scrollToBottom(); } // Remover indicador de escritura function removeTypingIndicator() { const typingIndicator = document.getElementById('typingIndicator'); if (typingIndicator) { typingIndicator.remove(); } } // Mostrar mensaje de error function showErrorMessage(error) { const errorDiv = document.createElement('div'); errorDiv.className = 'message system-error'; errorDiv.innerHTML = `
⚠️ ${escapeHtml(error)}
`; messagesContainer.appendChild(errorDiv); scrollToBottom(); } // Scroll al final del contenedor function scrollToBottom() { requestAnimationFrame(() => { messagesWrapper.scrollTop = messagesWrapper.scrollHeight; }); } // Escapar HTML para prevenir XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Formatear mensaje con markdown básico function formatMessage(text) { // Escapar HTML primero text = escapeHtml(text); // Convertir bloques de código ``` text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, '
$2
'); // Convertir **texto** a negrita text = text.replace(/\*\*(.*?)\*\*/g, '$1'); // Convertir *texto* a cursiva text = text.replace(/\*(.*?)\*/g, '$1'); // Convertir `código` a código inline text = text.replace(/`(.*?)`/g, '$1'); // Convertir URLs a enlaces text = text.replace(/(https?:\/\/[^\s]+)/g, '$1'); // Convertir saltos de línea a párrafos text = text.split('\n\n').map(p => `

${p.replace(/\n/g, '
')}

`).join(''); return text; } // Guardar conversación en localStorage function saveConversation() { const messages = Array.from(messagesContainer.querySelectorAll('.message:not(.system-error)')) .map(msg => ({ role: msg.classList.contains('user') ? 'user' : 'ai', content: msg.querySelector('.message-text').textContent })); if (messages.length > 0) { localStorage.setItem('lastConversation', JSON.stringify({ id: conversationId, messages, timestamp: Date.now() })); } } // Cargar conversación desde localStorage function loadConversation() { const saved = localStorage.getItem('lastConversation'); if (saved) { try { const data = JSON.parse(saved); conversationId = data.id; if (welcomeMessage) { welcomeMessage.style.display = 'none'; } data.messages.forEach(msg => { addMessage(msg.role, msg.content); }); } catch (e) { console.error('Error al cargar conversación:', e); } } } // Guardar conversación antes de cerrar window.addEventListener('beforeunload', saveConversation); // Responsive: Ajustar al cambiar tamaño de ventana window.addEventListener('resize', () => { if (window.innerWidth > 768) { sidebar.classList.remove('open'); } }); // Iniciar la aplicación init(); // Cargar última conversación si existe // loadConversation(); console.log('✨ Nexus AI Chat iniciado');