Nexus/public/js/app.js

415 lines
12 KiB
JavaScript

// 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 = `
<div class="welcome-message" id="welcomeMessage">
<div class="logo-wrapper">
<div class="logo">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
<h2>¿Cómo puedo ayudarte hoy?</h2>
<div class="suggestion-cards">
<button class="suggestion-card">
<div class="suggestion-icon">💡</div>
<div class="suggestion-content">
<h3>Ideas creativas</h3>
<p>Ayúdame con ideas innovadoras</p>
</div>
</button>
<button class="suggestion-card">
<div class="suggestion-icon">📝</div>
<div class="suggestion-content">
<h3>Escribir código</h3>
<p>Ayúdame a programar algo</p>
</div>
</button>
<button class="suggestion-card">
<div class="suggestion-icon">🎯</div>
<div class="suggestion-content">
<h3>Resolver problemas</h3>
<p>Analiza y encuentra soluciones</p>
</div>
</button>
<button class="suggestion-card">
<div class="suggestion-icon">📚</div>
<div class="suggestion-content">
<h3>Aprender algo nuevo</h3>
<p>Explícame conceptos complejos</p>
</div>
</button>
</div>
</div>
`;
// 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' ?
`<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>` :
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>`;
messageDiv.innerHTML = `
<div class="message-avatar">
${avatar}
</div>
<div class="message-content">
<div class="message-text">${formatMessage(content)}</div>
</div>
`;
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 = `
<div class="message-avatar">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div class="message-content">
<div class="typing-dots">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
</div>
`;
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 = `
<div class="message-content">
<div class="message-text" style="color: #ff6b6b;">
⚠️ ${escapeHtml(error)}
</div>
</div>
`;
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, '<pre><code>$2</code></pre>');
// Convertir **texto** a negrita
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Convertir *texto* a cursiva
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
// Convertir `código` a código inline
text = text.replace(/`(.*?)`/g, '<code>$1</code>');
// Convertir URLs a enlaces
text = text.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>');
// Convertir saltos de línea a párrafos
text = text.split('\n\n').map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).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');