1(function () { 2 // ======== CONFIG DE ENTORNO ======== 3 // Ãnico lugar a cambiar al pasar de dev a producción. 4 const WIDGET_URL = 'https://widget-dev.lovi.ai'; 5 const FUNCTIONS_BASE = 'https://vditxnjbejitzkcfqzzv.supabase.co/functions/v1'; 6 // ======== FIN CONFIG DE ENTORNO ======== 7 8 //Create elemento ia-widget 9 const node_element = document.createElement("app-chatbot"); 10 node_element.setAttribute("id", "app-chatbot"); 11 document.body.appendChild(node_element); 12 13 //Conditional wp 14 const mobile = (window.innerWidth <= 650) ? 'on' : 'off'; 15 const whatsapp_type = document.currentScript.getAttribute("whatsapp_type") ? document.currentScript.getAttribute("whatsapp_type") : false; 16 const whatsapp_number = document.currentScript.getAttribute("whatsapp_number") ? document.currentScript.getAttribute("whatsapp_number") : false; 17 // mode="voice" â carga el widget de voz (ElevenLabs) en vez del chat de texto. 18 const mode = document.currentScript.getAttribute("mode") || false; 19 const voice_customer = document.currentScript.getAttribute("customer") || false; 20 const voice_id_widget = document.currentScript.getAttribute("widget") || false; 21 const voice_language_attr = document.currentScript.getAttribute("language-lovi") || false; 22 23 // Carga el widget de VOZ: iframe transparente con ?mode=voice y responde el 24 // handshake get_voice (main.js) con identidad + config para VoiceWidget.vue. 25 const load_voice_widget = async () => { 26 if (!voice_customer || !voice_id_widget) { 27 console.error("LOVI ERROR (voz): customer/widget no definidos."); 28 return false; 29 } 30 const url = WIDGET_URL; 31 const functionsBase = FUNCTIONS_BASE; 32 33 // Identidad web: misma llave/criterio que el widget de texto (ia_infouser 34 // bajo la clave id_widget). El `id` se inyecta como user_id â el post-call 35 // crea el chat (mismo rastro que Ringover). 36 const STORAGE_KEY = voice_id_widget; 37 const readStore = () => { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } catch { return {}; } }; 38 let info_user = readStore().ia_infouser; 39 if (!info_user || !info_user.id) { 40 const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 41 info_user = { id }; 42 localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...readStore(), ia_infouser: info_user })); 43 } 44 45 let language = voice_language_attr || (navigator.language || navigator.userLanguage || 'en').slice(0, 2); 46 if (language === 'un') language = 'en'; 47 48 // Apariencia del orbe (color/posición/animación) configurada por empresa en 49 // channel_accounts.metadata['Voice-config']. Defaults si falla. 50 let voiceConfig = {}; 51 try { 52 const res = await fetch(`${functionsBase}/voice-widget?access_key=${encodeURIComponent(voice_customer)}&id_widget=${encodeURIComponent(voice_id_widget)}&language=${encodeURIComponent(language)}`); 53 const data = await res.json(); 54 if (!data?.error) voiceConfig = data?.widget?.["Voice-config"] || {}; 55 } catch (e) { /* defaults */ } 56 57 // El handshake debe estar escuchando ANTES de que el iframe cargue y lo pida. 58 window.addEventListener("message", function (e) {
59 if (e.data?.tipo_peticion === "get_voice") { 60 const iframe = document.getElementById("app-chatbot-voice-iframe"); 61 iframe?.contentWindow?.postMessage({ 62 tipo: "get_voice", 63 access_key: voice_customer, 64 id_widget: voice_id_widget, 65 language, 66 voiceConfig, 67 functionsBase, 68 info_user, 69 }, "*"); 70 } else if (e.data?.tipo_peticion === "voice_resize") { 71 // El VoiceWidget avisa su tamaño real (cápsula sola o con transcript). 72 // Redimensionamos el iframe para que no recorte el contenido; vuelve 73 // a achicarse a la cápsula al cerrar (mantiene el rectángulo mÃnimo 74 // que no bloquea clics del resto de la página). 75 const vf = document.getElementById("app-chatbot-voice-iframe"); 76 if (vf && e.data.width && e.data.height) { 77 vf.style.width = e.data.width + "px"; 78 vf.style.height = e.data.height + "px"; 79 } 80 } 81 }, false); 82 83 // Iframe transparente pequeño: su rectángulo Sà captura clics, por eso se 84 // mantiene ajustado a la cápsula (ver decisión previa). Posición según config. 85 const side = voiceConfig.floating_position === "left" ? "left:0;" : "right:0;"; 86 const iframe = document.createElement("iframe"); 87 iframe.id = "app-chatbot-voice-iframe"; 88 iframe.src = `${url}?mode=voice`; 89 iframe.allow = "microphone; autoplay"; 90 iframe.setAttribute("frameBorder", "0"); 91 iframe.style.cssText = `position:fixed;bottom:0;${side}width:285px;height:104px;border:0;background:transparent;z-index:99999999999;`; 92 document.querySelector("#app-chatbot").appendChild(iframe); 93 }; 94 95 //Function load whatsapp 96 const load_whatsapp = (whatsapp_number) => { 97 const iframe_element = ` 98 <a href="https://wa.me/${whatsapp_number}" target="_blank" class="widget_chat_switch_wp"> 99 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-whatsapp"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9" /><path d="M9 10a.5 .5 0 0 0 1 0v-1a.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a.5 .5 0 0 0 0 -1h-1a.5 .5 0 0 0 0 1" /></svg> 100 </a> 101 <style>.widget_chat_switch_wp{position:fixed;bottom:10px;right:20px;cursor:pointer;display:flex;align-items:center;width:50px;height:50px;border-radius:50%;background:#27d367;justify-content:center;box-shadow:rgba(0,18,46,.18) 0 2px 20px 0;color:#27d367;animation:2s infinite pulse;z-index:99999999999999999999999}.widget_chat_switch_wp svg{width:40px;height:40px;color:#fff}@keyframes pulse{0%{-webkit-box-shadow:0 0 #27d367;box-shadow:0 0 #27d367}80%{-webkit-box-shadow:0 0 7px 15px rgba(238,105,128,0);box-shadow:0 0 7px 15px rgba(238,105,128,0)}100%{-webkit-box-shadow:0 0 0 0 rgba(238,105,128,0);box-shadow:0 0 0 0 rgba(238,105,128,0)}}</style>`; 102 document.querySelector("#app-chatbot").insertAdjacentHTML("afterbegin", iframe_element); 103 } 104 105 //Function load_ia_widget 106 const load_ia_widget = () => { 107 let widget = false; 108 let language = "en"; 109 let company = false; 110 let name_company = false; 111 let previewBusy = false; 112 let url = WIDGET_URL; 113 const messageQueue = []; 114 const customer = document.currentScript.getAttribute("customer") ? document.currentScript.getAttribute("customer") : false; 115 const id_widget = document.currentScript.getAttribute("widget") ? document.currentScript.getAttribute("widget") : false; 116 const dom = document.currentScript.getAttribute("dom") ? document.currentScript.getAttribute("dom") : false; 117 const event = document.currentScript.getAttribute("event") ? document.currentScript.getAttribute("event") : false;
118 const contexto_extra = document.currentScript.getAttribute("extra-context") ? document.currentScript.getAttribute("extra-context").split(",") : []; 119 const showClose = document.currentScript.getAttribute("showClose") !== 'false'; 120 const languageLovi = document.currentScript.getAttribute("language-lovi") || false; 121 const source = document.currentScript.getAttribute("lovi-source") ? document.currentScript.getAttribute("lovi-source") : false; 122 123 const params = []; 124 if (!showClose) params.push('showClose=false'); 125 if (languageLovi) params.push(`lang=${languageLovi}`); 126 if (params.length > 0) url += `?${params.join('&')}`; 127 128 //Validamos los campos necesarios 129 if (!customer || customer == '') { 130 console.error("LOVI ERROR: The customer of the company ID has not been defined."); 131 return false; 132 } 133 if (!id_widget || id_widget == '') { 134 console.error("LOVI ERROR: The widget ID has not been defined."); 135 return false; 136 } 137 138 // ======== STORAGE HELPERS (Single key = id_widget) ======== 139 const STORAGE_KEY = id_widget; 140 const readStore = () => { 141 try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } 142 catch { return {}; } 143 }; 144 const writeStore = (obj) => localStorage.setItem(STORAGE_KEY, JSON.stringify(obj)); 145 const setStore = (patch) => { const s = readStore(); writeStore({ ...s, ...patch }); }; 146 const getFromStore = (key) => readStore()[key]; 147 const removeFromStore = (key) => { const s = readStore(); if (key in s) { delete s[key]; writeStore(s); } }; 148 149 // Migración de llaves antiguas (se ejecuta una sola vez) 150 (function migrateLegacyStorage() { 151 const legacyKeys = [ 152 'ia_infouser', 'ia_widget', 'ia_company', 'ia_name_company', 153 'ia_open', 'ia_widget_timestamp', 'widget_chat_switch_message' 154 ]; 155 const s = readStore(); 156 let changed = false; 157 legacyKeys.forEach(k => { 158 const raw = localStorage.getItem(k); 159 if (raw !== null && !(k in s)) { 160 let val = raw; 161 try { val = JSON.parse(raw); } catch { 162 if (raw === 'true') val = true; 163 else if (raw === 'false') val = false; 164 } 165 s[k] = val; 166 localStorage.removeItem(k); 167 changed = true; 168 } 169 }); 170 if (changed) writeStore(s); 171 })(); 172 // ======== FIN STORAGE HELPERS ======== 173 174 // Lee una cookie por nombre (solo cookies NO HttpOnly son accesibles desde JS) 175 function getCookie(name) { 176 const match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]*)')); 177 return match ? decodeURIComponent(match[2]) : null; 178 } 179 180 //Activar oyente del widget 181 function listener_event_widget() { 182 // Evita duplicados del listener global 183 if (window.__loviWidgetClicksBound) return; 184 window.__loviWidgetClicksBound = true; 185 186 document.addEventListener('click', (e) => { 187 // abrir desde el botón flotante 188 if (e.target.closest('.widget_chat_switch_open')) { 189 open_widget(); 190 return; 191 } 192 // abrir desde el bocadillo 193 if (e.target.closest('.widget_chat_switch_message p')) { 194 open_widget(); 195 return; 196 } 197 // cerrar / ocultar bocadillo 198 if (e.target.closest('.widget_chat_switch_message span')) { 199 const box = document.querySelector('.widget_chat_switch_message'); 200 if (box) box.style.display = 'none'; 201 setStore({ widget_chat_switch_message: true }); 202 return; 203 } 204 }, false); 205 } 206 207 //Activar oyente del servidor 208 function listener_event_server() { 209 // --- define send_iframe (igual que antes), pero más seguro ---
210 const send_iframe = function (tipo) { 211 const iframeEl = document.getElementById("app-chatbot-iframe"); 212 const win = iframeEl?.contentWindow; 213 if (!win || !win.postMessage) return; 214 215 if (tipo === "get_customer") { 216 const info_user = getFromStore("ia_infouser") || null; 217 win.postMessage({ 218 tipo: "get_customer", 219 language, 220 event, 221 contexto_extra, 222 mobile, 223 info_user, 224 showClose, 225 source, 226 access_key: customer, 227 id_widget 228 }, "*"); 229 } 230 231 if (tipo === "get_widget") { 232 const ia_widget = getFromStore("ia_widget") || null; 233 win.postMessage({ 234 tipo: "get_widget", 235 company, 236 name_company, 237 widget: ia_widget 238 }, "*"); 239 } 240 241 if (tipo === "reset_widget") { 242 win.postMessage({ tipo: "reset_widget" }, "*"); 243 } 244 245 if (tipo === "data_collection") { 246 const payload = { 247 tipo: "data_collection", 248 data_collection: { url: window.location.href } 249 }; 250 if (widget?.local_storage_keys?.length) { 251 widget.local_storage_keys.forEach(({ key, type, source, name }) => { 252 let raw; 253 if (source === "js_global") { 254 // Lee una variable JS global de la página anfitriona por 255 // ruta con puntos (p.ej. "drupalSettings.global_datalayer.sf_id"). 256 // Solo el loader puede leerla: corre en la página del cliente, 257 // no en el iframe cross-origin del widget. 258 raw = key.split(".").reduce((o, k) => (o == null ? o : o[k]), window); 259 } else { 260 raw = localStorage.getItem(key) ?? sessionStorage.getItem(key) ?? getCookie(key); 261 } 262 let parsedValue; 263 try { parsedValue = JSON.parse(raw); } catch { parsedValue = raw; } 264 // `name` opcional: nombre de salida limpio (p.ej. "sf_id") en vez 265 // de la ruta larga. Sin `name`, se mantiene `key` (compat). 266 payload.data_collection[name || key] = { value: parsedValue, type }; 267 }); 268 } 269 win.postMessage(payload, "*"); 270 } 271 }; 272 273 // --- evita duplicados: quita el handler previo si existe --- 274 if (window.__loviMsgHandler) { 275 window.removeEventListener("message", window.__loviMsgHandler); 276 } 277 278 const handler = function (e) { 279 const data = e.data;
280 if (data?.tipo_peticion === "get_widget") { 281 send_iframe("get_widget"); 282 } else if (data?.tipo_peticion === "get_customer") { 283 send_iframe("get_customer"); 284 } else if (data?.tipo_peticion === "data_collection") { 285 send_iframe("data_collection"); 286 } else if (data?.tipo_peticion === "save_customer") { 287 setStore({ ia_infouser: data.info_user }); 288 } else if (data?.tipo_peticion === "close") { 289 if (showClose) { 290 if (mobile === "on") document.body.style.overflow = "auto"; 291 const iframe = document.querySelector("#app-chatbot-iframe"); 292 if (iframe?.style) iframe.style.display = "none"; 293 const openBtn = document.querySelector(".widget_chat_switch_open"); 294 if (openBtn?.style) openBtn.style.display = "block"; 295 const bubble = document.querySelector(".widget_chat_switch_message"); 296 if (bubble) bubble.style.display = "flex"; 297 setStore({ ia_open: false }); 298 } 299 }
299 else if (data?.tipo_peticion === "reset") { 300 removeFromStore("ia_infouser"); 301 create_id_unique(); 302 send_iframe("reset_widget"); 303 } else if (data?.tipo_peticion === "new_message") { 304 const isOpen = !!getFromStore('ia_open'); 305 if (isOpen) return; 306 const alertDot = document.querySelector(".widget_chat_switch_open_alert"); 307 if (alertDot?.style) alertDot.style.display = "block"; 308 if (document.querySelector(".widget_chat_switch_message_preview") && data.message?.text) { 309 const msg = data.message.text; 310 if (msg) { messageQueue.push(msg); processQueue(); } 311 } 312 } 313 }; 314 315 window.__loviMsgHandler = handler; 316 window.addEventListener("message", handler, false); 317 } 318 319 //Validamos el idioma 320 (function validate_idioma() { 321 if (languageLovi) { 322 language = languageLovi; 323 } else { 324 language = navigator.language || navigator.userLanguage; 325 language = language.slice(0, 2); 326 if (language == 'un') { 327 language = 'en'; 328 } 329 } 330 })(); 331 332 //Creamos el id_unique 333 function create_id_unique() { 334 let info = getFromStore("ia_infouser"); 335 if (!info || !info.id) { 336 const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 337 setStore({ ia_infouser: { id } }); 338 } 339 } 340 create_id_unique(); 341 342 //Detectamos el ancho del texto 343 function getTextWidth(text, font) { 344 var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); 345 var context = canvas.getContext("2d"); 346 context.font = font; 347 var metrics = context.measureText(text); 348 return metrics.width; 349 }; 350 351 // Cola y estado de preview 352 function processQueue() { 353 if (previewBusy || messageQueue.length === 0) return; 354 const message = messageQueue.shift(); 355 displayPreview(message); 356 } 357 function displayPreview(message) { 358 const preview = document.querySelector(".widget_chat_switch_message_preview"); 359 preview.innerHTML = `<p>${message}</p>`; 360 preview.style.display = "block"; 361 preview.classList.remove("animate-hide"); 362 void preview.offsetWidth; 363 preview.classList.add("animate-show"); 364 const paddingTotal = 20; 365 const fontSpec = "normal 14px sans-serif"; 366 const textWidth = getTextWidth(message, fontSpec); 367 preview.style.width = Math.min(textWidth + paddingTotal, 220) + "px"; 368 previewBusy = true; 369 setTimeout(() => { 370 preview.classList.remove("animate-show"); 371 preview.classList.add("animate-hide"); 372 setTimeout(() => { 373 preview.style.display = "none"; 374 previewBusy = false; 375 processQueue(); 376 }, 500); 377 }, 5000); 378 } 379 380 //funcion para abrir el widget 381 function open_widget() { 382 setStore({ ia_open: true }); 383 // Ocultar icono y mensaje principal 384 const open_icon = document.querySelector(".widget_chat_switch_open"); 385 if (open_icon) open_icon.style.display = "none"; 386 const widget_chat_switch_message = document.querySelector(".widget_chat_switch_message"); 387 if (widget_chat_switch_message) widget_chat_switch_message.style.display = "none"; 388 389 // Limpiar alerta y preview 390 const newAlert = document.querySelector(".widget_chat_switch_open_alert"); 391 if (newAlert) newAlert.style.display = "none"; 392 const preview = document.querySelector(".widget_chat_switch_message_preview"); 393 if (preview) { 394 preview.style.display = "none"; 395 preview.classList.remove("animate-show", "animate-hide"); 396 preview.innerHTML = ""; 397 } 398 399 // Crear o mostrar el iframe 400 let iframe_element = document.querySelector("#app-chatbot-iframe"); 401 if (!iframe_element) { 402 iframe_element = `<iframe id="app-chatbot-iframe" src="${url}" frameBorder="0" allowfullscreen allow="fullscreen; autoplay; encrypted-media; picture-in-picture" style="overflow-y: hidden;display: none;" class="bounce-in-down"></iframe>`; 403 document.querySelector("#app-chatbot").insertAdjacentHTML("afterbegin", iframe_element); 404 } 405 if (mobile == 'on') document.body.style.overflow = "hidden"; 406 407 setTimeout(() => { 408 document.querySelector("#app-chatbot-iframe").style.display = "block"; 409 }, 500); 410 } 411 412 //Insert button open and close 413 function insertButton() { 414 //Verificamos si showClose es false para mostrar en pantalla completa 415 if (!showClose) { 416 let iframe_element = `<iframe id="app-chatbot-iframe" src="${url}" frameBorder="0" allowfullscreen allow="fullscreen; autoplay; encrypted-media; picture-in-picture" style="width: 100%; height: 100%; position: fixed; top: 0; left: 0; z-index: 99999999999999999999999; display: block;"></iframe>`; 417 document.querySelector("#app-chatbot").insertAdjacentHTML("afterbegin", iframe_element); 418 listener_event_server(); 419 if (mobile == 'on') document.body.style.overflow = "hidden"; 420 return; 421 } 422 423 //Verificamos si whassap esta activo 424 if (widget?.whatsapp_type == true && widget?.whatsapp_number != '' && mobile == 'on') { 425 let whatsapp_number = widget?.whatsapp_number; 426 load_whatsapp(whatsapp_number); 427 return false;
428 } 429 430 //Verificamos si va dentro de algun elemento del dom 431 if ((widget.is_fullscreen && widget.element_dom && document.getElementById(widget.element_dom)) || dom) { 432 //Insertamos el iframe dentro del elemento dom 433 let dom_element = widget.element_dom ? widget.element_dom : dom; 434 let iframe_element = `<iframe id="app-chatbot-iframe" src="${url}" frameBorder="0" allowfullscreen allow="fullscreen; autoplay; encrypted-media; picture-in-picture" style="overflow-y: hidden;"></iframe>`; 435 iframe_element += `<style>#app-chatbot-iframe{width:100%;height:100%}</style>`; 436 const parentElement = document.getElementById(dom_element); 437 parentElement.replaceChildren(); 438 parentElement.insertAdjacentHTML("afterbegin", iframe_element); 439 document.querySelector("#app-chatbot-iframe").style.display = "block"; 440 listener_event_server(); 441 } else { 442 let message = false; 443 let anchoWidth = 0; 444 const fm = widget?.translations?.[language]?.floating_message; 445 if (typeof fm === 'string' && fm !== '') { 446 message = fm; 447 anchoWidth = getTextWidth(fm, "bold 15px sans-serif"); 448 if (anchoWidth > 300) anchoWidth = 300; 449 const dismissed = !!getFromStore('widget_chat_switch_message'); 450 if (dismissed) message = false; 451 } 452 453 //Inser css style + markup 454 let iframe_element = ` 455 <div class="widget_chat_switch"> 456 ${(message) ? ` 457 <div class="widget_chat_switch_message"> 458 <p>${message}</p> 459 <span> 460 <svg width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 461 <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> 462 <path d="M18 6l-12 12"></path> 463 <path d="M6 6l12 12"></path> 464 </svg> 465 </span> 466 </div>` : ``} 467 <div class="widget_chat_switch_message_preview"></div> 468 <div class="widget_chat_switch_open"> 469 <img src="${widget.avatar_open}" alt=""> 470 <div class="widget_chat_switch_open_alert"></div> 471 </div> 472 </div> 473 <style> 474 #app-chatbot { 475 position: fixed; 476 bottom: 10px; 477 right: 0; 478 z-index: 99999999999999999999999; 479 } 480 .widget_chat_switch { 481 position: fixed; 482 bottom: 10px; 483 right: 20px; 484 cursor: pointer; 485 display: flex; 486 align-items: center; 487 animation: up-down 7.5s infinite ease-in-out; 488 } 489 .widget_chat_switch img { 490 width: 50px; 491 height: 50px; 492 border-radius: 50%; 493 box-shadow: 0 0 10px 0 #ccc; 494 background: ${widget.color_primary}; 495 object-fit: contain; 496 } 497 .widget_chat_switch_open_alert{ 498 display: none; 499 background: #fb6944; 500 width: 10px; 501 height: 10px; 502 position: absolute; 503 top: 0; 504 right: 0; 505 border-radius: 50%; 506 border: 2px solid #fff; 507 } 508 .widget_chat_switch_open img{ 509 animation: pulse 2s infinite; 510 color: ${widget.color_primary}; 511 } 512 .widget_chat_switch_open img:hover { 513 animation: none; 514 } 515 #app-chatbot-iframe { 516 display: none; 517 width: 450px; 518 height: 85vh; 519 max-height: 824px; 520 position: fixed; 521 right: 10px; 522 bottom: 10px; 523 border-radius: 20px; 524 z-index: 1; 525 } 526 .bounce-in-down { 527 animation: fadeIn 0.5s ease-in both; 528 } 529 .widget_chat_switch_message{ 530 position: absolute; 531 width: ${anchoWidth}px; 532 right: calc(100% + 10px); 533 box-shadow: rgba(0, 18, 46, 0.18) 0px 2px 20px 0px; 534 border-radius: 50px; 535 padding: 10px 5px 10px 15px; 536 background: ${widget.color_primary}; 537 display: flex; 538 align-items: center; 539 box-sizing: content-box; 540 } 541 .widget_chat_switch_message::after{ 542 position: absolute; 543 content: ""; 544 width: 0; 545 height: 0; 546 border-left: 10px solid ${widget.color_primary}; 547 border-top: 5px solid transparent; 548 border-bottom: 5px solid transparent; 549 right: -8px; 550 } 551 .widget_chat_switch_message_preview { 552 display: none; 553 position: absolute; 554 right: 0; 555 bottom: 65px; 556 padding: 10px; 557 background: #fff; 558 border-radius: 10px; 559 box-shadow: 0 0 10px 2px #eee; 560 max-width: 220px; 561 white-space: normal; 562 word-wrap: break-word; 563 opacity: 0; 564 transform: translateY(20px); 565 } 566 .widget_chat_switch_message_preview p { 567 margin: 0; 568 font: normal 14px sans-serif; 569 line-height: 1.5; 570 display: -webkit-box; 571 -webkit-box-orient: vertical; 572 -webkit-line-clamp: 2; 573 overflow: hidden; 574 text-align: center; 575 } 576 .widget_chat_switch_message_preview.animate-show { 577 animation: slideUpInBots 0.4s ease-out forwards; 578 } 579 .widget_chat_switch_message_preview.animate-hide { 580 animation: fadeOutBots 0.5s ease-out forwards; 581 } 582 @keyframes slideUpInBots { 583 0% { transform: translateY(20px); opacity: 0; } 584 100% { transform: translateY(0); opacity: 1; } 585 } 586 @keyframes fadeOutBots { 587 0% { opacity: 1; } 588 100% { opacity: 0; } 589 } 590 .widget_chat_switch_message p{ 591 text-align: left; 592 color: ${widget.color_message_text}; 593 font: bold 14px sans-serif; 594 margin: 0; 595 white-space: normal; 596 overflow: hidden; 597 display: -webkit-box; 598 -webkit-line-clamp: 2; 599 -webkit-box-orient: vertical; 600 text-overflow: ellipsis; 601 } 602 .widget_chat_switch_message span{ 603 position: absolute; 604 right: 5px; 605 top: -10px;
606 width: 20px; 607 height: 20px; 608 border-radius: 50%; 609 background: ${widget.color_primary}; 610 align-items: center; 611 justify-content: center; 612 display:none; 613 } 614 .widget_chat_switch_message span svg{ 615 color: ${widget.color_message_text}; 616 } 617 .widget_chat_switch_message:hover span{ 618 display: flex; 619 } 620 @keyframes fadeIn { 621 from { opacity: 0; transform: translate3d(0, 10px, 0); } 622 to { opacity: 1; transform: translate3d(0, -10px, 0); } 623 } 624 @keyframes pulse { 625 0% { -webkit-box-shadow: 0 0 currentColor; box-shadow: 0 0 currentColor; } 626 80% { -webkit-box-shadow: 0 0 7px 15px rgba(238, 105, 128, 0); box-shadow: 0 0 7px 15px rgba(238, 105, 128, 0); } 627 100% { -webkit-box-shadow: 0 0 0 0 rgba(238, 105, 128, 0); box-shadow: 0 0 0 0 rgba(238, 105, 128, 0); } 628 } 629 @keyframes up-down { 630 0%,25%,50%,75%,100% { transform: translate(0); } 631 12.5%,37.5%,62.5%,87.5% { transform: translate(0, 2%); } 632 } 633 @media (max-height: 650px) { 634 #app-chatbot-iframe { 635 max-height: 100%; 636 height:100%; 637 bottom: 0; 638 transform: none !important; 639 top: 0; 640 } 641 } 642 @media (max-width: 650px) { 643 #app-chatbot-iframe { 644 width: 100%; 645 height:100%; 646 left: 0; 647 right: 0; 648 max-height: 100%; 649 bottom: 0; 650 transform: none !important; 651 top: 0; 652 border-radius: 0; 653 } 654 } 655 </style>`; 656 //Verificamos si widget es izquierda 657 if (widget.floating_position == "left") { 658 iframe_element += ` 659 <style> 660 #app-chatbot{ left: 0; } 661 .widget_chat_switch{ left: 20px; right: initial; } 662 .widget_chat_switch_message{ right: initial; left: calc(100% + 10px); } 663 #app-chatbot-iframe{ right: initial; left: 10px; } 664 .widget_chat_switch_message::after{ 665 left: -8px; 666 border-right: 10px solid #013591; 667 border-left: none; 668 } 669 </style>`; 670 } 671 document.querySelector("#app-chatbot").insertAdjacentHTML("afterbegin", iframe_element); 672 listener_event_server(); 673 listener_event_widget(); 674 } 675 676 // Si existe extra_css, inyectarlo en el widget 677 if (widget.extra_css && widget.extra_css.trim().length > 0) { 678 const styleEl = document.createElement("style"); 679 styleEl.innerHTML = widget.extra_css; 680 document.querySelector("#app-chatbot").appendChild(styleEl); 681 } 682 683 //Verificamos si ya se abrio el widget 684 const ia_bot_open = !!getFromStore('ia_open'); 685 ia_bot_open ? open_widget() : false; 686 } 687 688 // Función para cargar la configuración del widget desde el storage o servidor 689 async function loadWidgetConfig() { 690 const timestampKey = 'ia_widget_timestamp'; 691 const oneDay = 24 * 60 * 60 * 1000; 692 const now = Date.now(); 693 const storedWidget = getFromStore('ia_widget'); 694 const storedTimestamp = getFromStore(timestampKey); 695 696 if (storedWidget && storedTimestamp && (now - parseInt(storedTimestamp, 10)) < oneDay) { 697 widget = storedWidget; 698 company = getFromStore('ia_company') || false; 699 name_company = getFromStore('ia_name_company') || false; 700 insertButton(); 701 } else { 702 await get_config_widget(); 703 setStore({ [timestampKey]: now.toString() }); 704 } 705 } 706 707 // Función para obtener la configuración del widget desde el servidor 708 get_config_widget = async () => { 709 const baseUrl = `${FUNCTIONS_BASE}/widget`; 710 let urlRequest = `${baseUrl}?access_key=${customer}&language=${language}&id_widget=${id_widget}`; 711 const requestOptions = { 712 method: "GET", 713 headers: { 714 "Content-Type": "application/json", 715 "Accept": "application/json", 716 }, 717 redirect: "follow", 718 }; 719 async function fetchData() { 720 try { 721 const response = await fetch(urlRequest, requestOptions); 722 if (!response.ok) { 723 throw new Error(`Error: ${response.status} - ${response.statusText}`); 724 } 725 const data = await response.json(); 726 if (data?.company && data?.widget?.translations) { 727 widget = data.widget; 728 company = data.company; 729 name_company = data.name_company; 730 setStore({ 731 ia_widget: widget, 732 ia_company: company, 733 ia_name_company: name_company 734 }); 735 insertButton(); 736 return data; 737 } else if (data?.error) {
738 console.error("LOVI ERROR:", data.error); 739 return false; 740 } else { 741 console.error("LOVI ERROR: Unexpected response structure", data); 742 return false; 743 } 744 } catch (error) { 745 console.error("Error en la solicitud:", error.message); 746 } 747 } 748 return await fetchData(); 749 }; 750 751 // En lugar de llamar a get_config_widget() directamente, se llama a loadWidgetConfig() 752 loadWidgetConfig(); 753 } 754 755 //Dispatch: voz | whatsapp | chat de texto 756 if (mode === 'voice') { 757 load_voice_widget(); 758 } else if (whatsapp_type && whatsapp_number && whatsapp_type == 'full' && whatsapp_number != '') { 759 load_whatsapp(whatsapp_number); 760 } else { 761 load_ia_widget(); 762 } 763})();
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.