415 lines
12 KiB
JavaScript
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');
|
|
|