victor34593993's picture
in-chat product cards + cart + 3 providers
10d4cc8 verified
Raw
History Blame
13.8 kB
/* Backend-served support chat widget — pro support-agent UI.
Embed: <script src="https://BACKEND/widget.js?t=slug" defer></script> */
(function () {
"use strict";
var me = document.currentScript;
if (!me) {
var ss = document.getElementsByTagName("script");
for (var i = ss.length - 1; i >= 0; i--) {
if (ss[i].src && ss[i].src.indexOf("/widget.js") !== -1) { me = ss[i]; break; }
}
}
var SRC = me ? new URL(me.src) : null;
var BACKEND = SRC ? SRC.origin : "";
var TENANT = (SRC && SRC.searchParams.get("t")) || "default";
var QS = "?t=" + encodeURIComponent(TENANT);
var SESSION_KEY = "ssb_session_id_" + TENANT;
var cfg = {
brand_name: "Asistente", brand_color: "#e8491d",
welcome: "¡Hola! ¿En qué puedo ayudarte?",
assistant_name: "Asistente", avatar_url: ""
};
function getSid() { try { return localStorage.getItem(SESSION_KEY) || null; } catch (e) { return null; } }
function setSid(id) { try { if (id) localStorage.setItem(SESSION_KEY, id); } catch (e) {} }
function shade(hex, p) {
try {
var n = parseInt(hex.replace("#", ""), 16);
var r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
r = Math.max(0, Math.min(255, r + Math.round(255 * p / 100)));
g = Math.max(0, Math.min(255, g + Math.round(255 * p / 100)));
b = Math.max(0, Math.min(255, b + Math.round(255 * p / 100)));
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
} catch (e) { return hex; }
}
// Professional "support agent" (person + headset) icon — clear & not a photo.
var ICON_AGENT = '<svg viewBox="0 0 24 24"><path d="M21 12.3C21 6.9 16.8 3.2 12 3.2 7.3 3.2 3 6.85 3 12.38c-.6.34-1 .98-1 1.72v2c0 1.1.9 2 2 2h1v-6.1c0-3.87 3.13-7 7-7s7 3.13 7 7V19h-8v2h8c1.1 0 2-.9 2-2v-1.22c.59-.31 1-.92 1-1.64v-2.3c0-.7-.41-1.31-1-1.62zM9 14c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm6 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm3-3.5c-.96-2.28-3.21-3.89-5.84-3.89-2.63 0-4.88 1.61-5.84 3.89C7.55 8.99 9.68 8 12.05 8c2.36 0 4.5.99 5.95 2.5z"/></svg>';
var ICON_SEND = '<svg viewBox="0 0 24 24"><path d="M3 11l18-8-8 18-2.5-7.5L3 11z"/></svg>';
function fillAvatar(el) {
if (cfg.avatar_url) {
el.style.background = shade(cfg.brand_color, 40) + " url('" + cfg.avatar_url + "') center/cover no-repeat";
el.innerHTML = "";
} else {
el.classList.add("ssb-av-icon");
el.innerHTML = ICON_AGENT;
}
}
function esc(s) { return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
function md(text) {
var lines = esc(text).split(/\n/), out = [], inList = false;
for (var i = 0; i < lines.length; i++) {
var ln = lines[i], bullet = ln.match(/^\s*[-•]\s+(.*)$/);
if (bullet) { if (!inList) { out.push("<ul>"); inList = true; } out.push("<li>" + inline(bullet[1]) + "</li>"); }
else { if (inList) { out.push("</ul>"); inList = false; } out.push(ln.trim() === "" ? "<br>" : "<div>" + inline(ln) + "</div>"); }
}
if (inList) out.push("</ul>");
return out.join("");
}
function inline(s) {
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
return s;
}
function injectStyles() {
var c = cfg.brand_color, c2 = shade(c, -18);
var css =
".ssb-av{background:linear-gradient(135deg," + shade(c, 8) + "," + c2 + ");border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden}" +
".ssb-av-icon svg{width:64%;height:64%;fill:#fff}" +
"#ssb-launch{position:fixed;bottom:20px;right:20px;width:62px;height:62px;border-radius:50%;cursor:pointer;z-index:99998;border:0;background:linear-gradient(135deg," + c + "," + c2 + ");box-shadow:0 8px 24px rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;transition:transform .2s}" +
"#ssb-launch:hover{transform:scale(1.06)}#ssb-launch svg{width:32px;height:32px;fill:#fff}" +
"#ssb-launch .ssb-dot{position:absolute;top:5px;right:5px;width:13px;height:13px;border-radius:50%;background:#22c55e;border:2px solid #fff}" +
"#ssb-launch .ssb-pulse{position:absolute;inset:0;border-radius:50%;animation:ssbpulse 2.2s infinite}" +
"@keyframes ssbpulse{0%{box-shadow:0 0 0 0 " + c + "66}70%{box-shadow:0 0 0 16px " + c + "00}100%{box-shadow:0 0 0 0 " + c + "00}}" +
"#ssb-panel{position:fixed;bottom:20px;right:20px;width:380px;max-width:94vw;height:600px;max-height:80vh;background:#fff;border-radius:18px;box-shadow:0 18px 60px rgba(0,0,0,.3);display:none;flex-direction:column;overflow:hidden;z-index:99999;font-family:system-ui,-apple-system,Segoe UI,sans-serif;opacity:0;transform:translateY(16px) scale(.98);transition:opacity .22s,transform .22s}" +
"#ssb-panel.open{display:flex;opacity:1;transform:none}" +
"#ssb-head{background:linear-gradient(135deg," + c + "," + c2 + ");color:#fff;padding:14px 16px;display:flex;align-items:center;gap:12px}" +
"#ssb-head .ssb-av{width:44px;height:44px;border:2px solid rgba(255,255,255,.7)}" +
"#ssb-head .ssb-name{font-weight:700;font-size:15px;line-height:1.2}" +
"#ssb-head .ssb-status{font-size:12px;opacity:.92;display:flex;align-items:center;gap:5px;margin-top:2px}" +
"#ssb-head .ssb-status i{width:8px;height:8px;border-radius:50%;background:#22e07b;display:inline-block;animation:ssblive 2s infinite}" +
"@keyframes ssblive{0%{box-shadow:0 0 0 0 #22e07b88}70%{box-shadow:0 0 0 6px #22e07b00}100%{box-shadow:0 0 0 0 #22e07b00}}" +
"#ssb-head .ssb-x{margin-left:auto;background:transparent;border:0;color:#fff;font-size:24px;cursor:pointer;opacity:.85;line-height:1}" +
"#ssb-msgs{flex:1;overflow-y:auto;padding:16px 14px;background:#f4f5f7;display:flex;flex-direction:column;gap:10px}" +
".ssb-row{display:flex;align-items:flex-end;gap:8px;max-width:88%}" +
".ssb-row.user{align-self:flex-end;flex-direction:row-reverse}.ssb-row.bot{align-self:flex-start}" +
".ssb-row .ssb-av{width:28px;height:28px}" +
".ssb-b{padding:10px 13px;border-radius:16px;font-size:14px;line-height:1.5;word-wrap:break-word}" +
".ssb-row.bot .ssb-b{background:#fff;color:#1a1a1a;border:1px solid #e6e6e9;border-bottom-left-radius:5px;box-shadow:0 1px 2px rgba(0,0,0,.04)}" +
".ssb-row.user .ssb-b{background:" + c + ";color:#fff;border-bottom-right-radius:5px}" +
".ssb-b a{color:inherit;text-decoration:underline}.ssb-row.bot .ssb-b a{color:" + c2 + "}" +
".ssb-b ul{margin:4px 0;padding-left:18px}.ssb-b li{margin:2px 0}.ssb-b code{background:#eee;padding:1px 4px;border-radius:4px;font-size:13px}" +
".ssb-typing{display:flex;gap:4px;padding:12px 14px}.ssb-typing span{width:7px;height:7px;border-radius:50%;background:#bbb;animation:ssbbounce 1.2s infinite}" +
".ssb-typing span:nth-child(2){animation-delay:.2s}.ssb-typing span:nth-child(3){animation-delay:.4s}" +
"@keyframes ssbbounce{0%,60%,100%{transform:translateY(0);opacity:.5}30%{transform:translateY(-5px);opacity:1}}" +
"#ssb-form{display:flex;align-items:center;gap:8px;padding:10px;border-top:1px solid #ececef;background:#fff}" +
"#ssb-input{flex:1;border:1px solid #e0e0e4;border-radius:22px;padding:11px 15px;font-size:14px;outline:none}" +
"#ssb-input:focus{border-color:" + c + "}" +
"#ssb-send{width:42px;height:42px;border-radius:50%;border:0;background:" + c + ";color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex:none}#ssb-send svg{width:19px;height:19px;fill:#fff}" +
"#ssb-foot{text-align:center;font-size:11px;color:#9aa;padding:6px;background:#fff}" +
".ssb-cards{align-self:flex-start;display:flex;flex-direction:column;gap:8px;max-width:92%;margin-left:36px}" +
".ssb-card{display:flex;gap:10px;background:#fff;border:1px solid #e6e6e9;border-radius:14px;padding:8px;box-shadow:0 1px 3px rgba(0,0,0,.05)}" +
".ssb-card img{width:60px;height:60px;object-fit:cover;border-radius:10px;flex:none;background:#f0f0f2}" +
".ssb-card .ssb-ci{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}" +
".ssb-card .ssb-ct{font-size:13px;font-weight:600;color:#1a1a1a;line-height:1.25}" +
".ssb-card .ssb-cp{font-size:14px;font-weight:700;color:" + c2 + "}" +
".ssb-card .ssb-cs{font-size:11px;font-weight:600}.ssb-card .ssb-in{color:#1a7f37}.ssb-card .ssb-out{color:#b00}" +
".ssb-card .ssb-cb{display:flex;gap:6px;margin-top:2px}" +
".ssb-card a.ssb-add{background:" + c + ";color:#fff;font-size:12px;font-weight:600;padding:6px 10px;border-radius:8px;text-decoration:none;text-align:center}" +
".ssb-card a.ssb-view{font-size:12px;color:#666;text-decoration:underline;padding:6px 4px;align-self:center}";
var st = document.createElement("style"); st.textContent = css; document.head.appendChild(st);
}
function build() {
injectStyles();
var name = cfg.assistant_name || cfg.brand_name;
var launch = document.createElement("button");
launch.id = "ssb-launch"; launch.setAttribute("aria-label", "Abrir chat de soporte");
launch.innerHTML = '<span class="ssb-pulse"></span>' + ICON_AGENT + '<span class="ssb-dot"></span>';
var panel = document.createElement("div");
panel.id = "ssb-panel";
panel.innerHTML =
'<div id="ssb-head"><div class="ssb-av"></div><div>' +
'<div class="ssb-name">' + esc(name) + '</div>' +
'<div class="ssb-status"><i></i>En línea · Responde en segundos</div></div>' +
'<button class="ssb-x" aria-label="Cerrar">&times;</button></div>' +
'<div id="ssb-msgs"></div>' +
'<form id="ssb-form"><input id="ssb-input" type="text" autocomplete="off" placeholder="Escribe tu mensaje…" />' +
'<button id="ssb-send" type="submit" aria-label="Enviar">' + ICON_SEND + '</button></form>' +
'<div id="ssb-foot">Asistente con IA</div>';
document.body.appendChild(launch);
document.body.appendChild(panel);
fillAvatar(panel.querySelector("#ssb-head .ssb-av"));
var msgs = panel.querySelector("#ssb-msgs");
var form = panel.querySelector("#ssb-form");
var input = panel.querySelector("#ssb-input");
var greeted = false;
function row(who) {
var r = document.createElement("div"); r.className = "ssb-row " + who;
if (who === "bot") { var a = document.createElement("div"); a.className = "ssb-av"; fillAvatar(a); r.appendChild(a); }
var b = document.createElement("div"); b.className = "ssb-b"; r.appendChild(b);
msgs.appendChild(r); msgs.scrollTop = msgs.scrollHeight; return b;
}
function botMsg(t) { var b = row("bot"); b.innerHTML = md(t); msgs.scrollTop = msgs.scrollHeight; return b; }
function money(p) { return p == null ? "" : (Math.round(p * 100) / 100).toLocaleString("es-ES", { minimumFractionDigits: 2 }) + " €"; }
function renderCards(products) {
if (!products || !products.length) return;
var wrap = document.createElement("div"); wrap.className = "ssb-cards";
products.slice(0, 6).forEach(function (p) {
var card = document.createElement("div"); card.className = "ssb-card";
var img = p.image ? '<img src="' + esc(p.image) + '" alt="" />' : '<img alt="" />';
var stock = p.available ? '<span class="ssb-cs ssb-in">● En stock</span>' : '<span class="ssb-cs ssb-out">● Agotado</span>';
var add = (p.available && p.add_url) ? '<a class="ssb-add" href="' + esc(p.add_url) + '" target="_blank" rel="noopener">Añadir al carrito</a>' : "";
var view = p.url ? '<a class="ssb-view" href="' + esc(p.url) + '" target="_blank" rel="noopener">Ver</a>' : "";
card.innerHTML = img + '<div class="ssb-ci"><div class="ssb-ct">' + esc(p.title || "") + '</div>' +
'<div class="ssb-cp">' + money(p.price) + '</div>' + stock +
'<div class="ssb-cb">' + add + view + '</div></div>';
wrap.appendChild(card);
});
msgs.appendChild(wrap); msgs.scrollTop = msgs.scrollHeight;
}
function userMsg(t) { row("user").textContent = t; msgs.scrollTop = msgs.scrollHeight; }
function typing() { var b = row("bot"); b.classList.add("ssb-typing"); b.innerHTML = "<span></span><span></span><span></span>"; msgs.scrollTop = msgs.scrollHeight; return b; }
function toggle() {
panel.classList.toggle("open");
if (panel.classList.contains("open")) { if (!greeted) { botMsg(cfg.welcome); greeted = true; } input.focus(); }
}
launch.addEventListener("click", toggle);
panel.querySelector(".ssb-x").addEventListener("click", toggle);
form.addEventListener("submit", function (e) {
e.preventDefault();
var text = input.value.trim(); if (!text) return;
userMsg(text); input.value = "";
var t = typing();
fetch(BACKEND + "/chat" + QS, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text, session_id: getSid() })
}).then(function (r) { return r.json(); }).then(function (d) {
setSid(d.session_id); t.classList.remove("ssb-typing"); t.innerHTML = md(d.reply || "…");
renderCards(d.products); msgs.scrollTop = msgs.scrollHeight;
}).catch(function () {
t.classList.remove("ssb-typing"); t.textContent = "Lo siento, ha habido un problema. Inténtalo de nuevo.";
});
});
}
function start() {
fetch(BACKEND + "/widget-config" + QS).then(function (r) { return r.json(); })
.then(function (d) { if (d) cfg = Object.assign(cfg, d); }).catch(function () {})
.then(function () { if (document.body) build(); else document.addEventListener("DOMContentLoaded", build); });
}
start();
})();