/* Coda 客答 嵌入式客服 Widget — 商家:晴日珈琲 用法: */ (function () { var SLUG = "cafe", BASE = "https://coda.tw", COLOR = "#5b3a29"; var WELCOME = "\u6b61\u8fce\u5149\u81e8\u6674\u65e5\u73c8\u7432 \u2615 \u60f3\u4e86\u89e3\u83dc\u55ae\u3001\u71df\u696d\u6642\u9593\uff0c\u9084\u662f\u60f3\u8a02\u4f4d\u5462\uff1f", NAME = "\u6674\u65e5\u73c8\u7432", DISCLAIMER = "\u6674\u65e5\u73c8\u7432\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(); }); })();