feat(widget): Widget 配置、loader 与右下角悬浮入口

生成可嵌入站点的 loader 与悬浮按钮,支持展开收起和移动端;附独立宿主测试页。
This commit is contained in:
WangShen007 committed 2026-09-25 20:50:00 +08:00
1 parent e24d3e2a65
commit 5d0a48505a
57 files changed
+32086

No files matched your search

+888
View File
@@ -0,0 +1,888 @@
(()=>{var wt=(at,ot,e)=>new Promise((M,d)=>{var j=x=>{try{B(e.next(x))}catch(N){d(N)}},V=x=>{try{B(e.throw(x))}catch(N){d(N)}},B=x=>x.done?M(x.value):Promise.resolve(x.value).then(j,V);B((e=e.apply(at,ot)).next())});window.chattermateId;window.chattermateBaseUrl;window.ChatterMate;window.chattermateConfig;(function(){function at(t){return/^#[0-9A-F]{6}$/i.test(t)}function ot(){return typeof window!="undefined"&&window.chattermateBaseUrl?window.chattermateBaseUrl:typeof window!="undefined"&&window.APP_CONFIG&&window.APP_CONFIG.API_URL?window.APP_CONFIG.API_URL:"https://api.chattermate.chat/api/v1"}let e={baseUrl:ot(),containerId:"chattermate-container",backdropId:"chattermate-backdrop",buttonId:"chattermate-button",chatBubbleColor:"#f34611",loadingContainerId:"chattermate-loading",tokenKey:"ctid",tokenProvider:null,containerBottom:100,containerRight:20,containerWidth:400,containerHeight:560,launcherBottom:20,launcherRight:20,side:"right",displayMode:"floating",launcher:!0,sidebarWidth:420,searchBarWidth:320,searchPlaceholder:"Ask anything...",zIndex:999999,trigger:null,hotkey:!0,surfaceIsPalette:null,chatStyle:null,chatInitiationMessages:[],initiationMessageId:"chattermate-initiation",initiationShownKey:"ctim_shown",unreadCount:0,hasOpened:!1},M={};function d(t,n){e[t]=n,M[t]=!0}let j=["floating","sidebar-left","sidebar-right","search-bar"],V=["ASK_ANYTHING","AURORA"],B=680,x=620,N=140,S={width:!1,height:!1},P=0;function q(){return e.surfaceIsPalette===!1?!1:(V.indexOf(e.chatStyle)!==-1||e.displayMode==="search-bar")&&e.displayMode!=="sidebar-left"&&e.displayMode!=="sidebar-right"}function Z(){let t=document.getElementById(e.containerId);if(!t)return;if(!q()||!P){t.style.height="";return}let n=Math.min(rt().height,Math.round(window.innerHeight*.76));t.style.height=Math.max(N,Math.min(n,P))+"px"}function rt(){return{width:S.width?e.containerWidth:B,height:S.height?e.containerHeight:x}}function _t(t){if(!t||typeof t!="object")return;let n=function(i,o){M[i]||(e[i]=o)};j.indexOf(t.mode)!==-1&&n("displayMode",t.mode),(t.side==="left"||t.side==="right")&&n("side",t.side),typeof t.launcher=="boolean"&&n("launcher",t.launcher),m(t.width)&&(n("containerWidth",Math.max(280,t.width)),M.containerWidth||(S.width=!0)),m(t.height)&&(n("containerHeight",Math.max(400,t.height)),M.containerHeight||(S.height=!0)),m(t.sidebar_width)&&n("sidebarWidth",Math.max(280,t.sidebar_width)),typeof t.search_placeholder=="string"&&t.search_placeholder&&n("searchPlaceholder",t.search_placeholder.slice(0,80)),m(t.offset_bottom)&&n("launcherBottom",Math.max(0,t.offset_bottom)),m(t.offset_side)&&n("launcherRight",Math.max(0,t.offset_side)),m(t.z_index)&&n("zIndex",Math.max(1,Math.floor(t.z_index)))}function It(){let t=q(),n=t?rt():{width:e.containerWidth,height:e.containerHeight};return{type:"WIDGET_DISPLAY",mode:t?"ask-ai":e.displayMode,width:n.width,height:n.height,hotkey:e.hotkey!==!1}}let c=null,R=[],X=!1,g=null,T=null,y=!1;function J(t){c?t():y||R.push(t)}let Q={},st=!1;function _(t,n){let i=Q[t];!i||!i.length||i.slice().forEach(function(o){try{o(n)}catch(h){}})}function Lt(t){let n=/^#?([0-9a-f]{6})$/i.exec((t||"").trim());if(!n)return"#0B0C10";let i=parseInt(n[1],16);return(.299*(i>>16&255)+.587*(i>>8&255)+.114*(i&255))/255>.6?"#0B0C10":"#FFFFFF"}let ct=!1,E=null,lt=[];function F(){if(ct)return;ct=!0,E&&(clearTimeout(E),E=null);let t=document.getElementById(e.buttonId);t&&t.classList.remove("chattermate-pending"),lt.splice(0).forEach(n=>{try{n()}catch(i){}})}function O(){let t=document.getElementById(e.buttonId);if(!t)return;let n=t.querySelector(".cm-badge");if(!n)return;let i=e.hasOpened?e.unreadCount:(e.chatInitiationMessages||[]).length;n.textContent=i>99?"99+":String(i),n.style.display=i>0&&!t.classList.contains("active")?"flex":"none"}function W(){let t=e.displayMode,n=t==="sidebar-left"||t==="sidebar-right",i=t==="sidebar-left"?"left":t==="sidebar-right"?"right":e.side,o=t==="search-bar"?48:64,h=V.indexOf(e.chatStyle)===-1,r=q(),l=rt(),w=document.createElement("style");w.id="chattermate-styles",w.textContent=`
/* ===== Rounded-square launcher (design comp) ===== */
#${e.buttonId} {
position: fixed;
bottom: ${e.launcherBottom}px;
${i}: ${e.launcherRight}px;
width: 64px;
height: 64px;
border-radius: 20px 20px 20px 6px;
background: ${e.chatBubbleColor};
color: ${Lt(e.chatBubbleColor)};
box-shadow: 0 16px 40px -8px ${e.chatBubbleColor}cc, inset 0 0 0 1px rgba(255,255,255,0.08);
cursor: pointer;
z-index: ${e.zIndex};
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s cubic-bezier(.34,1.3,.5,1), opacity 320ms ease;
animation: chattermate-float 4s ease-in-out infinite;
}
#${e.buttonId}.active { animation: none; }
/* Stay fully invisible until the org's real brand color is known (or the
widget errors out / times out) \u2014 avoids ever flashing the wrong color. */
#${e.buttonId}.chattermate-pending {
opacity: 0;
pointer-events: none;
}
#${e.buttonId}:hover { transform: scale(1.06); }
/* Expanding rings (closed state only) */
#${e.buttonId} .cm-ring {
position: absolute;
inset: 0;
border-radius: 20px 20px 20px 6px;
border: 1.5px solid ${e.chatBubbleColor};
animation: chattermate-ring 2.4s ease-out infinite;
pointer-events: none;
}
#${e.buttonId} .cm-ring.r2 { animation-delay: 1.2s; }
#${e.buttonId}.active .cm-ring { display: none; }
/* Pulsing 3-dot mark (closed). Absolutely centered so it stays pinned to the
button's center regardless of the button's display mode (flex on desktop,
block when .mobile-closed) or viewport. */
#${e.buttonId} .cm-dots {
display: flex;
gap: 5px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#${e.buttonId} .cm-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
animation: chattermate-pulse 1.5s ease-in-out infinite;
}
#${e.buttonId} .cm-dot:nth-child(2) { animation-delay: .18s; }
#${e.buttonId} .cm-dot:nth-child(3) { animation-delay: .36s; }
#${e.buttonId}.active .cm-dots { display: none; }
/* Chevron (open) \u2014 absolutely centered, pinned to the button center. */
#${e.buttonId} .cm-chevron {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 26px;
font-weight: 700;
line-height: 1;
color: currentColor;
}
#${e.buttonId}.active .cm-chevron { display: block; }
/* Unread badge = number of nudges */
#${e.buttonId} .cm-badge {
position: absolute;
top: -5px;
right: -5px;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
background: #FF8A73;
color: #0B0C10;
font: 700 11.5px/1 -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
align-items: center;
justify-content: center;
border: 2px solid #F5F3EE;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
display: none;
}
#${e.buttonId}.active .cm-badge { display: none; }
#${e.buttonId}.loading .cm-dots,
#${e.buttonId}.loading .cm-chevron { visibility: hidden; }
#${e.buttonId}.loading:after {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin: -8px 0 0 -8px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #fff;
animation: chattermate-spin 0.6s linear infinite;
}
/* Search-bar trigger content: hidden unless search-bar mode (below) shows it. */
#${e.buttonId} .cm-search { display: none; }
${t==="search-bar"?`
@media (min-width: 769px) {
/* ===== Search-bar mode: the launcher becomes a search-input-like bar.
Same element, same click/toggle/badge logic. Mobile keeps the bubble. ===== */
#${e.buttonId} {
width: ${e.searchBarWidth}px;
height: 48px;
border-radius: 24px;
padding: 0 16px;
background: #ffffff;
color: #374151;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14), inset 0 0 0 1.5px ${e.chatBubbleColor}55;
animation: none;
justify-content: flex-start;
box-sizing: border-box;
}
#${e.buttonId}:hover { transform: scale(1.02); }
#${e.buttonId} .cm-ring,
#${e.buttonId} .cm-dots,
#${e.buttonId} .cm-chevron,
#${e.buttonId}.active .cm-chevron { display: none; }
/* Dark spinner \u2014 the stock white one vanishes on the white bar. */
#${e.buttonId}.loading .cm-search { visibility: hidden; }
#${e.buttonId}.loading:after {
border-color: rgba(0, 0, 0, 0.15);
border-top-color: ${e.chatBubbleColor};
}
#${e.buttonId} .cm-search {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
width: 100%;
}
#${e.buttonId} .cm-search svg {
flex-shrink: 0;
color: ${e.chatBubbleColor};
}
#${e.buttonId} .cm-search-placeholder {
font: 400 14px/1.2 -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
color: #6b7280;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
`:""}
@keyframes chattermate-spin { to { transform: rotate(360deg); } }
@keyframes chattermate-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
@keyframes chattermate-ring { 0% { transform: scale(.85); opacity: .5; } 100% { transform: scale(1.6); opacity: 0; } }
@keyframes chattermate-pulse { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
#${e.buttonId},
#${e.buttonId} .cm-ring,
#${e.buttonId} .cm-dot,
.initiation-orb { animation: none !important; }
/* Open and close in place: no slide, scale or fade for people who asked for less motion. */
#${e.buttonId},
#${e.containerId},
#${e.backdropId} { transition: none !important; }
}
#${e.containerId} {
position: fixed;
/* The window sits above the launcher; when the launcher is moved (via
ChatterMate.init position / setPosition) the window shifts by the same
delta so the two stay together. Mobile is full-screen (media query below)
and is unaffected. */
bottom: ${(e.containerBottom||100)+((e.launcherBottom||20)-20)}px;
${i}: ${(e.containerRight||20)+((e.launcherRight||20)-20)}px;
width: ${e.containerWidth||384}px;
max-width: calc(100vw - 48px);
height: ${e.containerHeight}px;
max-height: calc(100vh - 132px);
background: transparent;
z-index: ${e.zIndex};
overflow: hidden;
/* Animated open/close with slide + fade */
opacity: 0;
visibility: hidden;
transform: translateY(16px) scale(0.98);
transition: opacity 360ms cubic-bezier(0.22, 1, 0.36, 1), transform 360ms cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 360ms;
border: none;
padding: 0;
margin: 0;
pointer-events: none;
}
#${e.containerId}.active {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
transition: opacity 420ms cubic-bezier(0.22, 1, 0.36, 1), transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
pointer-events: auto;
}
/* Backdrop element: always in the DOM, so keep it out of the host page's flow
even when it is not in use \u2014 an unstyled div in <body> adds a phantom row to
a flex/grid body layout. */
#${e.backdropId} {
position: fixed;
inset: 0;
display: none;
}
${r?`
/* ===== Ask AI: centered command palette (\u2318K), the pattern people expect from
an "Ask anything" surface \u2014 dimmed page, top-anchored so the eye lands on
the input rather than the middle of the screen. ===== */
#${e.containerId} {
top: 12vh;
bottom: auto;
left: 50%;
right: auto;
width: min(${l.width}px, calc(100vw - 32px));
max-width: none;
/* A cross-document iframe can't size its embedder, so this is a starting
height only: the palette measures its content and posts WIDGET_RESIZE,
which sets an inline height (clamped to this ceiling). height:100% against
an auto-height parent would collapse to the 150px replaced default. */
height: min(${l.height}px, 76vh);
max-height: 76vh;
transform: translate(-50%, -8px) scale(0.98);
z-index: ${e.zIndex+1};
transition: opacity 360ms cubic-bezier(0.22, 1, 0.36, 1),
transform 360ms cubic-bezier(0.22, 1, 0.36, 1),
height 200ms cubic-bezier(0.22, 1, 0.36, 1),
visibility 0s linear 360ms;
}
#${e.containerId}.active {
transform: translate(-50%, 0) scale(1);
transition: opacity 420ms cubic-bezier(0.22, 1, 0.36, 1),
transform 420ms cubic-bezier(0.22, 1, 0.36, 1),
height 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
#${e.containerId} .chattermate-iframe {
border-radius: 16px;
}
#${e.backdropId} {
display: block;
background: rgba(9, 9, 14, 0.45);
backdrop-filter: blur(2px);
z-index: ${e.zIndex};
opacity: 0;
visibility: hidden;
transition: opacity 220ms ease, visibility 0s linear 220ms;
}
#${e.backdropId}.active {
opacity: 1;
visibility: visible;
transition: opacity 220ms ease;
}
/* The palette dims the page and closes on backdrop click / Esc, so the launcher
would just be a stray button floating over the dim. */
#${e.buttonId}.active {
display: none;
}
@media (max-width: 768px) {
/* Full-screen on phones like every other mode. The main mobile block below
pins the edges but never resets transform \u2014 without this the centering
translate would shove the panel half a screen to the left. */
#${e.containerId},
#${e.containerId}.active { transform: none !important; }
#${e.backdropId} { display: none; }
}
`:""}
${n?`
/* ===== Sidebar mode: full-height drawer hugging the ${i} edge ===== */
#${e.containerId} {
top: 0;
bottom: auto;
${i}: 0;
${i==="left"?"right: auto;":"left: auto;"}
width: ${e.sidebarWidth}px;
max-width: 100vw;
height: 100dvh;
max-height: 100dvh;
transform: translateX(${i==="left"?"-100%":"100%"});
box-shadow: ${i==="left"?"":"-"}12px 0 40px rgba(0, 0, 0, 0.12);
}
#${e.containerId}.active {
transform: translateX(0);
}
#${e.containerId} .chattermate-iframe {
border-radius: 0;
}
${h?`
/* The drawer's own header chevron closes it, so keeping the launcher on screen
would just park a duplicate button beside the panel. Hide it while open. */
#${e.buttonId}.active {
display: none;
}
`:`
/* ASK_ANYTHING / AURORA draw no in-panel chevron, and the drawer would paint over
the launcher (later sibling, equal z-index) \u2014 leaving nothing to click. Keep it
reachable: slide it out beside the drawer edge, one layer up. */
#${e.buttonId} {
z-index: ${e.zIndex+1};
transition: transform 0.3s cubic-bezier(.34,1.3,.5,1), opacity 320ms ease,
left 360ms cubic-bezier(0.22, 1, 0.36, 1), right 360ms cubic-bezier(0.22, 1, 0.36, 1);
}
#${e.buttonId}.active {
${i}: ${e.sidebarWidth+20}px;
}
`}
`:""}
/* Clean border around the widget container */
/* The widget panel (inside the iframe) draws its own theme-aware border, so the
container no longer adds one \u2014 avoids a double border (and a white line on dark themes). */
.chattermate-iframe {
width: 100%;
height: 100%;
border: none;
padding: 0;
margin: 0;
display: block;
border-radius: 24px;
/* Rely on corner shadows from container */
box-shadow: none;
}
#chattermate-mobile-close {
display: none;
position: fixed;
top: 20px;
right: 20px;
width: 44px;
height: 44px;
background: transparent;
border: none;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: ${e.zIndex+1};
transition: all 0.3s ease;
}
#chattermate-mobile-topbar {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
z-index: ${e.zIndex};
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
}
#chattermate-mobile-topbar.active {
display: flex;
}
#chattermate-mobile-topbar .topbar-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
/* Only ASK_ANYTHING (which has no in-panel header chevron) uses the floating
mobile-close; other styles close via the header chevron, so don't double up. */
.ask-anything-mobile #chattermate-mobile-close.active {
display: flex;
}
@media (max-width: 768px) {
#${e.containerId} {
width: 100vw !important;
height: 100vh !important;
height: 100dvh !important; /* Dynamic viewport height for mobile browsers */
/* Reset the desktop max-width/height clamp \u2014 without this the desktop rule's
max-width: calc(100vw - 48px) / max-height: calc(100vh - 132px) keeps the
window a floating panel instead of going full-screen on mobile. */
max-width: 100vw !important;
max-height: 100vh !important;
max-height: 100dvh !important;
top: 0 !important;
left: 0 !important;
bottom: 0 !important;
right: 0 !important;
border-radius: 0 !important;
position: fixed !important;
margin: 0 !important;
padding: 0 !important;
}
#${e.containerId}.active {
bottom: 0 !important;
}
.chattermate-iframe {
border-radius: 0 !important;
width: 100vw !important;
height: 100vh !important;
height: 100dvh !important; /* Dynamic viewport height for mobile browsers */
position: absolute !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
#${e.buttonId} {
display: none;
}
#${e.buttonId}.mobile-closed {
/* flex (not block) keeps the launcher's centering context; the dots/chevron
are absolutely centered regardless, but this keeps it consistent. */
display: flex !important;
width: 56px !important;
height: 56px !important;
/* Respect the configured position on mobile too, so the closed launcher can
clear a fixed bottom nav bar (the main reason to move it up). */
bottom: ${e.launcherBottom}px !important;
${i}: ${e.launcherRight}px !important;
}
.ask-anything-mobile #chattermate-mobile-close.active {
display: flex !important;
}
#chattermate-mobile-close:hover {
opacity: 0.7;
}
/* ASK_ANYTHING style specific mobile topbar */
.ask-anything-mobile #chattermate-mobile-topbar.active {
display: flex !important;
}
.ask-anything-mobile #chattermate-mobile-close.active {
top: 15px !important;
right: 15px !important;
z-index: ${e.zIndex+2} !important;
}
/* When topbar is visible, push iframe down to avoid overlap */
.ask-anything-mobile .chattermate-iframe {
top: 60px !important;
height: calc(100vh - 60px) !important;
height: calc(100dvh - 60px) !important;
}
}
@media (min-width: 769px) {
#chattermate-mobile-close {
display: none !important;
}
}
/* Chat Initiation Message Styles */
#${e.initiationMessageId} {
position: fixed !important;
/* Sit above the launcher (+12px gap) and align to its edge on the active side,
so it never overlaps the icon regardless of the configured position/mode. */
bottom: ${(e.launcherBottom||20)+o+12}px !important;
${i}: ${e.launcherRight||20}px !important;
max-width: 260px !important;
background: white !important;
padding: 12px 36px 12px 14px !important;
border-radius: 14px !important;
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.1) !important;
z-index: ${e.zIndex-1} !important;
cursor: pointer !important;
opacity: 0;
visibility: hidden;
transform: translateY(10px) scale(0.95);
transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1) !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
font-size: 14px !important;
box-sizing: border-box !important;
margin: 0 !important;
border: none !important;
display: block !important;
width: auto !important;
height: auto !important;
min-width: 0 !important;
min-height: 0 !important;
max-height: none !important;
line-height: 1.4 !important;
text-align: left !important;
vertical-align: baseline !important;
overflow: visible !important;
}
/* Clamp the nudge text to 4 lines with an ellipsis so a long welcome/initiation
message can never balloon the bubble over the page or into the launcher.
Applied to the text node only, so the orb (left:-34px) is never clipped. */
#${e.initiationMessageId} .initiation-message-text {
margin: 0 !important;
display: -webkit-box !important;
-webkit-line-clamp: 4 !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
}
/* Siri-style orb avatar beside the nudge (design comp) */
.initiation-orb {
position: absolute !important;
left: -34px !important;
bottom: 2px !important;
width: 26px !important;
height: 26px !important;
border-radius: 50% !important;
background: conic-gradient(from 0deg, #C9F24E, #9D8CFF, #5FE3D6, #FF8A73, #C9F24E) !important;
box-shadow: 0 4px 14px rgba(157, 140, 255, 0.4) !important;
animation: chattermate-orb-spin 6s linear infinite !important;
z-index: 1 !important;
}
@keyframes chattermate-orb-spin { to { transform: rotate(360deg); } }
${i==="left"?`
/* On the left edge the orb (which protrudes 34px past the bubble) would clip
off-screen \u2014 mirror it to the bubble's page-facing side. */
.initiation-orb {
left: auto !important;
right: -34px !important;
}
`:""}
#${e.initiationMessageId}.show {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
animation: chattermate-bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
@keyframes chattermate-bounce-in {
0% {
opacity: 0;
transform: translateY(20px) scale(0.8);
}
50% {
transform: translateY(-5px) scale(1.02);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
#${e.initiationMessageId}:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.14);
}
#${e.initiationMessageId}::after {
content: '' !important;
position: absolute !important;
bottom: -7px !important;
${i}: 30px !important;
width: 14px !important;
height: 14px !important;
min-width: 14px !important;
min-height: 14px !important;
max-width: 14px !important;
max-height: 14px !important;
background: white !important;
transform: rotate(45deg) !important;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.06) !important;
clip-path: polygon(0 0, 100% 0, 100% 100%) !important;
border: none !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
z-index: -1 !important;
}
.initiation-message-text {
font-size: 13px !important;
line-height: 1.4 !important;
color: #374151 !important;
margin: 0 !important;
padding: 0 4px 0 0 !important;
position: relative !important;
z-index: 1 !important;
min-height: 18px !important;
box-sizing: border-box !important;
border: none !important;
background: transparent !important;
display: block !important;
width: 100% !important;
height: auto !important;
max-width: none !important;
max-height: none !important;
text-align: left !important;
vertical-align: baseline !important;
font-weight: normal !important;
font-style: normal !important;
text-decoration: none !important;
text-transform: none !important;
letter-spacing: normal !important;
word-spacing: normal !important;
white-space: normal !important;
overflow-wrap: break-word !important;
word-wrap: break-word !important;
}
.initiation-message-text::after {
content: '|';
animation: blink 1s step-end infinite;
margin-left: 2px;
color: ${e.chatBubbleColor};
font-weight: 500;
}
.initiation-message-text.typing-complete::after {
display: none;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.initiation-close {
position: absolute !important;
top: 10px !important;
right: 10px !important;
width: 20px !important;
height: 20px !important;
min-width: 20px !important;
min-height: 20px !important;
max-width: 20px !important;
max-height: 20px !important;
background: rgba(0, 0, 0, 0.04) !important;
border: none !important;
border-radius: 5px !important;
cursor: pointer !important;
opacity: 0.5 !important;
transition: all 0.2s ease !important;
z-index: 2 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box !important;
outline: none !important;
box-shadow: none !important;
text-decoration: none !important;
font-size: 0 !important;
line-height: 0 !important;
overflow: hidden !important;
flex-shrink: 0 !important;
}
.initiation-close:hover {
opacity: 1 !important;
background: rgba(0, 0, 0, 0.08) !important;
transform: scale(1.05) !important;
}
.initiation-close::before,
.initiation-close::after {
content: '' !important;
position: absolute !important;
width: 9px !important;
height: 1.5px !important;
min-width: 9px !important;
min-height: 1.5px !important;
max-width: 9px !important;
max-height: 1.5px !important;
background: #4a5568 !important;
border-radius: 1px !important;
top: 50% !important;
left: 50% !important;
margin: -0.75px 0 0 -4.5px !important;
padding: 0 !important;
border: none !important;
box-sizing: border-box !important;
}
.initiation-close::before {
transform: rotate(45deg) !important;
}
.initiation-close::after {
transform: rotate(-45deg) !important;
}
@media (max-width: 768px) {
#${e.initiationMessageId} {
display: none !important;
}
}
/* Error UI Styles */
.chattermate-error-ui {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 24px;
overflow: hidden;
position: relative;
}
.chattermate-error-ui::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 30%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(168, 85, 247, 0.06) 0%, transparent 50%);
pointer-events: none;
}
.chattermate-error-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 24px;
text-align: center;
position: relative;
z-index: 1;
max-width: 90%;
}
.chattermate-error-close {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: rgba(120, 120, 140, 0.12);
color: #4b5563;
font-size: 22px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease;
}
.chattermate-error-close:hover {
background: rgba(120, 120, 140, 0.22);
}
.chattermate-error-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 72px;
height: 72px;
background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%);
border-radius: 20px;
margin-bottom: 20px;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15);
}
.chattermate-error-icon {
width: 36px;
height: 36px;
color: #6366f1;
}
.chattermate-error-title {
font-size: 20px;
font-weight: 600;
color: #1e293b;
margin: 0 0 12px 0;
letter-spacing: -0.01em;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.chattermate-error-message {
font-size: 14px;
line-height: 1.6;
color: #64748b;
margin: 0 0 24px 0;
max-width: 280px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.chattermate-error-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 12px;
color: #94a3b8;
opacity: 0.8;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.chattermate-error-footer-link {
color: inherit;
text-decoration: none;
cursor: pointer;
}
.chattermate-error-footer-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.chattermate-error-ui {
border-radius: 0;
}
.chattermate-error-card {
padding: 24px 20px;
}
.chattermate-error-icon-wrapper {
width: 64px;
height: 64px;
border-radius: 16px;
}
.chattermate-error-icon {
width: 32px;
height: 32px;
}
.chattermate-error-title {
font-size: 18px;
}
.chattermate-error-message {
font-size: 13px;
max-width: 260px;
}
}
${e.launcher?"":`
/* Launcher hidden by config \u2014 the site opens the chat with its own trigger or
the JS API. Last in the sheet so it also beats the mobile .mobile-closed
re-show (display: flex !important at equal specificity). */
#${e.buttonId}, #${e.buttonId}.mobile-closed { display: none !important; }
`}
`;let f=document.getElementById("chattermate-styles");f&&f.remove(),document.head.appendChild(w),vt()}function vt(){let t=document.getElementById(e.buttonId);if(!t)return;let n=t.querySelector(".cm-search-placeholder");n&&n.textContent!==e.searchPlaceholder&&(n.textContent=e.searchPlaceholder)}function C(t){if(typeof t!="string")return null;let n=t.trim();return!n||n==="undefined"||n==="null"?null:n}function dt(){return C(window.chattermateToken)||C(localStorage.getItem(e.tokenKey))}function ht(t){let n=C(t);if(!n)return null;let i=n.split(".")[1];if(!i)return null;try{let o=JSON.parse(atob(i.replace(/-/g,"+").replace(/_/g,"/")));return o.sub||o.customer_id||null}catch(o){return null}}function tt(t){t&&localStorage.setItem(e.tokenKey,t)}function H(){localStorage.removeItem(e.tokenKey)}function U(){return window.innerWidth<=768}function zt(t){return t?t.replace(/\uFFFD/g,"").replace(/[��]/g,"").replace(/[\x00-\x1F\x7F-\x9F]/g,"").trim():""}function Bt(){if(!e.chatInitiationMessages||e.chatInitiationMessages.length===0)return null;let t=Math.floor(Math.random()*e.chatInitiationMessages.length),n=e.chatInitiationMessages[t];return n?zt(n):null}function Nt(){return U()||!e.launcher||!e.chatInitiationMessages||e.chatInitiationMessages.length===0?!1:!sessionStorage.getItem(e.initiationShownKey)}function pt(){sessionStorage.setItem(e.initiationShownKey,"true")}function Pt(){let t=Bt();if(!t)return null;let n=document.createElement("div");return n.id=e.initiationMessageId,n.innerHTML=`
<span class="initiation-orb"></span>
<button class="initiation-close" aria-label="Close"></button>
<p class="initiation-message-text"></p>
`,n.dataset.fullMessage=t,n}function Rt(t,n,i=50){let o=t.querySelector(".initiation-message-text");if(!o)return;let h=0,r=setInterval(()=>{h<n.length?(o.textContent+=n.charAt(h),h++):(clearInterval(r),o.classList.add("typing-complete"),t.typeInterval=null)},i);t.typeInterval=r}function Ft(t,n){if(!t||!Nt())return;document.body.appendChild(t),setTimeout(()=>{t.classList.add("show"),setTimeout(()=>{let o=t.dataset.fullMessage;o&&Rt(t,o,40)},300)},1500),t.addEventListener("click",o=>{o.target.classList.contains("initiation-close")||(t.typeInterval&&clearInterval(t.typeInterval),mt(t),n(),pt())});let i=t.querySelector(".initiation-close");i&&i.addEventListener("click",o=>{o.stopPropagation(),t.typeInterval&&clearInterval(t.typeInterval),mt(t),pt()})}function mt(t){t&&(t.classList.remove("show"),setTimeout(()=>{t.parentNode&&t.parentNode.removeChild(t)},400))}function ut(){y=!1,W();let t=document.createElement("div");t.id=e.buttonId,t.classList.add("chattermate-pending"),t.innerHTML=`
<span class="cm-ring"></span>
<span class="cm-ring r2"></span>
<div class="cm-dots"><span class="cm-dot"></span><span class="cm-dot"></span><span class="cm-dot"></span></div>
<span class="cm-chevron">&#8964;</span>
<span class="cm-search">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-4.35-4.35"></path></svg>
<span class="cm-search-placeholder"></span>
</span>
<span class="cm-badge"></span>
`,E=setTimeout(F,8e3);let n=document.createElement("div");n.id=e.containerId;let i=document.createElement("div");i.id="chattermate-mobile-close",i.innerHTML=`
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;let o=document.createElement("div");o.id="chattermate-mobile-topbar",o.innerHTML=`
<h3 class="topbar-title">Chat</h3>
<div style="width: 44px;"></div>
`;let h=document.createElement("div");h.id=e.backdropId,document.body.appendChild(t),document.body.appendChild(h),document.body.appendChild(n),document.body.appendChild(i),document.body.appendChild(o),O(),vt();let r=!1,l=null,w=!1,f=null;function b(){r=!r,n.classList.toggle("active"),h.classList.toggle("active",r),t.classList.toggle("active"),i.classList.toggle("active"),r&&(e.hasOpened=!0,e.unreadCount=0),l&&l.contentWindow&&l.contentWindow.postMessage({type:"WIDGET_VISIBILITY",open:r},"*"),O(),r&&f&&(mt(f),f=null,pt()),U()?r?(t.classList.remove("mobile-closed"),t.style.animation=""):(t.classList.add("mobile-closed"),t.style.animation="chattermate-float 4s ease-in-out infinite",o.classList.remove("active"),document.body.classList.remove("ask-anything-mobile")):r&&i.classList.remove("active"),r&&l&&l.contentWindow.postMessage({type:"SCROLL_TO_BOTTOM"},"*"),_(r?"open":"close")}let K=null,bt=!1;function Yt(a){let s=String(a||"").slice(0,2e3);s&&(bt&&l&&l.contentWindow?l.contentWindow.postMessage({type:"PREFILL_MESSAGE",text:s},"*"):K=s)}let D=0;function Ct(){for(D++;n.firstChild;)n.removeChild(n.firstChild);l=null,g=null,T=null,bt=!1,st=!1,K=null,w=!1,t.classList.remove("loading"),P=0,Z()}function Gt(){r&&b(),Ct(),e.unreadCount=0,e.hasOpened=!1,O(),et()}function jt(){r&&b(),Ct(),window.removeEventListener("resize",Tt),window.removeEventListener("message",St),f&&(f.typeInterval&&clearInterval(f.typeInterval),ft(f),f=null),[t,h,n,i,o].forEach(ft),ft(document.getElementById("chattermate-styles")),document.body.classList.remove("ask-anything-mobile"),ct=!1,E&&(clearTimeout(E),E=null),lt.length=0,e.unreadCount=0,e.hasOpened=!1,c=null,R.length=0,X=!1,y=!0}function At(a){let s=document.createElement("div");s.className="chattermate-error-ui";let I=document.createElement("div");I.className="chattermate-error-card";let v=document.createElement("button");v.type="button",v.className="chattermate-error-close",v.setAttribute("aria-label","Close chat"),v.textContent="\xD7",v.addEventListener("click",function(){r&&b()}),s.appendChild(v);let u=document.createElement("div");u.className="chattermate-error-icon-wrapper";let p=document.createElementNS("http://www.w3.org/2000/svg","svg");p.setAttribute("class","chattermate-error-icon"),p.setAttribute("viewBox","0 0 24 24"),p.setAttribute("fill","none"),p.setAttribute("stroke","currentColor"),p.setAttribute("stroke-width","1.5");let k=document.createElementNS("http://www.w3.org/2000/svg","path");k.setAttribute("d","M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z");let $=document.createElementNS("http://www.w3.org/2000/svg","path");$.setAttribute("d","M9 12l2 2 4-4"),p.appendChild(k),p.appendChild($),u.appendChild(p);let A=document.createElement("h2");A.className="chattermate-error-title",A.textContent="Chat Unavailable";let yt=document.createElement("p");yt.className="chattermate-error-message",yt.textContent=a;let it=document.createElement("div");it.className="chattermate-error-footer";let L=document.createElementNS("http://www.w3.org/2000/svg","svg");L.setAttribute("width","14"),L.setAttribute("height","14"),L.setAttribute("viewBox","0 0 60 60"),L.setAttribute("fill","none");let G=document.createElementNS("http://www.w3.org/2000/svg","path");G.setAttribute("d","M18 12 H42 A6 6 0 0 1 48 18 V42 A6 6 0 0 1 42 48 H18 A6 6 0 0 1 12 42 V18 A6 6 0 0 1 18 12 Z M17 30 a4 4 0 1 0 8 0 a4 4 0 1 0 -8 0 Z M26 30 a4 4 0 1 0 8 0 a4 4 0 1 0 -8 0 Z M35 30 a4 4 0 1 0 8 0 a4 4 0 1 0 -8 0 Z"),G.setAttribute("fill-rule","evenodd"),G.setAttribute("fill","currentColor"),G.setAttribute("opacity","0.6"),L.appendChild(G);let z=document.createElement("a");return z.href="https://chattermate.chat",z.target="_blank",z.rel="noopener noreferrer",z.className="chattermate-error-footer-link",z.textContent="Powered by ChatterMate",it.appendChild(L),it.appendChild(z),I.appendChild(u),I.appendChild(A),I.appendChild(yt),I.appendChild(it),s.appendChild(I),s}function et(){return wt(this,null,function*(){if(w||l)return;let a=D;try{w=!0,t.classList.add("loading");let s=dt(),I=`${e.baseUrl}/widgets/${window.chattermateId}/data?widget_id=${window.chattermateId}`,v={method:"GET",mode:"cors",headers:s?{Authorization:`Bearer ${s}`}:{}};fetch(I,v).then(u=>wt(null,null,function*(){if(a!==D)return null;if(u.status===401)try{let k=(yield u.json()).detail;if(k&&k.code==="identity_expired")return t.classList.remove("loading"),_("identityExpired"),Vt(),null;let $=typeof k=="string"?k:"";if($.includes("generate-token")||$.includes("API key")||$.includes("Token must be obtained")){t.classList.remove("loading"),H();let A=At("This chat widget is not currently configured. Please contact the website administrator to enable chat support.");return n.appendChild(A),F(),null}}catch(p){}if(!u.ok)throw new Error("HTTP error! status: "+u.status);return u.text()})).then(u=>{u!==null&&a===D&&(l=document.createElement("iframe"),l.className="chattermate-iframe",l.title="Chat",l.srcdoc=u,l.addEventListener("load",function(){bt=!0,l.contentWindow.postM Line truncated