/* Coda 客答 嵌入式客服 Widget — 商家:粽生有幸 用法: */ (function () { var SLUG = "zongzi", BASE = "https://coda.tw", COLOR = "#3f7d4f"; var WELCOME = "\u6b61\u8fce\u5149\u81e8\u7cbd\u751f\u6709\u5e78 \ud83d\udc32 \u6211\u662f\u963f\u7cbd\uff0c\u5de5\u865f No.0710\uff0c\u6c38\u4e0d\u4e0b\u73ed\u7684 AI \u5ba2\u670d\u3002\u60f3\u554f\u53e3\u5473\u3001\u6210\u5206\u3001\u4fdd\u5b58\u3001\u5b85\u914d\uff0c\u9084\u662f\u7aef\u5348\u6a94\u671f\uff1f\uff08\u542b\u82b1\u751f\uff0f\u904e\u654f\u554f\u984c\u6211\u6703\u8a8d\u771f\u5e6b\u60a8\u67e5\uff0c\u4e0d\u4e82\u731c\uff09", NAME = "\u7cbd\u751f\u6709\u5e78", DISCLAIMER = "\u7cbd\u751f\u6709\u5e78\u70ba Coda \u5ba2\u7b54\u793a\u7bc4\u7528\u7684\u865b\u69cb\u5e97\u5bb6\uff0c\u5546\u54c1\u3001\u5730\u5740\u8207\u96fb\u8a71\u7686\u975e\u771f\u5be6\uff0c\u50c5\u4f9b\u9ad4\u9a57\u3002"; var CONTACT_LINE = ""; var AVATAR = ""; var SID = "s-" + Math.random().toString(36).slice(2); // 觸控裝置開窗時不自動 focus 輸入框,避免鍵盤立刻彈出蓋掉歡迎訊息 var IS_TOUCH = ("ontouchstart" in window) || (navigator.maxTouchPoints || 0) > 0; 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:380px;max-width:92vw;height:560px;max-height:78vh;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-box.sv-full{right:0;bottom:0;top:0;left:0;width:100%;max-width:none;height:100%;max-height:none;border-radius:0}" + "@media (max-width:480px){.sv-box{right:0;bottom:0;top:0;left:0;width:100%;max-width:none;height:100%;max-height:none;border-radius:0}}" + ".sv-hd{background:" + COLOR + ";color:#fff;padding:12px 14px;font-weight:600;display:flex;align-items:center;gap:8px}" + ".sv-ttl{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" + ".sv-ic{background:transparent;border:0;color:#fff;cursor:pointer;font-size:17px;line-height:1;padding:4px;opacity:.85;flex:0 0 auto}.sv-ic:hover{opacity:1}" + "@media (max-width:480px){.sv-exp{display:none}}" + ".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;align-items:flex-end}.sv-m.u{justify-content:flex-end}" + ".sv-b{max-width:78%;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-meta{font-size:10px;color:#9ca3af;margin:0 5px;line-height:1.35;white-space:nowrap;display:flex;flex-direction:column}" + ".sv-m.u .sv-meta{align-items:flex-end}.sv-m.a .sv-meta{align-items:flex-start}" + ".sv-tick.read{color:#22a559}" + ".sv-bar{display:flex;gap:6px;padding:8px;border-top:1px solid #e5e7eb;align-items:center}" + ".sv-bar input{flex:1;min-width:0;padding:8px;border:1px solid #d1d5db;border-radius:8px;font-size:16px}" /* 16px 防 iOS 點擊自動放大;min-width:0 讓輸入框可縮、不擠掉按鈕 */ + ".sv-bar button{flex:0 0 auto;padding:8px 12px;background:" + COLOR + ";color:#fff;border:0;border-radius:8px;cursor:pointer;white-space:nowrap}" + ".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:16px}" + ".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); } var ttl = document.createElement("span"); ttl.className = "sv-ttl"; ttl.textContent = NAME; // textContent 避免店名含 HTML 造成 XSS hd.appendChild(ttl); // 放大/縮小切換(桌機用;手機本就全螢幕,CSS 已隱藏此鈕) var expBtn = document.createElement("button"); expBtn.className = "sv-ic sv-exp"; expBtn.title = "放大"; expBtn.setAttribute("aria-label", "放大視窗"); expBtn.textContent = "⤢"; expBtn.onclick = function () { var full = box.classList.toggle("sv-full"); expBtn.textContent = full ? "⤡" : "⤢"; expBtn.title = full ? "縮小" : "放大"; if (!IS_TOUCH) inEl.focus(); }; hd.appendChild(expBtn); var clsBtn = document.createElement("button"); clsBtn.className = "sv-ic"; clsBtn.title = "關閉"; clsBtn.setAttribute("aria-label", "關閉視窗"); clsBtn.textContent = "✕"; clsBtn.onclick = function () { box.style.display = "none"; }; hd.appendChild(clsBtn); if (DISCLAIMER) box.querySelector(".sv-dc").textContent = DISCLAIMER; var logEl = box.querySelector("#svLog"), inEl = box.querySelector("#svIn"), sendEl = box.querySelector("#svSend"); function nowHM() { var d = new Date(); return ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2); } // 只渲染我們自己抓取入庫的官網 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); if (text === "…") { // 「輸入中」佔位泡泡:不加時間/勾勾 m.appendChild(b); } else { var meta = document.createElement("div"); meta.className = "sv-meta"; var tm = document.createElement("span"); tm.textContent = nowHM(); if (role === "u") { var tick = document.createElement("span"); tick.className = "sv-tick"; tick.textContent = "✓"; // 已傳送;收到回覆後升級 ✓✓ meta.appendChild(tick); meta.appendChild(tm); m.appendChild(meta); m.appendChild(b); // 自己的訊息:時間/勾勾在泡泡左側(仿 LINE) b.statusEl = tick; } else { meta.appendChild(tm); m.appendChild(b); m.appendChild(meta); // 對方訊息:時間在泡泡右側 } } logEl.appendChild(m); logEl.scrollTop = logEl.scrollHeight; return b; // Return bubble to append buttons / status element } // 捕獲客戶: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 () { var open = box.style.display !== "flex"; box.style.display = open ? "flex" : "none"; if (open) { // 全新對話(只有歡迎語)捲到最上面先看到歡迎;已聊過則回到最新訊息 logEl.scrollTop = logEl.children.length > 1 ? logEl.scrollHeight : 0; if (!IS_TOUCH) inEl.focus(); // 桌機才自動 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; var ub = 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(); if (ub && ub.statusEl) { ub.statusEl.textContent = "✓✓"; ub.statusEl.classList.add("read"); } // 阿粽已收到→已讀 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(); }); })();