Saltar al contenido
ivan r.
ElementorHighlevelShopifyWordpress

Crea un chat para tu web /widget N8N

Por Iván Rodríguez ·

🎯 ¿Quieres tener un Agente de IA en tu sitio web que responda automáticamente con ChatGPT? En este tutorial te enseño paso a paso cómo crear un asistente virtual inteligente usando ChatGPT + n8n, sin necesidad de programar.

🔌 Compatible con:
✅ WordPress y woocommerce
✅ Shopify
✅ HighLevel

📥 Descarga la plantilla de N8N aquí 👉 DESCARGAR

En el codigo acontinuacion copia  y personaliza tu widget 

<!-- Widget Chat IA (N8N) -->
<style>
:root{
  --p:#000000;   /* negro */
  --ink:#ffffff; /* blanco */
  --bg:#fff;
  --tx:#1f2937;
  --bd:#e5e7eb;
  --bot:#f8fafc;
  --sh:0 18px 44px rgba(2,6,23,.18);
  --r:16px;
}

#tww,#twbtn{
  all:initial;
  font-family:'Maven Pro', sans-serif;
}

/* Ventana principal */
#tww{
  position:fixed !important;
  right:22px;
  bottom:90px;
  z-index:2147483647 !important;
  width:min(380px,92vw);
  height:620px;            /* altura fija escritorio */
  max-height:80vh;         /* nunca más grande que la pantalla */
  background:var(--bg);
  border:1px solid var(--bd);
  border-radius:var(--r);
  box-shadow:var(--sh);
  display:none;
  overflow:hidden;

  display:flex;
  flex-direction:column;
}
#tww.open{display:flex}

@media(max-width:480px){
  #tww{
    left:10px;
    right:10px;
    bottom:86px;
    width:auto;
    height:75vh;     /* altura proporcional en móviles */
  }
}

.tw-h{display:flex;justify-content:space-between;gap:10px;padding:12px 14px;border-bottom:1px solid var(--bd)}
.tw-b{display:flex;gap:10px;align-items:center}
.tw-b img{width:28px;height:28px;border-radius:8px}
.tw-t strong{display:block;font-size:14px;color:var(--tx)}
.tw-t i{display:block;font-size:12px;color:#6b7280}

.screen{flex:1;display:flex;flex-direction:column;min-height:0}
.hidden{display:none!important}

.wel{justify-content:center;align-items:center;gap:14px;background:linear-gradient(180deg,#f7f7ff,#fff)}
.wel h3{margin:0 16px;font-size:22px;color:var(--tx);text-align:center}
.ops{display:grid;gap:12px;width:88%;max-width:320px}
.btn{border:1px solid var(--bd);border-radius:14px;padding:14px 16px;font-weight:700;display:flex;align-items:center;gap:10px;cursor:pointer;justify-content:center;box-shadow:var(--sh)}
.btn svg{width:22px;height:22px}
.btn.chat{background:var(--p);color:#fff}

.chat{flex-direction:column}
.tw-m{flex:1;overflow:auto;padding:16px;white-space:pre-wrap;display:flex;flex-direction:column;gap:8px;background:var(--bg)}
.row{display:flex}
.bub{max-width:78%;padding:10px 12px;border-radius:14px;font-size:14px;line-height:1.35}
.u{margin-left:auto;background:var(--p);color:#fff}
.b{background:var(--bot);border:1px solid var(--bd)}
.typ{display:flex;gap:6px;padding:0 16px 8px}
.dot{width:8px;height:8px;border-radius:999px;background:#9aa4b2;opacity:.4;animation:b 1s infinite}
.dot:nth-child(2){animation-delay:.15s}
.dot:nth-child(3){animation-delay:.3s}
@keyframes b{0%,80%,100%{transform:translateY(0)}40%{transform:translateY(-4px)}}
.tw-f{display:flex;gap:8px;padding:14px;border-top:1px solid var(--bd)}
.tw-f input{flex:1;border:1px solid var(--bd);border-radius:12px;padding:12px}
.tw-f button{min-width:52px;border:0;border-radius:12px;background:var(--p);color:#fff;font-weight:600;cursor:pointer;font-size:14px;}

.bub a{color:#2563eb;text-decoration:underline;word-break:break-word}
.bub img{max-width:100%;height:auto;border-radius:10px;display:block;margin-top:6px}

@media(max-width:480px){
  #tww{left:10px;right:10px;bottom:86px;width:auto}
}

/* Posicion icono*/
#twbtn {
  position: fixed !important;
  right: 22px;
  bottom: 22px;
  z-index: 2147483647 !important;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: transparent;
  box-shadow: none;
  cursor: pointer;
}

#twbtn img {
  width: 90%;
  height: 90%;
  object-fit: contain;
  pointer-events: none;
}

/* imagen icono*/
</style>

<div id="twbtn" aria-label="Abrir chat">
  <img decoding="async" src="https://ivanrodriguezdigital.com/wp-content/uploads/2025/09/comentario.png" alt="Abrir chat">
</div>
<div id="tww" role="dialog" aria-live="polite">
  <div class="tw-h">
    <div class="tw-b">
      <img id="logo" alt="logo">
      <div class="tw-t"><strong id="title"></strong><i id="sub"></i></div>
    </div>
    <div style="display:flex;gap:8px;align-items:center">
      <button id="back" class="hidden" style="all:unset;cursor:pointer;font-size:14px;border:1px solid var(--bd);padding:4px 8px;border-radius:10px">↩︎Atrás</button>
      <button id="x" style="all:unset;cursor:pointer;font-size:18px" aria-label="Cerrar chat">✕</button>
    </div>
  </div>

  <!-- Pantalla de opciones -->
  <section class="screen wel" id="wel">
    <h3>¡Hola 👋! ¿En qué podemos ayudarte?</h3>
    <div class="ops">
      <button class="btn chat" id="opt-chat" aria-label="Usar chat con asistente IA">Asistente Virtual</button>
    </div>
    <small style="color:#6b7280;margin-top:8px">Atención inmediata a través de nuestro asistente virtual.</small>
  </section>

  <!-- Vista de chat IA -->
  <section class="screen chat hidden" id="chat">
    <div class="tw-m" id="msgs"></div>
    <div class="typ" id="typ"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
    <div class="tw-f">
      <input id="q" placeholder="Escribe tu mensaje aquí…" aria-label="Escribe tu mensaje">
      <button id="send" aria-label="Enviar mensaje">Enviar</button>
    </div>
  </section>
</div>

<script>
(() => {
  // ====== CONFIG CHAT URL ======
  const EP = "https://n8n.srv867795.hstgr.cloud/webhook/4ac82bcd-1b53-422c-b7f6-44d02cdac7a1/chat";
  const EP_LEADS = "";
  const BRAND = {
    title: "TU LOGO",
    subtitle: "Normalmente respondemos al instante",
    logo: "https://ivanrodriguezdigital.com/wp-content/uploads/2025/09/TU-LOGO--e1757803368737.png"
  };

  // ====== DOM ready ======
  function ready(fn) {
    if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn);
    else fn();
  }

  ready(() => {
    // DOM refs (usamos getElementById para mayor robustez)
    const box = document.getElementById('tww');
    const btn = document.getElementById('twbtn');
    const closeBtn = document.getElementById('x');
    const back = document.getElementById('back');
    const optChat = document.getElementById('opt-chat');
    const msgs = document.getElementById('msgs');
    const typ = document.getElementById('typ');
    const q = document.getElementById('q');
    const send = document.getElementById('send');

    if (!box || !btn) return; // required elements

    // Asegurar que el widget esté en body (evita stacking contexts de padres)
    if (box.parentNode !== document.body) document.body.appendChild(box);
    if (btn.parentNode !== document.body) document.body.appendChild(btn);

    // Aplicar branding
    const $title = document.getElementById('title');
    const $sub = document.getElementById('sub');
    const $logo = document.getElementById('logo');
    if ($title) $title.textContent = BRAND.title;
    if ($sub) $sub.textContent = BRAND.subtitle;
    if ($logo) $logo.src = BRAND.logo;

    // ====== Session & History ======
    const sid = localStorage.getItem("tw-sid") || (localStorage.setItem("tw-sid", crypto.randomUUID()), localStorage.getItem("tw-sid"));
    const key = `tw-h-${sid}`;
    const hist = JSON.parse(localStorage.getItem(key) || "[]");
    const leadKey = `tw-leads-${sid}`;

    function save(t, w) {
      const h = JSON.parse(localStorage.getItem(key) || "[]");
      h.push({ t, w });
      if (h.length > 300) h.shift();
      localStorage.setItem(key, JSON.stringify(h));
    }

    // ====== Helpers ======
    function esc(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
    function md(raw) {
      let s = esc(raw);
      s = s.replace(/!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g, (_, a, u) => `<img decoding="async" alt="${a||''}" src="${u}">`);
      s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, t, u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${t}</a>`);
      s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
      s = s.replace(/(^|\n)\s*-\s+/g, '$1• ');
      s = s.replace(/\n/g, '<br>');
      return s;
    }
    function bubble(text, who = "b") {
      const row = document.createElement("div"); row.className = "row";
      const b = document.createElement("div"); b.className = `bub ${who === "u" ? "u" : "b"}`;
      b.innerHTML = md(text);
      row.appendChild(b);
      msgs.appendChild(row);
      msgs.scrollTop = msgs.scrollHeight;
    }
    function typing(v) { if (!typ) return; typ.style.display = v ? "flex" : "none"; if (msgs) msgs.scrollTop = msgs.scrollHeight; }

    function detectPhones(text) {
      const re = /(?:\+?57)?\D*(3\d{2})\D*(\d{3})\D*(\d{4})/g;
      const out = []; let m;
      while ((m = re.exec(text)) !== null) { out.push(`57${m[1]}${m[2]}${m[3]}`); }
      return [...new Set(out)];
    }
    function alreadySaved(phone) { const set = new Set(JSON.parse(localStorage.getItem(leadKey) || "[]")); return set.has(phone); }
    function markSaved(phone) { const set = new Set(JSON.parse(localStorage.getItem(leadKey) || "[]")); set.add(phone); localStorage.setItem(leadKey, JSON.stringify([...set])); }
    async function sendLead(phone, lastMsg) {
      if (!EP_LEADS) return;
      try {
        await fetch(EP_LEADS, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: sid, phone, message: String(lastMsg || "").slice(0, 500), page: location.href, ts: new Date().toISOString() }) });
      } catch (e) { /* ignore */ }
    }

    // ====== Open / Close robustos ======
    function openChat() {
      box.classList.add('open');
      box.style.display = 'flex';            // forzar visible
      box.setAttribute('aria-hidden', 'false');
      // focus input shortly after abrir
      setTimeout(() => { if (q) q.focus(); }, 120);
    }
    function closeChat() {
      box.classList.remove('open');
      box.style.display = 'none';            // forzar oculto
      box.setAttribute('aria-hidden', 'true');
    }

    // Cargar historial (no abrir)
    if (hist.length) hist.forEach(m => bubble(m.t, m.w));

    // Listeners (open/close)
    btn.addEventListener('click', (e) => { e.stopPropagation(); openChat(); });
    if (closeBtn) closeBtn.addEventListener('click', (e) => { e.stopPropagation(); closeChat(); });
    if (back) back.addEventListener('click', (e) => { e.stopPropagation(); showWelcome(); });

    // cerrar con ESC
    document.addEventListener('keydown', (e) => { if (e.key === "Escape") closeChat(); });

    // cerrar al clic fuera (opcional, cómodo en móviles)
    document.addEventListener('click', (e) => {
      if (!box.contains(e.target) && !btn.contains(e.target) && box.classList.contains('open')) {
        closeChat();
      }
    });

    // Mostrar welcome / chat helpers
    function showWelcome() { if (document.getElementById('wel')) { document.getElementById('wel').classList.remove('hidden'); document.getElementById('chat').classList.add('hidden'); back.classList.add('hidden'); } }
    function showChat() { if (document.getElementById('wel')) { document.getElementById('wel').classList.add('hidden'); document.getElementById('chat').classList.remove('hidden'); back.classList.remove('hidden'); } }

    // texto de bienvenida
    if (optChat) optChat.addEventListener('click', (e) => {
      e.stopPropagation();
      showChat();
      if (!hist.length) {
        const g = "¡Hola! Soy el asistente virtual de este negocio digital. ¿En qué puedo ayudarte hoy?";
        bubble(g, "b"); save(g, "b");
      }
    });

    // ====== Chat logic ======
    async function speak(t) {
      if (!t) return;
      bubble(t, "u");
      save(t, "u");
      typing(true);

      // lead si escribe celular
      const phones = detectPhones(t);
      if (phones.length) {
        const phone = phones[0];
        if (!alreadySaved(phone)) { await sendLead(phone, t); markSaved(phone); }
      }

      try {
        const r = await fetch(EP, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chatInput: t, sessionId: sid, locale: "es", url: location.href }) });
        const d = await r.json().catch(() => ({}));
        const reply = d.reply ?? d.text ?? d.message ?? d.output ?? (Array.isArray(d.messages) && d.messages[0]?.content) ?? JSON.stringify(d);
        bubble(String(reply), "b");
        save(String(reply), "b");
      } catch (e) {
        bubble("⚠️ Error de conexión. Intenta de nuevo.", "b");
      } finally {
        typing(false);
      }
    }

    if (send) send.addEventListener('click', () => {
      const t = (q && q.value) ? q.value.trim() : "";
      if (!t) return;
      if (q) q.value = "";
      speak(t);
    });

    if (q) q.addEventListener('keydown', (e) => { if (e.key === "Enter") { e.preventDefault(); send && send.click(); } });

    // Inicial: oculto (por si CSS estaba diferente)
    closeChat();
  });
})();
</script>

<!-- /Widget Chat IA -->

Sigue aprendiendo