/* Coda 客答 嵌入式客服 Widget — 商家:Alura 歐菈服飾店
用法: */
(function () {
var SLUG = "alura", BASE = "https://coda.tw", COLOR = "#b5705c";
var WELCOME = "\u6b61\u8fce\u4f86\u5230 Alura \u6b50\u83c8 \ud83d\udc57 \u60f3\u554f\u5c3a\u5bf8\u3001\u73fe\u8ca8\u3001\u51fa\u8ca8\uff0c\u9084\u662f\u9000\u63db\u8ca8\u5462\uff1f", NAME = "Alura \u6b50\u83c8\u670d\u98fe\u5e97", DISCLAIMER = "Alura \u6b50\u83c8\u670d\u98fe\u5e97\u70ba Coda \u5ba2\u7b54\u793a\u7bc4\u7528\u7684\u865b\u69cb\u5e97\u5bb6\uff0c\u8cc7\u8a0a\u50c5\u4f9b\u9ad4\u9a57\u3002";
var CONTACT_LINE = "";
var AVATAR = "";
var SID = "s-" + Math.random().toString(36).slice(2);
var css = ""
+ ".sv-btn{position:fixed;right:20px;bottom:20px;width:56px;height:56px;border-radius:50%;background:" + COLOR + ";color:#fff;border:0;font-size:24px;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.25);z-index:2147483646}"
+ ".sv-box{position:fixed;right:20px;bottom:88px;width:340px;max-width:92vw;height:460px;max-height:70vh;background:#fff;border-radius:14px;box-shadow:0 8px 30px rgba(0,0,0,.25);display:none;flex-direction:column;overflow:hidden;z-index:2147483647;font-family:-apple-system,'PingFang TC',sans-serif}"
+ ".sv-hd{background:" + COLOR + ";color:#fff;padding:12px 14px;font-weight:600;display:flex;align-items:center;gap:8px}"
+ ".sv-av{width:26px;height:26px;border-radius:50%;object-fit:cover;background:#fff;flex:0 0 auto}"
+ ".sv-btn.sv-img{background-size:cover;background-position:center;font-size:0}"
+ ".sv-log{flex:1;overflow-y:auto;padding:12px;background:#f3f4f6}"
+ ".sv-m{margin:6px 0;display:flex}.sv-m.u{justify-content:flex-end}"
+ ".sv-b{max-width:80%;padding:8px 12px;border-radius:12px;font-size:14px;line-height:1.5;white-space:pre-wrap}"
+ ".sv-m.u .sv-b{background:" + COLOR + ";color:#fff}.sv-m.a .sv-b{background:#e5e7eb;color:#111}"
+ ".sv-bar{display:flex;gap:6px;padding:8px;border-top:1px solid #e5e7eb}"
+ ".sv-bar input{flex:1;padding:8px;border:1px solid #d1d5db;border-radius:8px;font-size:14px}"
+ ".sv-bar button{padding:8px 12px;background:" + COLOR + ";color:#fff;border:0;border-radius:8px;cursor:pointer}"
+ ".sv-dc{font-size:10px;color:#9ca3af;text-align:center;padding:4px 8px;background:#fafafa}"
+ ".sv-b a{color:" + COLOR + ";text-decoration:underline;word-break:break-all}"
+ ".sv-imgs{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px}.sv-imgs a{display:block;line-height:0}"
+ ".sv-imgs img{width:72px;height:96px;object-fit:cover;border-radius:8px;background:#dcdde1;border:1px solid #d1d5db}"
+ ".sv-line-btn{display:inline-block;margin-top:10px;padding:8px 16px;background:#06C755;color:#fff !important;text-decoration:none !important;border-radius:8px;font-weight:bold;font-size:14px;text-align:center;}"
+ ".sv-lead{margin-top:10px;display:flex;flex-direction:column;gap:6px}"
+ ".sv-lead .t{font-size:13px;color:#374151}"
+ ".sv-lead input{padding:7px 9px;border:1px solid #d1d5db;border-radius:7px;font-size:13px}"
+ ".sv-lead button{padding:7px;background:" + COLOR + ";color:#fff;border:0;border-radius:7px;font-size:13px;cursor:pointer}"
+ ".sv-lead button:disabled{opacity:.6}";
var st = document.createElement("style"); st.textContent = css; document.head.appendChild(st);
var btn = document.createElement("button"); btn.className = "sv-btn";
if (AVATAR) { btn.classList.add("sv-img"); btn.style.backgroundImage = "url('" + encodeURI(AVATAR) + "')"; }
else { btn.textContent = "💬"; }
var box = document.createElement("div"); box.className = "sv-box";
box.innerHTML = '
'
+ ''
+ (DISCLAIMER ? '' : '');
document.body.appendChild(btn); document.body.appendChild(box);
var hd = box.querySelector(".sv-hd");
if (AVATAR) { var av = document.createElement("img"); av.className = "sv-av"; av.src = AVATAR; av.alt = ""; hd.appendChild(av); }
hd.appendChild(document.createTextNode(NAME)); // textContent 避免店名含 HTML 造成 XSS
if (DISCLAIMER) box.querySelector(".sv-dc").textContent = DISCLAIMER;
var logEl = box.querySelector("#svLog"), inEl = box.querySelector("#svIn"), sendEl = box.querySelector("#svSend");
// 只渲染我們自己抓取入庫的官網 https 圖片;用
元素而非 innerHTML,杜絕注入
function addImages(bubble, images) {
if (!images || !images.length) return;
var box = document.createElement("div"); box.className = "sv-imgs";
images.forEach(function (src) {
if (typeof src !== "string" || !/^https:\/\//.test(src)) return;
var a = document.createElement("a"); a.href = src; a.target = "_blank"; a.rel = "noopener";
var img = document.createElement("img"); img.src = src; img.loading = "lazy"; img.alt = "商品圖片";
img.onerror = function () { a.remove(); };
a.appendChild(img); box.appendChild(a);
});
if (box.children.length) bubble.appendChild(box);
}
function add(role, text, images) {
var m = document.createElement("div"); m.className = "sv-m " + role;
var b = document.createElement("div"); b.className = "sv-b";
var re = /(https?:\/\/[^\s]+)/g, last = 0, mm;
while ((mm = re.exec(text)) !== null) {
if (mm.index > last) b.appendChild(document.createTextNode(text.slice(last, mm.index)));
var a = document.createElement("a"); a.href = mm[0]; a.target = "_blank"; a.rel = "noopener"; a.textContent = mm[0];
b.appendChild(a); last = mm.index + mm[0].length;
}
if (last < text.length) b.appendChild(document.createTextNode(text.slice(last)));
addImages(b, images);
m.appendChild(b); logEl.appendChild(m); logEl.scrollTop = logEl.scrollHeight;
return b; // Return bubble to append buttons
}
// 捕獲客戶:AI 答不出來時,請客人留個聯絡方式,存進店家後台並通知店長
function leadForm(bubble, question) {
var wrap = document.createElement("div"); wrap.className = "sv-lead";
var tip = document.createElement("div"); tip.className = "t";
tip.textContent = "留個聯絡方式,店長親自回覆您:";
var nm = document.createElement("input"); nm.placeholder = "稱呼(選填)"; nm.maxLength = 60;
var ct = document.createElement("input"); ct.placeholder = "電話 / LINE / Email"; ct.maxLength = 120;
var bt = document.createElement("button"); bt.textContent = "送出給店長";
wrap.appendChild(tip); wrap.appendChild(nm); wrap.appendChild(ct); wrap.appendChild(bt);
bubble.appendChild(wrap); logEl.scrollTop = logEl.scrollHeight;
bt.onclick = function () {
var c = ct.value.trim(); if (!c) { ct.focus(); return; }
bt.disabled = true; bt.textContent = "送出中…";
fetch(BASE + "/api/lead", {method:"POST",headers:{"Content-Type":"application/json"},
body: JSON.stringify({slug:SLUG, contact:c, name:nm.value.trim(), question:question, session_id:SID})})
.then(function(r){return r.json();}).then(function(d){
wrap.innerHTML = "";
var ok = document.createElement("div"); ok.className = "t";
ok.textContent = d.ok ? "✓ 已收到,店長會盡快用您留的方式聯絡您 🙏" : "送出失敗,請稍後再試。";
wrap.appendChild(ok);
})
.catch(function(){ bt.disabled = false; bt.textContent = "送出給店長"; });
};
}
add("a", WELCOME);
btn.onclick = function () { box.style.display = box.style.display === "flex" ? "none" : "flex"; inEl.focus(); };
// 點對話視窗以外(且非泡泡按鈕)即收起;Esc 也可關閉
document.addEventListener("click", function (e) {
if (box.style.display === "flex" && !box.contains(e.target) && e.target !== btn) box.style.display = "none";
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && box.style.display === "flex") box.style.display = "none";
});
var busy = false;
function ask() {
if (busy) return; // 防連點:請求進行中不再送
var t = inEl.value.trim(); if (!t) return;
busy = true; sendEl.disabled = true;
add("u", t); inEl.value = ""; add("a", "…");
var ph = logEl.lastChild;
fetch(BASE + "/api/chat", {method:"POST",headers:{"Content-Type":"application/json"},
body: JSON.stringify({slug:SLUG,message:t,session_id:SID})})
.then(function(r){return r.json();}).then(function(d){
ph.remove();
var bubble = add("a", d.answer, d.images);
// AI 答不出來(且非被擋/限流)→ 請客人留資,順帶提供 LINE 即時聯絡
if (d.fallback && !d.blocked && !d.rate_limited) {
leadForm(bubble, t);
if (CONTACT_LINE) {
var lbtn = document.createElement("a");
lbtn.href = CONTACT_LINE; lbtn.target = "_blank"; lbtn.className = "sv-line-btn";
lbtn.textContent = "💬 或直接加 LINE 聯絡店長";
bubble.appendChild(lbtn);
}
logEl.scrollTop = logEl.scrollHeight;
}
})
.catch(function(){ ph.remove(); add("a","連線發生問題,請稍後再試。"); })
.finally(function(){ setTimeout(function(){ busy = false; sendEl.disabled = false; }, 800); });
}
sendEl.onclick = ask;
inEl.addEventListener("keydown", function(e){ if(e.key==="Enter") ask(); });
})();