1/* ======================================== 2 WIDGET CHATBOT - UI & FRONTEND LOGIC 3 ======================================== */ 4 5// === CONFIGURACIÃN UI === 6const MAX_LONGITUD_MENSAJE = 2000; // Máximo de caracteres por mensaje (validación UX) 7 8// === FUNCIONES PARA INDICADOR DE CARGA === 9function mostrarIndicadorCarga() { 10 const chatBody = document.getElementById("chat-body"); 11 const loadingDiv = document.createElement("div"); 12 loadingDiv.className = "loading-indicator"; 13 loadingDiv.id = "loading-indicator"; 14 15 const spinner = document.createElement("div"); 16 spinner.className = "spinner"; 17 18 const loadingText = document.createElement("div"); 19 loadingText.className = "loading-text loading-dots"; 20 loadingText.textContent = "Iniciando conversación"; 21 22 loadingDiv.appendChild(spinner); 23 loadingDiv.appendChild(loadingText); 24 chatBody.appendChild(loadingDiv); 25 26 chatBody.scrollTop = chatBody.scrollHeight; 27} 28 29function ocultarIndicadorCarga() { 30 const loadingIndicator = document.getElementById("loading-indicator"); 31 if (loadingIndicator) { 32 loadingIndicator.remove(); 33 } 34} 35 36// === FUNCIONES DE VALIDACIÃN (Solo UX - La seguridad real está en el backend) === 37function validarMensaje(mensaje) { 38 return mensaje && 39 mensaje.trim().length > 0 && 40 mensaje.length <= MAX_LONGITUD_MENSAJE; 41} 42 43// === FUNCIONES DE GESTIÃN DE BOTONES === 44function deshabilitarBotonesAnteriores() { 45 const chatBody = document.getElementById("chat-body"); 46 const botonesAnteriores = chatBody.querySelectorAll(".opcion-boton"); 47
48 botonesAnteriores.forEach(boton => { 49 boton.disabled = true; 50 boton.style.opacity = "0.5"; 51 boton.style.cursor = "not-allowed"; 52 boton.onclick = null; // Eliminar el evento onclick 53 }); 54} 55 56function hayMensajeUsuarioReciente() { 57 const chatBody = document.getElementById("chat-body"); 58 const mensajes = chatBody.querySelectorAll(".usuario, .bot"); 59 60 if (mensajes.length === 0) return false; 61 62 // Verificar si el último mensaje es del usuario 63 const ultimoMensaje = mensajes[mensajes.length - 1]; 64 return ultimoMensaje.classList.contains("usuario"); 65} 66 67function restaurarBotonesHistorial() { 68 const chatBody = document.getElementById("chat-body"); 69 const todosLosElementos = Array.from(chatBody.children); 70 const todosLosBotones = chatBody.querySelectorAll(".opcion-boton"); 71 72 if (todosLosBotones.length === 0) return; 73 74 // Encontrar el Ãndice del último mensaje del usuario 75 let indiceUltimoMensajeUsuario = -1; 76 for (let i = todosLosElementos.length - 1; i >= 0; i--) { 77 const elemento = todosLosElementos[i]; 78 if (elemento.classList && elemento.classList.contains("usuario")) { 79 indiceUltimoMensajeUsuario = i; 80 break; 81 } 82 } 83 84 // Si no hay mensaje del usuario, todos los botones deben estar habilitados 85 if (indiceUltimoMensajeUsuario === -1) { 86 todosLosBotones.forEach(boton => { 87 const textoBoton = boton.textContent; 88 boton.disabled = false; 89 boton.style.opacity = "1"; 90 boton.style.cursor = "pointer"; 91 boton.onclick = () => { 92 if (!isSocketConnected()) { 93 console.error("Socket no conectado, reintentando..."); 94 alert("Conexión perdida. Por favor, intenta nuevamente en un momento."); 95 return; 96 } 97 if (validarMensaje(textoBoton)) { 98 enviarMensajeSocket(textoBoton); 99 mostrarMensaje(textoBoton, "usuario"); 100 setTimeout(() => { 101 document.getElementById("mensaje").focus(); 102 }, 100); 103 } 104 }; 105 }); 106 return; 107 } 108 109 // Deshabilitar solo los botones que están ANTES del último mensaje del usuario 110 todosLosBotones.forEach(boton => { 111 const contenedorBoton = boton.parentElement; 112 const indiceBoton = todosLosElementos.indexOf(contenedorBoton); 113 114 if (indiceBoton !== -1 && indiceBoton < indiceUltimoMensajeUsuario) { 115 // Botón antes del último mensaje del usuario - deshabilitar 116 boton.disabled = true; 117 boton.style.opacity = "0.5"; 118 boton.style.cursor = "not-allowed"; 119 boton.onclick = null; 120 } else { 121 // Botón después del último mensaje del usuario - habilitar 122 const textoBoton = boton.textContent; 123 boton.disabled = false; 124 boton.style.opacity = "1"; 125 boton.style.cursor = "pointer"; 126 boton.onclick = () => { 127 if (!isSocketConnected()) { 128 console.error("Socket no conectado, reintentando..."); 129 alert("Conexión perdida. Por favor, intenta nuevamente en un momento."); 130 return; 131 } 132 if (validarMensaje(textoBoton)) { 133 enviarMensajeSocket(textoBoton); 134 mostrarMensaje(textoBoton, "usuario"); 135 setTimeout(() => { 136 document.getElementById("mensaje").focus(); 137 }, 100); 138 } 139 }; 140 } 141 }); 142} 143 144// === RENDERIZADO DE MENSAJES === 145function mostrarMensaje(mensaje, tipo) { 146 const chatBody = document.getElementById("chat-body"); 147 const lineas = mensaje.split("\n"); 148 149 // Si es un mensaje del bot con nuevos botones Y hay un mensaje del usuario previo, 150 // entonces deshabilitar los botones anteriores 151 if (tipo === "bot") { 152 const tieneNuevosBotones = lineas.some(linea => linea.trim().startsWith("- ")); 153 if (tieneNuevosBotones && hayMensajeUsuarioReciente()) { 154 deshabilitarBotonesAnteriores(); 155 } 156 } 157 158 for (let linea of lineas) { 159 linea = linea.trim(); 160 161 // === Mostrar PDF si es un enlace === 162 if (linea.match(/\.pdf$/i) || linea.includes("/print_bill/")) { 163 const linkContainer = document.createElement("div"); 164 linkContainer.style.textAlign = "center"; 165 linkContainer.style.margin = "10px 0"; 166 167 const mensaje = document.createElement("div"); 168 mensaje.className = tipo; 169 mensaje.textContent = "Haz clic abajo para abrir el archivo PDF:"; 170 171 const botonPDF = document.createElement("a"); 172 botonPDF.href = linea; 173 botonPDF.target = "_blank"; 174 botonPDF.rel = "noopener noreferrer"; 175 botonPDF.textContent = "Abrir PDF"; 176 botonPDF.style.display = "inline-block"; 177 botonPDF.style.backgroundColor = "var(--color-principal)"; 178 botonPDF.style.color = "white"; 179 botonPDF.style.padding = "10px 16px"; 180 botonPDF.style.borderRadius = "6px"; 181 botonPDF.style.textDecoration = "none"; 182 botonPDF.style.marginTop = "6px"; 183 botonPDF.style.fontWeight = "bold"; 184 185 linkContainer.appendChild(mensaje); 186 linkContainer.appendChild(botonPDF); 187 chatBody.appendChild(linkContainer); 188 continue; 189 } 190 191 // === Mostrar imagen si termina en jpg/png/etc === 192 if (linea.match(/\.(jpg|jpeg|png|gif)$/i)) { 193 const img = document.createElement("img"); 194 img.src = linea; 195 img.alt = "Imagen del chat"; 196 chatBody.appendChild(img); 197 continue; 198 } 199 200 // === Mostrar botones si comienza con "- " === 201 if (tipo === "bot" && linea.startsWith("- ")) { 202 const textoBoton = linea.slice(2); 203 const boton = document.createElement("button"); 204 boton.textContent = textoBoton; 205 boton.className = "opcion-boton"; 206 boton.onclick = () => { 207 if (!isSocketConnected()) { 208 console.error("Socket no conectado, reintentando..."); 209 alert("Conexión perdida. Por favor, intenta nuevamente en un momento."); 210 return; 211 } 212 213 if (validarMensaje(textoBoton)) { 214 enviarMensajeSocket(textoBoton); 215 mostrarMensaje(textoBoton, "usuario"); 216 setTimeout(() => { 217 document.getElementById("mensaje").focus(); 218 }, 100); 219 } 220 }; 221 222 const contenedor = document.createElement("div"); 223 contenedor.style.textAlign = "center"; 224 contenedor.appendChild(boton); 225 chatBody.appendChild(contenedor); 226 continue; 227 } 228 229 // === Mostrar texto con negrita y enlaces === 230 const msgDiv = document.createElement("div"); 231 msgDiv.className = tipo; 232 233 // Procesar negritas y enlaces de forma segura (prevención XSS) 234 const fragmentos = []; 235 const regexURL = /(https?:\/\/[^\s]+)/g; 236 let ultimoIndice = 0; 237 let match; 238 239 while ((match = regexURL.exec(linea)) !== null) { 240 // Texto antes de la URL 241 if (match.index > ultimoIndice) { 242 const textoAntes = linea.substring(ultimoIndice, match.index); 243 fragmentos.push(procesarNegritas(textoAntes)); 244 } 245 246 // Crear enlace seguro 247 const url = match[0]; 248 const enlace = document.createElement('a'); 249 enlace.href = url; 250 enlace.target = '_blank'; 251 enlace.rel = 'noopener noreferrer'; 252 enlace.textContent = url; 253 fragmentos.push(enlace); 254 255 ultimoIndice = regexURL.lastIndex; 256 } 257 258 // Texto restante 259 if (ultimoIndice < linea.length) { 260 const textoRestante = linea.substring(ultimoIndice); 261 fragmentos.push(procesarNegritas(textoRestante)); 262 } 263 264 // Si no hay URLs, solo procesar negritas 265 if (fragmentos.length === 0) { 266 msgDiv.appendChild(procesarNegritas(linea)); 267 } else { 268 fragmentos.forEach(fragmento => { 269 msgDiv.appendChild(fragmento); 270 }); 271 } 272 273 chatBody.appendChild(msgDiv); 274 } 275 276 chatBody.scrollTop = chatBody.scrollHeight; 277 agregarEventosImagenes(); 278 guardarHistorial(); 279} 280 281// Procesar negritas de forma segura 282function procesarNegritas(texto) { 283 const span = document.createElement('span'); 284 const regexNegrita = /\*([^*]+)\*/g; 285 let ultimoIndice = 0; 286 let match; 287 288 while ((match = regexNegrita.exec(texto)) !== null) { 289 // Texto antes de la negrita 290 if (match.index > ultimoIndice) { 291 span.appendChild(document.createTextNode(texto.substring(ultimoIndice, match.index))); 292 } 293 294 // Crear negrita 295 const strong = document.createElement('strong'); 296 strong.textContent = match[1].toUpperCase(); 297 span.appendChild(strong); 298 299 ultimoIndice = regexNegrita.lastIndex; 300 } 301 302 // Texto restante 303 if (ultimoIndice < texto.length) { 304 span.appendChild(document.createTextNode(texto.substring(ultimoIndice))); 305 } 306 307 // Si no hay negritas, devolver solo el texto 308 return span.childNodes.length > 0 ? span : document.createTextNode(texto); 309} 310 311// === MODAL DE IMÃGENES === 312function openImageModal(imageSrc) { 313 console.log("Abriendo modal con imagen:", imageSrc); 314 const modal = document.getElementById("image-modal"); 315 const modalImg = modal.querySelector("img"); 316 if (modal && modalImg) { 317 modalImg.src = imageSrc; 318 modal.classList.add("show"); 319 console.log("Modal abierto"); 320 } else { 321 console.error("No se encontró el modal o la imagen"); 322 } 323} 324 325function closeImageModal() { 326 console.log("Cerrando modal"); 327 const modal = document.getElementById("image-modal"); 328 if (modal) { 329 modal.classList.remove("show"); 330 } 331} 332 333function agregarEventosImagenes() { 334 const chatBody = document.getElementById("chat-body"); 335 const imagenes = chatBody.querySelectorAll("img");
336 imagenes.forEach(img => { 337 img.style.cursor = "pointer"; 338 img.onclick = function() { 339 openImageModal(img.src); 340 }; 341 }); 342} 343 344// === MANEJO DE INPUT === 345function enviarMensaje() { 346 const input = document.getElementById("mensaje"); 347 const message = input.value.trim(); 348 349 // Validar mensaje antes de enviar 350 if (!validarMensaje(message)) { 351 if (message.length > MAX_LONGITUD_MENSAJE) { 352 alert(`El mensaje es muy largo. Máximo ${MAX_LONGITUD_MENSAJE} caracteres.`); 353 } 354 return; 355 } 356 357 input.value = ""; 358 359 if (enviarMensajeSocket(message)) { 360 mostrarMensaje(message, "usuario"); 361 } 362} 363 364// === INICIALIZACIÃN DEL WIDGET === 365function initChatWidget() { 366 const chatButton = document.getElementById("chat-button"); 367 const chatWindow = document.getElementById("chat-window"); 368 369 // === EVENTOS PARA DRAG & DROP === 370 let dragCounter = 0; 371 372 chatWindow.addEventListener('dragenter', (e) => { 373 e.preventDefault(); 374 e.stopPropagation(); 375 dragCounter++; 376 chatWindow.classList.add('dragging'); 377 }); 378 379 chatWindow.addEventListener('dragleave', (e) => { 380 e.preventDefault(); 381 e.stopPropagation(); 382 dragCounter--; 383 if (dragCounter === 0) { 384 chatWindow.classList.remove('dragging'); 385 } 386 }); 387 388 chatWindow.addEventListener('dragover', (e) => { 389 e.preventDefault(); 390 e.stopPropagation(); 391 }); 392 393 chatWindow.addEventListener('drop', (e) => { 394 e.preventDefault(); 395 e.stopPropagation(); 396 chatWindow.classList.remove('dragging'); 397 dragCounter = 0; 398 399 const files = Array.from(e.dataTransfer.files); 400 if (files.length > 0) { 401 enviarArchivosDirectamente(files); 402 } 403 }); 404 405 // === EVENTO BOTÃN CHAT === 406 chatButton.addEventListener("click", (e) => { 407 e.stopPropagation(); 408 409 if (chatWindow.style.display === "block") { 410 chatWindow.style.display = "none"; 411 } else { 412 chatWindow.style.display = "block"; 413 document.getElementById("mensaje").focus(); 414 415 iniciarSocket(); 416 restaurarHistorial(); 417 418 // Evento Enter para enviar mensaje 419 const mensajeInput = document.getElementById("mensaje"); 420 if (!mensajeInput.hasAttribute('data-listener-added')) { 421 mensajeInput.addEventListener("keydown", function (e) { 422 if (e.key === "Enter") { 423 e.preventDefault(); 424 enviarMensaje(); 425 } 426 }); 427 mensajeInput.setAttribute('data-listener-added', 'true'); 428 } 429 } 430 }); 431 432 // === CERRAR CHAT AL HACER CLIC FUERA === 433 document.addEventListener("click", (e) => { 434 const imageModal = document.getElementById("image-modal"); 435 if (!chatWindow.contains(e.target) && e.target !== chatButton && !imageModal.contains(e.target)) { 436 chatWindow.style.display = "none"; 437 } 438 }); 439 440 // === MODAL DE IMAGEN === 441 document.getElementById("image-modal").addEventListener("click", function(e) { 442 if (e.target.id === "image-modal") { 443 closeImageModal(); 444 } 445 }); 446 447 // Restaurar historial al cargar la página 448 restaurarHistorial(); 449} 450 451// Inicializar cuando el DOM esté listo 452if (document.readyState === 'loading') { 453 document.addEventListener('DOMContentLoaded', initChatWidget); 454} else { 455 initChatWidget(); 456}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.