PageSourceSearch

https://sista.ar/assets/chatbot/widget_chatbot_ui.js

js sista.ar collected 2026-10-10 00:06:34 UTC 14,182 bytes, 456 lines download raw bytes

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.