Files
ai-kefu-jc/static/widget.js
T
2026-09-30 13:59:33 +08:00

706 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
var script = document.currentScript || (function () {
var scripts = document.getElementsByTagName("script");
return scripts[scripts.length - 1];
})();
// Save config immediately (before DOM ready)
var position = script.getAttribute("data-position") || "bottom-right";
var theme = script.getAttribute("data-theme") || "light";
var title = script.getAttribute("data-title") || "HESUPIPE Support";
var autoOpen = script.getAttribute("data-auto-open") === "true";
var whatsapp = script.getAttribute("data-whatsapp") || "https://wa.me/+8615937108317";
var email = script.getAttribute("data-email") || "mailto:hesu@hesuu.com";
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
var isMobile = script.getAttribute("data-mobile") === "true";
var wsBase = new URL(script.src).origin;
/* ===== 主题派生单元:与 static/js/theme.js 逐字同步,tests/test_theme_static.py 强制;只可整块替换,不可单边修改 ===== */
var DEFAULT_COLOR = "#066fc0";
var DEFAULT_PALETTE = {
primary: "#066fc0",
primaryRgb: "6,111,192",
gradientEnd: "#0666bb",
deepText: "#14517a",
captionText: "#2174ab",
softBg: "#f3f9fe",
borderSoft: "#bbdcf2"
};
function clampNum(v, lo, hi) {
return Math.min(hi, Math.max(lo, v));
}
function hexToRgb(hex) {
hex = String(hex || "").replace(/^#/, "");
if (hex.length === 3) {
hex = hex.split("").map(function (ch) { return ch + ch; }).join("");
}
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16)
};
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var l = (max + min) / 2;
var d = max - min;
var h = 0, s = 0;
if (d !== 0) {
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = ((g - b) / d) % 6;
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
if (h < 0) h += 360;
}
return { h: h, s: s * 100, l: l * 100 };
}
function toHex2(v) {
var s = Math.round(clampNum(v, 0, 255)).toString(16);
return s.length < 2 ? "0" + s : s;
}
function hslToHex(h, s, l) {
h = ((h % 360) + 360) % 360;
s = clampNum(s, 0, 100) / 100;
l = clampNum(l, 0, 100) / 100;
var c = (1 - Math.abs(2 * l - 1)) * s;
var x = c * (1 - Math.abs(((h / 60) % 2) - 1));
var m = l - c / 2;
var rp = 0, gp = 0, bp = 0;
if (h < 60) { rp = c; gp = x; }
else if (h < 120) { rp = x; gp = c; }
else if (h < 180) { gp = c; bp = x; }
else if (h < 240) { gp = x; bp = c; }
else if (h < 300) { rp = x; bp = c; }
else { rp = c; bp = x; }
return "#" + toHex2((rp + m) * 255) + toHex2((gp + m) * 255) + toHex2((bp + m) * 255);
}
function derivePalette(color) {
try {
var rgb = hexToRgb(color);
if (!rgb) {
if (typeof console !== "undefined" && console.warn) console.warn("[chat-theme] invalid color, fallback:", color);
return DEFAULT_PALETTE;
}
var hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
var h = hsl.h, s = hsl.s, l = hsl.l;
return {
primary: "#" + toHex2(rgb.r) + toHex2(rgb.g) + toHex2(rgb.b),
primaryRgb: rgb.r + "," + rgb.g + "," + rgb.b,
gradientEnd: hslToHex(h + 2, s, l - 1),
deepText: hslToHex(h - 2, s * 0.76, clampNum(l - 24, 28, 40)),
captionText: hslToHex(h - 2, s * 0.72, clampNum(l - 10, 40, 52)),
softBg: hslToHex(h, s * 0.9, 97.5),
borderSoft: hslToHex(h - 2, s * 0.72, 84)
};
} catch (e) {
return DEFAULT_PALETTE;
}
}
/* ===== 主题派生单元结束 ===== */
var DEFAULT_AVATAR_PATH = "/static/kefu-small.png";
var avatarAttr = (script.getAttribute("data-avatar") || "").trim();
var avatarUrl = avatarAttr
? (/^https?:\/\//i.test(avatarAttr) ? avatarAttr : wsBase + "/" + avatarAttr.replace(/^\//, ""))
: wsBase + DEFAULT_AVATAR_PATH;
var colorAttr = (script.getAttribute("data-color") || "").trim();
var palette = derivePalette(colorAttr || DEFAULT_COLOR);
if (script.getAttribute("data-debug") === "true") {
window.__chatWidgetDebug = { derivePalette: derivePalette, palette: palette };
}
var positionStyles = {
"bottom-right": { bottom: "80px", right: "10px" },
"bottom-left": { bottom: "20px", left: "20px" },
"top-right": { top: "20px", right: "20px" },
"top-left": { top: "20px", left: "20px" },
};
var positionStylesMobile = {
"bottom-right": { bottom: "20px", right: "16px" },
"bottom-left": { bottom: "20px", left: "16px" },
"top-right": { top: "20px", right: "16px" },
"top-left": { top: "20px", left: "16px" },
};
var colors = {
light: { bg: "#ffffff", text: "#333", bubble: "#f0f0f0" },
dark: { bg: "#1e1e1e", text: "#e0e0e0", bubble: "#2d2d2d" },
};
var c = colors[theme] || colors.light;
// 无效 position 归一化(防 "pos-垃圾" class 与查表错位)
if (!positionStyles[position]) position = "bottom-right";
var pos = (isMobile ? positionStylesMobile : positionStyles)[position]
|| (isMobile ? positionStylesMobile : positionStyles)["bottom-right"];
function init() {
var host = document.createElement("div");
host.id = "cc-widget-host";
document.body.appendChild(host);
var shadow = host.attachShadow({ mode: "open" });
// Inject styles
var style = document.createElement("style");
style.textContent = [
":host{--w-primary:" + palette.primary + ";--w-primary-rgb:" + palette.primaryRgb + ";--w-gradient-end:" + palette.gradientEnd + ";--w-deep-text:" + palette.deepText + ";--w-caption-text:" + palette.captionText + ";--w-soft-bg:" + palette.softBg + ";--w-border-soft:" + palette.borderSoft + "}",
"* { margin:0; padding:0; box-sizing:border-box; }",
".widget-container { position:fixed; z-index:99999; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }",
".widget-container { " + Object.keys(pos).map(function (k) { return k + ":" + pos[k]; }).join(";") + "; }",
// Toggle button
".toggle-btn { width:auto; height:auto; background:var(--w-primary); border-radius:30px; padding:8px 20px; display:flex; align-items:center; gap:12px; box-shadow:0 4px 16px rgba(var(--w-primary-rgb),0.4); transition:transform 0.2s; cursor:pointer; border:none; }",
".toggle-btn:hover { transform:scale(1.1); }",
".toggle-btn.mobile { flex-direction:column; gap:5px; padding:0; background:transparent; box-shadow:none; border-radius:0; }",
".toggle-btn.mobile:hover { transform:scale(1.05); }",
".toggle-btn.mobile .mobile-circle { position:relative; width:56px; height:56px; border-radius:50%; background:var(--w-primary); display:flex; align-items:center; justify-content:center; font-size:28px; box-shadow:0 4px 16px rgba(var(--w-primary-rgb),0.45); }",
".toggle-btn.mobile .mobile-dot { position:absolute; bottom:2px; right:2px; width:13px; height:13px; background:#22c55e; border-radius:50%; border:2px solid var(--w-primary); }",
".toggle-btn.mobile .mobile-label { color:var(--w-deep-text); font-size:10px; font-weight:600; line-height:1.1; }",
".toggle-avatar-wrap { position:relative; }",
".toggle-avatar-circle { position:relative; width:36px; height:36px; background:rgba(255,255,255,0.2); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:18px; }",
".toggle-avatar-dot { position:absolute; top:0; right:0; width:10px; height:10px; background:#22c55e; border-radius:50%; border:2px solid var(--w-primary); }",
".avatar-emoji { line-height:1; }",
".avatar-img { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; border-radius:50%; }",
// 移动端 D2 按钮比桌面药丸高(72px vs 52px),抬高面板底边避免压住按钮(覆盖面板默认 80px 与 @media 60px)
".widget-container.mobile-host .chat-panel { bottom:84px; }",
".widget-container.mobile-host { bottom:70px; }",
// Chat panel
".chat-panel { position:absolute; bottom:80px; right:0; width:400px; height:540px; background:" + c.bg + "; border-radius:16px; box-shadow:0 10px 40px rgba(0,0,0,0.15); display:none; flex-direction:column; overflow:hidden; }",
".chat-panel.open { display:flex; }",
// 位置联动(2026-09-30 修复): 面板/邀请卡对齐随 data-position 翻转,
// 默认 right:0 只适配 bottom-right——左侧模式溢出屏幕、顶部模式向上溢出
// (邀请卡 invite-container 与 chat-panel 同款定位,必须一起联动)
".widget-container.pos-bottom-left .chat-panel,"
+ " .widget-container.pos-bottom-left .invite-container { left:0; right:auto; }",
".widget-container.pos-top-right .chat-panel,"
+ " .widget-container.pos-top-right .invite-container { top:0; bottom:auto; }",
".widget-container.pos-top-left .chat-panel,"
+ " .widget-container.pos-top-left .invite-container { left:0; right:auto; top:0; bottom:auto; }",
// Panel header
".panel-header { display:flex; align-items:center; gap:12px; padding:14px 18px; background:var(--w-primary); color:white; }",
".panel-header .avatar { width:48px; height:48px; background:rgba(255,255,255,0.2); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:22px; position:relative; }",
".panel-header .avatar::after { content:''; position:absolute; bottom:1px; right:1px; width:14px; height:14px; background:#22c55e; border-radius:50%; border:3px solid var(--w-primary); }",
".panel-header .info { flex:1; }",
".panel-header .info h3 { font-size:17px; font-weight:600; margin:0; }",
".panel-header .info .status { font-size:13px; opacity:0.95; margin:0; }",
".panel-header .actions { display:flex; gap:8px; }",
".panel-header .action-btn { width:32px; height:32px; border-radius:8px; background:rgba(255,255,255,0.15); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background 0.2s; border:none; color:white; }",
".panel-header .action-btn:hover { background:rgba(255,255,255,0.25); }",
// Messages
".panel-top { flex-shrink:0; padding:10px 16px; background:var(--w-soft-bg); }",
".panel-messages { flex:1; overflow-y:auto; padding:16px; background:#ffffff; display:flex; flex-direction:column; gap:8px; padding-bottom:8px; }",
".popular-topics-fixed { flex-shrink:0; padding:12px 16px 8px; background:#ffffff; }",
".chat-separator-topics { height:1px; background:var(--w-primary); opacity:0.15; flex-shrink:0; }",
".help-block { background:var(--w-soft-bg); border-radius:12px; padding:10px 14px; border:1px solid var(--w-border-soft); }",
".help-block .help-content { flex:1; min-width:0; }",
".help-block .help-title { font-size:16px; font-weight:700; color:#111827; margin-bottom:3px; }",
".help-block .help-description { font-size:12px; color:#6b7280; line-height:1.4; }",
".help-block .help-actions { display:grid; gap:8px; flex-shrink:0; }",
".help-block .call-btn { justify-content: center;background: #fff; color: var(--w-deep-text);border: 1px solid var(--w-border-soft);padding: 6px 10px; border-radius:20px; font-size:13px; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:5px; text-decoration:none; flex-shrink:0; transition:all 0.2s; }",
// ".help-block .whatsapp-btn:hover { box-shadow:0 4px 12px rgba(37,211,102,0.4); transform:translateY(-1px); }",
// ".help-block .email-btn { background:linear-gradient(135deg, #EA4335 0%, #C5221F 100%); }",
// ".help-block .email-btn:hover { box-shadow:0 4px 12px rgba(234,67,53,0.4); transform:translateY(-1px); }",
".popular-topics-label { font-size:11px; font-weight:600; color:var(--w-caption-text); letter-spacing:0.14em; margin-bottom:8px; text-transform:uppercase; }",
".popular-topics-grid { display:flex; gap:8px; flex-wrap:wrap; }",
".popular-topic-btn { border-radius:20px; border:1px solid var(--w-border-soft); background:var(--w-soft-bg); padding:8px 16px; font-size:12px; font-weight:500; color:var(--w-deep-text); cursor:pointer; transition:background 0.2s; white-space:nowrap; }",
".popular-topic-btn:hover { background:white; }",
".msg-wrapper { display:flex; gap:10px; }",
".msg-wrapper .avatar { width:32px; height:32px; background:var(--w-primary); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:14px; color:white; flex-shrink:0; box-shadow:0 2px 8px rgba(var(--w-primary-rgb),0.3); }",
".msg { max-width:280px; padding:12px 16px; border-radius:16px; font-size:14px; line-height:1.5; word-break:break-word; }",
".msg.user { align-self:flex-end; background:var(--w-primary); color:#fff; border-bottom-right-radius:4px; }",
".msg.ai { background:#f3f4f6; color:#1f2937; border-bottom-left-radius:4px; }",
// Separator
".chat-separator { height:1px; background:var(--w-primary); opacity:0.3; flex-shrink:0; }",
// Footer
".chat-footer { padding:10px 10px; background:white; display:flex; align-items:center; justify-content:space-between; font-size:11px; color:var(--w-caption-text); gap:20px; }",
".chat-footer .footer-left { flex:1; }",
".chat-footer .footer-right { flex-shrink:0; }",
".chat-footer a { color:var(--w-caption-text); text-decoration:underline; }",
// Typing
".typing { display:none; align-self:flex-start; padding:8px 12px; background:" + c.bubble + "; border-radius:12px; }",
".typing.active { display:flex; gap:4px; align-items:center; }",
".typing span { width:6px; height:6px; background:#999; border-radius:50%; animation:blink 1.4s infinite; }",
".typing span:nth-child(2){animation-delay:0.2s} .typing span:nth-child(3){animation-delay:0.4s}",
"@keyframes blink { 0%,60%,100%{transform:translateY(0);opacity:0.4} 30%{transform:translateY(-4px);opacity:1} }",
// Input
".input-container { padding:10px 10px; background:white; }",
".input-wrapper { display:flex; align-items:center; gap:0; padding:8px 16px; background:var(--w-soft-bg); border-radius:28px; box-shadow:0 1px 3px rgba(0,0,0,0.05); }",
".input-wrapper input { flex:1; padding:0; border:none; font-size:14px; background:transparent; outline:none; min-height:20px; color:#1f2937; }",
".input-wrapper input::placeholder { color:#9ca3af; }",
".input-separator { width:1px; height:24px; background:#e5e7eb; margin:0 8px; flex-shrink:0; }",
".input-wrapper button { padding:8px 16px; border:none; border-radius:12px; background:linear-gradient(135deg, var(--w-primary), var(--w-gradient-end)); color:#fff; font-size:14px; font-weight:500; cursor:pointer; display:flex; align-items:center; gap:6px; white-space:nowrap; box-shadow:0 2px 8px rgba(var(--w-primary-rgb),0.25); flex-shrink:0; }",
".input-wrapper button:disabled { background:#ccc; box-shadow:none; }",
// Ended
".ended { display:none; text-align:center; padding:8px; background:#e8f5e9; color:#34a853; font-size:12px; }",
".ended.active { display:block; }",
// Invite card container
// pointer-events:none:容器永不拦截点击,展示态由 .invite-panel.show 的 auto 恢复交互
".invite-container { z-index:99998;position: absolute;bottom: 80px;right: 0;pointer-events:none; }",
".invite-panel { width:360px; max-width:calc(100vw - 24px); overflow:hidden; border-radius:26px; border:1px solid var(--w-border-soft); background:white; box-shadow:0 28px 80px rgba(var(--w-primary-rgb),0.22); opacity:0; transform:translateY(20px); transition:opacity 0.3s, transform 0.3s; pointer-events:none; }",
".invite-panel.show { opacity:1; transform:translateY(0); pointer-events:auto; }",
".invite-panel.hide { display:none; }",
// Invite header
".invite-header { background:linear-gradient(135deg, var(--w-primary), var(--w-gradient-end)); padding:16px 20px; color:white; display:flex; gap:12px; }",
".invite-icon-wrapper { flex-shrink:0; }",
".invite-icon { position:relative;width:40px; height:40px; background:rgba(255,255,255,0.18); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:18px; backdrop-filter:blur(10px); }",
".invite-status-dot { position:absolute; bottom:-2px; right:-2px; width:12px; height:12px; background:#22c55e; border-radius:50%; border:3px solid var(--w-primary); }",
".invite-header-content { flex:1; min-width:0; }",
".invite-title { font-size:15px; font-weight:600; line-height:1.3; }",
".invite-subtitle { font-size:12px; margin-top:2px; opacity:0.9; line-height:1.4; }",
".invite-header-actions { display:flex; flex-direction:column; gap:6px; flex-shrink:0; margin-left:auto; }",
".invite-close-btn { align-self: flex-end; width:28px; height:28px; border-radius:6px; background:rgba(255,255,255,0.1); border:none; color:white; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background 0.2s; flex-shrink:0; }",
".invite-close-btn:hover { background:rgba(255,255,255,0.2); }",
".invite-action-btn { justify-content: center; background:#fff; color:var(--w-deep-text); border:1px solid var(--w-border-soft); padding:5px 10px; border-radius:20px; font-size:12px; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:5px; text-decoration:none; flex-shrink:0; transition:all 0.2s; white-space:nowrap; }",
".invite-action-btn:hover { background:var(--w-soft-bg); }",
// Invite body
".invite-body { padding:16px 8px; background:var(--w-soft-bg); }",
// Topic buttons
".invite-topics { display:grid; grid-template-columns:1fr 1fr; gap:5px; margin-bottom:16px; }",
".invite-topic-btn { border-radius:22px; border:1px solid var(--w-border-soft); background:white; padding:14px 10px; text-align:center; font-size:13px; font-weight:500; color:var(--w-deep-text); cursor:pointer; transition:background 0.2s; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }",
".invite-topic-btn:hover { background:var(--w-soft-bg); }",
// Action buttons
".invite-actions { display:flex; gap:10px; }",
".invite-start-btn { flex:1; border-radius:20px; border:none; background:linear-gradient(135deg, var(--w-primary), var(--w-gradient-end)); padding:14px 20px; color:white; font-size:14px; font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; box-shadow:0 2px 8px rgba(var(--w-primary-rgb),0.25); transition:opacity 0.2s; }",
".invite-start-btn:hover { opacity:0.95; }",
".invite-dismiss-btn { border-radius:20px; border:1px solid var(--w-border-soft); background:white; padding:14px 20px; color:var(--w-deep-text); font-size:14px; font-weight:500; cursor:pointer; transition:background 0.2s; }",
".invite-dismiss-btn:hover { background:#FEFEFE; }",
".invite-privacy { padding:2px 16px 12px; font-size:11px; color:var(--w-caption-text); text-align:center; }",
".invite-privacy a { color:var(--w-caption-text); text-decoration:underline; }",
// Responsive
"@media(max-width:450px){ .chat-panel{width:calc(100vw - 20px);height:calc(100vh - 200px);bottom:60px;right:0px;} .invite-panel{width:calc(100vw - 24px);} .invite-topics{grid-template-columns:1fr;} }",
].join("\n");
shadow.appendChild(style);
// Build DOM
var container = document.createElement("div");
container.className = "widget-container pos-" + position + (isMobile ? " mobile-host" : "");
var panel = document.createElement("div");
panel.className = "chat-panel";
panel.innerHTML = [
'<div class="panel-header">',
'<div class="avatar"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="'+avatarUrl+'"></div>',
'<div class="info"><h3>' + title + '</h3><div class="status">Online now · Usually replies in minutes</div></div>',
'<div class="actions">',
'<button class="action-btn" aria-label="Minimize">−</button>',
// '<button class="close-btn action-btn" aria-label="Close">×</button>',
'</div></div>',
'<div class="ended" id="w-ended">Chat ended. We\'ll be in touch!</div>',
'<div class="panel-top">',
'<div class="help-block"><div style="display:flex;align-items:center;gap:14px;"><div class="help-content"><div class="help-title">How can we help?</div><div class="help-description">Ask about pipe specifications, quotations, MOQ, shipping and delivery.</div></div><div class="help-actions"><a href="' + whatsapp + '" target="_blank" class="call-btn whatsapp-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg><span>WhatsApp</span></a><a href="' + email + '" class="call-btn email-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg><span>Email</span></a></div></div></div>',
'</div>',
'<div class="chat-separator"></div>',
'<div class="panel-messages" id="w-msgs">',
'<div class="msg-wrapper"><div class="msg ai">What kind of piping project are you working on?</div></div>',
'</div>',
'<div class="typing" id="w-typing"><span></span><span></span><span></span></div>',
'<div class="popular-topics-fixed" id="w-popular-topics"><div class="popular-topics-label">POPULAR TOPICS</div><div class="popular-topics-grid"><button class="popular-topic-btn" data-topic="Get Quotation">Get Quotation</button><button class="popular-topic-btn" data-topic="Pipe Specifications">Pipe Specifications</button><button class="popular-topic-btn" data-topic="Delivery &amp; Shipping">Delivery &amp; Shipping</button></div></div>',
'<div class="chat-separator-topics"></div>',
'<div class="input-container"><div class="input-wrapper">',
'<input type="text" id="w-input" placeholder="Type a message...">',
'<button id="w-send"><span style="font-size:16px;">➤</span><span>Send</span></button>',
'</div></div>',
'<div class="chat-footer"><span class="footer-left">By chatting, you agree to our <a href="https://hesupipe.com/privacy-policy/" target="_blank" rel="noopener">privacy policy</a>.</span></div>',
].join("");
// Invite card HTML template
var inviteTemplate = [
'<div class="invite-panel hide">',
'<div class="invite-header">',
'<div class="invite-icon-wrapper">',
'<div class="invite-icon"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="'+avatarUrl+'"><span class="invite-status-dot"></span></div>',
'</div>',
'<div class="invite-header-content">',
'<div class="invite-title">Chat with us for pipe specs, MOQ, delivery timing and quotations.</div>',
'</div>',
'<div class="invite-header-actions">',
'<button class="invite-close-btn" aria-label="Close">',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
'</button>',
'<a href="' + whatsapp + '" target="_blank" class="invite-action-btn whatsapp-btn">',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>',
'<span>WhatsApp</span>',
'</a>',
'<a href="' + email + '" class="invite-action-btn email-btn">',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>',
'<span>Email</span>',
'</a>',
'</div>',
'</div>',
'<div class="invite-body">',
'<div class="invite-topics">',
'<button class="invite-topic-btn" data-topic="Get Quotation">Get Quotation</button>',
'<button class="invite-topic-btn" data-topic="Pipe Specifications">Pipe Specifications</button>',
'<button class="invite-topic-btn" data-topic="Delivery &amp; Shipping">Delivery &amp; Shipping</button>',
'</div>',
'</div>',
'<div class="chat-separator"></div>',
'<div class="input-container">',
'<div class="invite-actions">',
'<button class="invite-start-btn">Start Chat',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg>',
'</button>',
'<button class="invite-dismiss-btn">No Thanks</button>',
'</div>',
'<div class="invite-privacy">By chatting, you agree to our <a href="https://hesupipe.com/privacy-policy/" target="_blank" rel="noopener">privacy policy</a>.</div>',
'</div>',
'</div>'
].join("");
var inviteEl = document.createElement("div");
inviteEl.className = "invite-container";
inviteEl.innerHTML = inviteTemplate;
// 不变量:面板出厂即 hide(display:none)—— 任何不经过 showInvite 的路径都零占位。
// 2026-09-01 修复:此前 permanent 关闭后刷新时 showInvite 不运行、invitePanel 恒为
// null、hideInvite 提前返回,隐形 invite-panel(display:block + opacity:0)在
// z-index:99998 容器里拦截右下角点击。出厂 hide 取代原 !autoInvite 特判。
var toggleBtn = document.createElement("button");
toggleBtn.className = "toggle-btn" + (isMobile ? " mobile" : "");
if (isMobile) {
toggleBtn.innerHTML = `
<div class="mobile-circle"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="${avatarUrl}"><span class="mobile-dot"></span></div>
<span class="mobile-label">Chat with us</span>
`;
} else {
toggleBtn.innerHTML = `
<div class="toggle-avatar-wrap">
<div class="toggle-avatar-circle"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="${avatarUrl}"></div>
<div class="toggle-avatar-dot"></div>
</div>
<div style="color:white;">
<div style="font-size:14px;font-weight:600;">Chat with us</div>
<div style="font-size:11px;opacity:0.9;">Usually replies quickly</div>
</div>
`;
}
container.appendChild(panel);
container.appendChild(inviteEl);
container.appendChild(toggleBtn);
shadow.appendChild(container);
// 头像兜底:load 成功隐藏底层 emoji;error 移除 img 露出 emoji(spec §12.4)
Array.prototype.forEach.call(shadow.querySelectorAll(".avatar-img"), function (img) {
function hideEmoji() {
var s = img.parentElement && img.parentElement.querySelector(".avatar-emoji");
if (s) s.style.display = "none";
}
if (img.complete && img.naturalWidth > 0) { hideEmoji(); return; }
img.addEventListener("load", hideEmoji);
img.addEventListener("error", function () { img.remove(); });
});
// Widget state
var isOpen = autoOpen;
var wSessionId = null;
var wWs = null;
var wEnded = false;
var streamBubble = null;
var pageToken = crypto.randomUUID();
var pendingMessage = null; // Queue message to send after WebSocket opens
// Invite card state management
var inviteTimer = null;
var invitePanel = null;
var inviteShown = false;
var inviteStorageKey = "widget_invite_dismissed_" + window.location.pathname;
var isPermanentlyClosed = false;
function checkInviteStorage() {
try {
var dismissed = sessionStorage.getItem(inviteStorageKey);
if (dismissed === "permanent") {
isPermanentlyClosed = true;
return true;
}
return false;
} catch (e) {
return false; // Assume not dismissed on error
}
}
function setInviteDismissed(type) {
try {
if (type !== "permanent" && type !== "temporary") {
type = "temporary"; // Default fallback
}
if (type === "permanent") {
sessionStorage.setItem(inviteStorageKey, "permanent");
isPermanentlyClosed = true;
} else {
sessionStorage.setItem(inviteStorageKey, "temporary");
}
} catch (e) {
// Silently fail - storage unavailable
}
}
function clearInviteDismissed() {
try {
sessionStorage.removeItem(inviteStorageKey);
isPermanentlyClosed = false;
inviteShown = false; // Reset shown flag
} catch (e) {
// Silently fail - storage unavailable
}
}
function startInviteTimer() {
if (!autoInvite) return; // 配置关闭则不启动计时器,邀请卡永不自动弹
if (checkInviteStorage()) {
return; // Already dismissed, don't show
}
inviteTimer = window.setTimeout(function() {
if (!isOpen) {
showInvite();
}
}, 5000); // 5 seconds
}
function showInvite() {
if (inviteShown || isPermanentlyClosed) return;
inviteShown = true;
invitePanel = shadow.querySelector(".invite-panel");
if (invitePanel) {
invitePanel.classList.remove("hide");
void invitePanel.offsetWidth; // 强制回流:remove 与 add 同帧会跳过 0.3s 淡入过渡
invitePanel.classList.add("show");
}
}
function hideInvite(type) {
if (!invitePanel) return;
invitePanel.classList.remove("show");
invitePanel.classList.add("hide");
setInviteDismissed(type || "temporary");
}
var msgsEl = shadow.getElementById("w-msgs");
var inputEl = shadow.getElementById("w-input");
var sendEl = shadow.getElementById("w-send");
var typingEl = shadow.getElementById("w-typing");
var endedEl = shadow.getElementById("w-ended");
var inviteCloseBtn = shadow.querySelector(".invite-close-btn");
var inviteStartBtn = shadow.querySelector(".invite-start-btn");
var inviteDismissBtn = shadow.querySelector(".invite-dismiss-btn");
var inviteTopicBtns = shadow.querySelectorAll(".invite-topic-btn");
if (isOpen) panel.classList.add("open");
function toggle() {
isOpen = !isOpen;
panel.classList.toggle("open", isOpen);
if (isOpen) {
if (inviteTimer) {
window.clearTimeout(inviteTimer);
inviteTimer = null;
}
hideInvite("temporary");
if (!wWs) wConnect();
} else {
// Only restart timer if not permanently closed
if (!isPermanentlyClosed && !checkInviteStorage()) {
inviteShown = false;
startInviteTimer();
}
}
}
toggleBtn.addEventListener("click", toggle);
// panel.querySelector(".close-btn").addEventListener("click", toggle);
panel.querySelector(".action-btn:not(.close-btn)")?.addEventListener("click", toggle);
// Invite close button
// 2026-08-20 用户决策: 点 X 与点 No Thanks 同效(permanent)——本标签页会话+本页面
// 路径内不再自动弹(含关面板后/刷新后);temporary 写了从未被 checkInviteStorage
// 消费,等于无抑制,原行为即"关面板5s后又弹"。
inviteCloseBtn.addEventListener("click", function() {
hideInvite("permanent");
});
// Invite Start Chat button
inviteStartBtn.addEventListener("click", function() {
hideInvite("temporary");
if (!isOpen) {
toggle();
}
});
// Invite No Thanks button
inviteDismissBtn.addEventListener("click", function() {
hideInvite("permanent");
});
// Invite topic buttons - auto send message
Array.prototype.forEach.call(inviteTopicBtns, function(btn) {
btn.addEventListener("click", function() {
var topic = this.getAttribute("data-topic");
hideInvite("temporary");
if (!isOpen) {
toggle();
}
// Queue message to send after WebSocket opens
pendingMessage = topic;
});
});
// Popular topics buttons
var popularTopicBtns = shadow.querySelectorAll(".popular-topic-btn");
Array.prototype.forEach.call(popularTopicBtns, function(btn) {
btn.addEventListener("click", function() {
var topic = this.getAttribute("data-topic");
// Hide popular topics after first interaction
var popularTopicsEl = shadow.getElementById("w-popular-topics");
if (popularTopicsEl) {
popularTopicsEl.style.display = "none";
}
// Send message
autoSendMessage(topic);
});
});
function wConnect() {
var proto = wsBase.indexOf("https") === 0 ? "wss" : "ws";
var wsHost = wsBase.replace(/^https?/, proto);
wWs = new WebSocket(wsHost + "/ws/chat");
wWs.onopen = function () {
if (wSessionId) {
wWs.send(JSON.stringify({ session_id: wSessionId, page_token: pageToken }));
} else {
wWs.send(JSON.stringify({ source: "widget", page_token: pageToken }));
}
// Send any pending message after WebSocket opens
if (pendingMessage) {
wWs.send(JSON.stringify({ content: pendingMessage }));
addMsg("user", pendingMessage);
typingEl.classList.add("active");
pendingMessage = null;
}
};
wWs.onmessage = function (e) {
var data = JSON.parse(e.data);
if (data.type === "init") {
wSessionId = data.session_id;
}
if (data.type === "stream_start") {
typingEl.classList.remove("active");
streamBubble = addStreamBubble();
}
if (data.type === "stream_chunk") {
if (streamBubble) {
streamBubble.textContent += data.content;
msgsEl.scrollTop = msgsEl.scrollHeight;
}
}
if (data.type === "stream_end") {
streamBubble = null;
}
if (data.type === "evaluated") {
if (data.ended) {
wEnded = true;
endedEl.classList.add("active");
inputEl.disabled = true;
sendEl.disabled = true;
}
}
if (data.type === "error") {
typingEl.classList.remove("active");
streamBubble = null;
addMsg("ai", "Sorry, something went wrong.");
}
};
wWs.onclose = function () {
if (!wEnded && host.isConnected) setTimeout(wConnect, 3000);
};
}
function addMsg(role, text) {
var div = document.createElement("div");
div.className = "msg " + role;
div.textContent = text;
msgsEl.appendChild(div);
msgsEl.scrollTop = msgsEl.scrollHeight;
}
function addStreamBubble() {
var div = document.createElement("div");
div.className = "msg ai";
div.textContent = "";
msgsEl.appendChild(div);
msgsEl.scrollTop = msgsEl.scrollHeight;
return div;
}
function autoSendMessage(text) {
if (!text || !wWs || wWs.readyState !== WebSocket.OPEN) return;
addMsg("user", text);
typingEl.classList.add("active");
wWs.send(JSON.stringify({ content: text }));
}
function wSend() {
var text = inputEl.value.trim();
if (!text || wEnded || !wWs || wWs.readyState !== WebSocket.OPEN) return;
addMsg("user", text);
inputEl.value = "";
typingEl.classList.add("active");
wWs.send(JSON.stringify({ content: text }));
}
sendEl.addEventListener("click", wSend);
inputEl.addEventListener("keydown", function (e) {
if (e.key === "Enter") { e.preventDefault(); wSend(); }
});
// Start invite timer
startInviteTimer();
}
// Wait for body if script is in <head>
if (document.body) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
})();