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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
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 -->



