1(function () { 2 var scriptTag = document.currentScript; 3 var businessId = scriptTag.getAttribute('data-business'); 4 if (!businessId) { console.error('Chat widget: data-business attribute missing'); return; } 5 6 var API_ROOT = "https://api.mentalforge.ai"; 7 try { 8 if (scriptTag.src) API_ROOT = new URL(scriptTag.src).origin; 9 } catch (e) {} 10 var API_BASE = API_ROOT + "/api"; 11 12 if (window.__mfWidgetLoaded) return; window.__mfWidgetLoaded = true; 13 14 var STORAGE_KEY = "mf_widget_state_" + businessId; 15 16 var widgetHost = document.createElement('div'); 17 widgetHost.id = 'mf-widget-host'; 18 document.body.appendChild(widgetHost); 19 var shadowRoot = widgetHost.attachShadow ? widgetHost.attachShadow({ mode: 'open' }) : widgetHost; 20 21 var config = { name: "Chat", logoUrl: "", brandColor: "#1B1A18", ctaLinks: [], launcherType: "default", launcherMediaUrl: "", hideBranding: false }; 22 var busy = false; 23 var socket = null; 24 var seenMsgIds = {}; 25 26 var state = loadState(); 27 if (!state.sessionId) { state.sessionId = genSessionId(); saveState(); } 28 29 function botIconSvg(px) { 30 return '<svg viewBox="0 0 24 24" width="' + px + '" height="' + px + '" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + 31 '<circle cx="12" cy="5.2" r="1.3" fill="currentColor" stroke="none"/>' + 32 '<path d="M12 6.8v2"/>' + 33 '<rect x="5" y="9.4" width="14" height="11.2" rx="5"/>' + 34 '<circle cx="9.3" cy="14.7" r="1.25" fill="currentColor" stroke="none"/>' + 35 '<circle cx="14.7" cy="14.7" r="1.25" fill="currentColor" stroke="none"/>' + 36 '<path d="M9.5 17.4c.9.65 3.6.65 4.5 0"/>' + 37 '</svg>'; 38 } 39 var BOT_ICON_SVG = botIconSvg(15); 40 var USER_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'; 41 var HUMAN_ICON_SVG = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>'; 42 var CLOSE_ICON_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'; 43 var DEFAULT_LAUNCH_SVG = '<svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><circle cx="9" cy="10" r="1" fill="#ffffff"/><circle cx="12" cy="10" r="1" fill="#ffffff"/><circle cx="15" cy="10" r="1" fill="#ffffff"/></svg>'; 44 var SEND_ICON_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>'; 45 var MENU_ICON_SVG = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>'; 46 var DOWNLOAD_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'; 47 var LEAVE_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>';
48 var THUMB_UP_SVG = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>'; 49 var MAIL_ICON_SVG = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><polyline points="22 6 12 13 2 6"/></svg>'; 50 var THUMB_DOWN_SVG = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/><path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/></svg>'; 51 52 function genSessionId() { 53 return 's_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10); 54 } 55 56 function shadeColor(hex, percent) { 57 hex = String(hex || '#1B1A18').replace('#', ''); 58 if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 59 var num = parseInt(hex, 16); 60 if (isNaN(num)) return '#1B1A18'; 61 var r = Math.min(255, Math.max(0, (num >> 16) + percent)); 62 var g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + percent)); 63 var b = Math.min(255, Math.max(0, (num & 0x0000FF) + percent)); 64 return '#' + (0x1000000 + r * 0x10000 + g * 0x100 + b).toString(16).slice(1); 65 } 66 67 function loadState() { 68 try { 69 var raw = localStorage.getItem(STORAGE_KEY); 70 if (raw) return JSON.parse(raw); 71 } catch (e) {} 72 return { leadCaptured: false, visitor: { name: "", email: "", phone: "" }, messages: [], liveMode: false, chatId: null, sessionId: null }; 73 } 74 function saveState() { 75 try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} 76 } 77 78 fetch(API_BASE + "/public/config/" + businessId) 79 .then(function (r) { return r.json(); }) 80 .then(function (data) { config = data; init(); }) 81 .catch(function () { init(); }); 82 83 function init() { 84 injectStyles(); 85 buildDOM(); 86 } 87 88 function injectStyles() { 89 var side = config.launcherPosition === 'left' ? 'left' : 'right'; 90 var brand = config.brandColor; 91 var brandDeep = shadeColor(brand, -35); 92 var FONT = 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'; 93 var css = 94 '.mf2-launch{position:fixed;' + side + ':22px;bottom:22px;z-index:2147483000;width:68px;height:68px;border:none;border-radius:50%;' + 95 'background:transparent;color:#fff;cursor:pointer;box-shadow:0 14px 34px -6px ' + brand + '77,0 6px 16px -4px rgba(0,0,0,.25);' + 96 'padding:0;transition:transform .2s cubic-bezier(.34,1.56,.64,1), box-shadow .2s ease;' + FONT + 97 'animation:mf2PopIn .35s cubic-bezier(.34,1.56,.64,1);}' + 98 '.mf2-launch:hover{transform:translateY(-4px) scale(1.06);box-shadow:0 18px 40px -6px ' + brand + '90,0 8px 20px -4px rgba(0,0,0,.3);}' + 99 '.mf2-launch:active{transform:translateY(-1px) scale(.97);}' + 100 '.mf2-launch-inner{position:relative;z-index:1;width:100%;height:100%;border-radius:50%;overflow:hidden;background:' + brand + ';display:grid;place-items:center;}' + 101 '.mf2-launch-media{width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;}' + 102 '.mf2-launch-ring{position:absolute;inset:0;border-radius:50%;background:' + brand + ';animation:mf2RingPulse 2.6s cubic-bezier(0,0,.2,1) infinite;pointer-events:none;}' + 103 '@keyframes mf2RingPulse{0%{transform:scale(.9);opacity:.4;}80%,100%{transform:scale(1.7);opacity:0;}}' + 104 '@keyframes mf2PopIn{0%{transform:scale(0);opacity:0;}100%{transform:scale(1);opacity:1;}}' + 105 '.mf2-dot{position:absolute;top:4px;right:4px;width:15px;height:15px;border-radius:50%;background:#EF4444;border:2.5px solid #fff;display:none;box-shadow:0 2px 6px rgba(0,0,0,.25);}' + 106 '.mf2-dot.show{display:block;animation:mf2Pulse 1.8s ease-in-out infinite;}' + 107 '@keyframes mf2Pulse{0%,100%{transform:scale(1);}50%{transform:scale(1.22);}}' + 108 109 '.mf2-teaser{position:fixed;' + side + ':22px;bottom:98px;z-index:2147482999;width:290px;max-width:calc(100vw - 44px);' + 110 'background:#fff;border-radius:18px;box-shadow:0 20px 46px -14px rgba(20,20,30,.28),0 4px 14px -6px rgba(20,20,30,.12);' + 111 'padding:16px;opacity:0;transform:translateY(12px) scale(.95);pointer-events:none;' + 112 'transition:opacity .22s ease, transform .22s ease;box-sizing:border-box;
112cursor:pointer;' + FONT + 'color:#15161F;}' + 113 '.mf2-teaser.show{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;}' + 114 '.mf2-teaser:after{content:"";position:absolute;bottom:-6px;' + side + ':30px;width:14px;height:14px;background:#fff;' + 115 'transform:rotate(45deg);box-shadow:3px 3px 6px -3px rgba(20,20,30,.15);}' + 116 '.mf2-teaser-close{position:absolute;top:8px;right:8px;width:22px;height:22px;border-radius:50%;border:none;background:#F1F1F4;' + 117 'color:#8A8B99;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;line-height:1;padding:0;transition:background .12s ease;}' + 118 '.mf2-teaser-close:hover{background:#E5E5EB;color:#45465A;}' + 119 '.mf2-teaser-row{display:flex;gap:10px;align-items:flex-start;padding-right:14px;}' + 120 '.mf2-teaser-avatar{width:36px;height:36px;border-radius:50%;flex:0 0 auto;background:' + brand + ';color:#fff;' + 121 'display:flex;align-items:center;justify-content:center;overflow:hidden;font-weight:700;font-size:13px;}' + 122 '.mf2-teaser-avatar img{width:100%;height:100%;object-fit:cover;display:block;}' + 123 '.mf2-teaser-name{font-weight:700;font-size:13px;margin-bottom:3px;color:#15161F;}' + 124 '.mf2-teaser-text{font-size:13px;line-height:1.45;color:#4B4C5C;}' + 125 '.mf2-teaser-chips{display:flex;flex-direction:column;gap:6px;margin-top:12px;}' + 126 '.mf2-teaser-chip{border:1px solid ' + brand + '4d;background:' + brand + '0f;color:' + brandDeep + ';text-align:left;' + 127 'padding:8px 12px;border-radius:10px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;' + 128 'transition:background .12s ease,border-color .12s ease;}' + 129 '.mf2-teaser-chip:hover{background:' + brand + '1f;border-color:' + brand + ';}' + 130 131 '.mf2-typing{display:flex;gap:4px;align-items:center;padding:13px 16px;}
131' + 132 '.mf2-typing span{width:6px;height:6px;border-radius:50%;background:#B5B6C4;display:inline-block;animation:mf2Wave 1.1s ease-in-out infinite;}' + 133 '.mf2-typing span:nth-child(2){animation-delay:.15s;}' + 134 '.mf2-typing span:nth-child(3){animation-delay:.3s;}' + 135 '@keyframes mf2Wave{0%,60%,100%{transform:translateY(0);opacity:.5;}30%{transform:translateY(-5px);opacity:1;}}' + 136 137 '.mf2-panel{position:fixed;' + side + ':22px;bottom:98px;z-index:2147483000;width:390px;max-width:calc(100vw - 32px);height:590px;' + 138 'max-height:calc(100vh - 130px);background:#fff;border:1px solid rgba(20,20,30,.08);border-radius:22px;overflow:hidden;display:flex;' + 139 'flex-direction:column;box-shadow:0 30px 70px -24px rgba(20,20,30,.35),0 8px 24px -8px rgba(20,20,30,.15);opacity:0;' + 140 'transform:translateY(16px) scale(.96);pointer-events:none;' + 141 'transition:opacity .18s ease, transform .18s cubic-bezier(.2,.8,.3,1);box-sizing:border-box;' + 142 FONT + 'color:#15161F;}' + 143 '.mf2-panel.open{opacity:1;transform:translateY(0) scale(1);pointer-events:auto;}' + 144 '.mf2-panel *{box-sizing:border-box;}' + 145 '.mf2-head{background:linear-gradient(135deg,' + brand + ',' + brandDeep + ');color:#fff;padding:16px 18px;display:flex;align-items:center;gap:12px;flex:0 0 auto;position:relative;}' + 146 '.mf2-head-logo{width:34px;height:34px;border-radius:10px;background:#fff;flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:4px;box-sizing:border-box;box-shadow:0 1px 4px rgba(0,0,0,.18),0 0 0 1px rgba(255,255,255,.55);overflow:hidden;}' +
147 '.mf2-head-logo img{width:100%;height:100%;object-fit:contain;display:block;}' + 148 '.mf2-head-text{min-width:0;}' + 149 '.mf2-title{font-weight:700;font-size:15.5px;letter-spacing:-0.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' + 150 '.mf2-status{font-size:11.5px;opacity:.9;display:flex;align-items:center;gap:6px;margin-top:2px;}' + 151 '.mf2-status-dot{width:7.5px;height:7.5px;border-radius:50%;background:#4ADE80;box-shadow:0 0 0 2px rgba(255,255,255,.35);flex:0 0 auto;position:relative;}' + 152 '.mf2-status-dot:after{content:"";position:absolute;inset:-3px;border-radius:50%;background:#4ADE80;opacity:.65;animation:mf2Glow 1.8s ease-out infinite;}' + 153 '.mf2-status-dot.away{background:#9CA0AE;}' + 154 '.mf2-status-dot.away:after{display:none;}' + 155 '@keyframes mf2Glow{0%{transform:scale(1);opacity:.65;}100%{transform:scale(2.6);opacity:0;}}' + 156 '.mf2-x{background:none;border:none;color:#fff;cursor:pointer;opacity:.85;display:flex;align-items:center;justify-content:center;padding:6px;border-radius:8px;transition:background .12s ease,opacity .12s ease;flex:0 0 auto;}' + 157 '.mf2-x:hover{opacity:1;background:rgba(255,255,255,.18);}' + 158 '.mf2-menu-btn{margin-left:auto;background:none;border:none;color:#fff;cursor:pointer;padding:6px;opacity:.85;display:grid;place-items:center;border-radius:8px;transition:background .12s ease,opacity .12s ease;flex:0 0 auto;}' + 159 '.mf2-menu-btn:hover{opacity:1;background:rgba(255,255,255,.18);}' + 160 '.mf2-menu-panel{position:absolute;inset:0;background:#fff;z-index:5;display:none;flex-direction:column;}' + 161 '.mf2-menu-panel.open{display:flex;}' + 162 '.mf2-menu-head{background:linear-gradient(135deg,' + brand + ',' + brandDeep + ');color:#fff;padding:16px 16px;display:flex;align-items:center;gap:10px;flex:0 0 auto;}' + 163 '.mf2-menu-back{background:none;border:none;color:#fff;cursor:pointer;font-size:18px;padding:4px;}' + 164 '.mf2-menu-title{font-weight:700;font-size:15px;}' + 165 '.mf2-menu-body{padding:10px;display:flex;flex-direction:column;gap:6px;}' + 166 '.mf2-menu-item{display:flex;align-items:center;gap:12px;padding:13px 12px;border:none;background:none;text-align:left;border-radius:10px;cursor:pointer;font-size:14px;color:#15161F;font-family:inherit;width:100%;transition:background .12s ease;}' + 167 '.mf2-menu-item:hover{background:#F4F4F7;}' + 168 '.mf2-menu-item.danger{color:#DC2626;}' + 169 '.mf2-menu-item svg{width:18px;height:18px;flex:0 0 auto;}' + 170 '.mf2-body{flex:1;overflow-y:auto;padding:18px 16px;display:flex;flex-direction:column;gap:14px;background:#F7F7FA;}' + 171 '.mf2-row{display:flex;gap:8px;max-width:88%;align-items:flex-end;}' + 172 '.mf2-row.user{align-self:flex-end;flex-direction:row-reverse;}' + 173 '.mf2-av{width:28px;height:28px;border-radius:9px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;background:#E9E9EF;color:#6B6C7E;}' + 174 '.mf2-av.rep{background:' + brand + ';color:#fff;}' + 175 '.mf2-bub{padding:11px 15px;border-radius:15px;font-size:14px;line-height:1.52;white-space:pre-wrap;}' + 176 '.mf2-bub.bot{background:#fff;border:1px solid #EAEAF0;border-top-left-radius:4px;box-shadow:0 1px 3px rgba(20,20,30,.04);}' + 177 '.mf2-bub.user{background:' + brand + ';color:#fff;border-top-right-radius:4px;box-shadow:0 2px 8px -2px ' + brand + '55;}' + 178 '.mf2-bub.rep{background:#fff;border:1px solid ' + brand + ';border-top-left-radius:4px;}' + 179 '.mf2-bub.system{background:#ECECF2;color:#5C5D70;font-size:12px;border-radius:10px;}' + 180 '.mf2-cta{margin-top:8px;display:inline-block;text-decoration:none;background:' + brand + ';color:#fff;' + 181 'font-size:13px;font-weight:600;padding:8px 14px;border-radius:10px;transition:opacity .12s ease;}' + 182 '.mf2-cta:hover{opacity:.9;}' + 183 '.mf2-feedback{display:flex;gap:4px;margin-top:4px;}' + 184 '.mf2-fb-btn{width:24px;height:24px;border:none;background:none;border-radius:7px;cursor:pointer;display:flex;' + 185 'align-items:center;justify-content:center;color:#ADAEBE;transition:background .12s ease,color .12s ease;padding:0;}' + 186 '.mf2-fb-btn:hover{background:#EFEFF4;color:#6B6C7E;}' + 187 '.mf2-fb-btn.active.up{color:#0E9F6E;background:#E7F4EC;}' +
188 '.mf2-fb-btn.active.down{color:#DC5B54;background:#FAE7E6;}' + 189 '.mf2-fb-btn[disabled]{cursor:default;}' + 190 '.mf2-fb-btn[disabled]:not(.active):hover{background:none;color:#ADAEBE;}' + 191 '.mf2-foot{border-top:1px solid #EDEDF2;padding:12px 14px;background:#fff;flex:0 0 auto;}' + 192 '.mf2-inrow{display:flex;gap:8px;align-items:center;}' + 193 '.mf2-input{flex:1;resize:none;border:1px solid #E5E5EC;border-radius:12px;padding:11px 14px;font-size:14px;font-family:inherit;outline:none;max-height:100px;background:#FAFAFC;transition:border-color .15s ease, box-shadow .15s ease, background .15s ease;}' + 194 '.mf2-input:focus{border-color:' + brand + ';background:#fff;box-shadow:0 0 0 3px ' + brand + '22;}' + 195 '.mf2-send{width:42px;height:42px;border:none;border-radius:12px;background:' + brand + ';color:#fff;cursor:pointer;display:grid;place-items:center;transition:transform .12s ease, opacity .12s ease;flex:0 0 auto;}' + 196 '.mf2-send:hover{opacity:.92;transform:translateY(-1px);}' + 197 '.mf2-send:active{transform:translateY(0);}' + 198 '.mf2-human-row{text-align:right;margin-top:8px;}' + 199 '.mf2-human{background:none;border:none;color:#8A8B99;cursor:pointer;font-size:12px;font-family:inherit;padding:0;display:inline-flex;align-items:center;gap:6px;transition:color .12s ease;font-weight:500;}' + 200 '.mf2-human span{text-decoration:underline;}' + 201 '.mf2-human:hover{color:' + brand + ';}' + 202 '.mf2-lead{background:#fff;border:1px solid #EAEAF0;border-radius:14px;padding:14px;font-size:13.5px;width:100%;box-shadow:0 1px 3px rgba(20,20,30,.04);}' + 203 '.mf2-lead input{width:100%;box-sizing:border-box;border:1px solid #E5E5EC;border-radius:9px;padding:9px 11px;font-size:13.5px;margin-bottom:8px;font-family:inherit;background:#FAFAFC;transition:border-color .15s ease, box-shadow .15s ease;}' + 204 '.mf2-lead input:focus{outline:none;border-color:' + brand + ';box-shadow:0 0 0 3px ' + brand + '22;background:#fff;}' + 205 '.mf2-lead button{width:100%;border:none;background:' + brand + ';color:#fff;font-weight:600;padding:10px;border-radius:9px;cursor:pointer;transition:opacity .12s ease;}' + 206 '.mf2-lead button:hover{opacity:.9;}' + 207 '.mf2-err{color:#DC2626;font-size:12px;margin:-3px 0 8px;}' + 208 209 '.mf2-gate{flex:1;display:flex;align-items:center;justify-content:center;padding:28px 24px;background:#fff;overflow-y:auto;}' + 210 '.mf2-gate-card{width:100%;max-width:290px;text-align:center;}' + 211 '.mf2-gate-icon{width:58px;height:58px;border-radius:50%;background:linear-gradient(135deg,' + brand + ',' + brandDeep + ');color:#fff;' + 212 'display:flex;align-items:center;justify-content:center;margin:0 auto 16px;box-shadow:0 8px 20px -6px ' + brand + '55;}' + 213 '.mf2-gate-title{font-weight:700;font-size:17.5px;margin-bottom:6px;color:#15161F;letter-spacing:-0.01em;}' + 214 '.mf2-gate-desc{font-size:13px;color:#6B6C7E;margin-bottom:20px;line-height:1.5;}' + 215 '.mf2-gate-card input{width:100%;box-sizing:border-box;border:1px solid #E5E5EC;border-radius:11px;padding:12px 14px;font-size:14px;' + 216 'margin-bottom:10px;font-family:inherit;background:#FAFAFC;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease;}' + 217 '.mf2-gate-card input:focus{outline:none;border-color:' + brand + ';box-shadow:0 0 0 3px ' + brand + '22;background:#fff;}' + 218 '.mf2-gate-card button{width:100%;border:none;background:' + brand + ';color:#fff;font-weight:700;padding:13px;border-radius:11px;' + 219 'cursor:pointer;font-size:14px;transition:opacity .12s ease,transform .12s ease;margin-top:4px;}' + 220 '.mf2-gate-card button:hover{opacity:.92;transform:translateY(-1px);}' + 221 '.mf2-gate-err{color:#DC2626;font-size:12px;margin:8px 0 0;}' + 222 '.mf2-branding{display:flex;align-items:center;justify-content:center;gap:6px;padding:9px 10px;border-top:1px solid #F0F0F4;background:#FBFBFD;font-size:11px;color:#A5A6B3;text-decoration:none;flex:0 0 auto;font-family:inherit;transition:color .12s ease;}' + 223 '.mf2-branding:hover{color:#5C5D70;}' + 224 '.mf2-branding-mark{width:14px;
224height:14px;border-radius:4px;background:#5B5BD6;color:#fff;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex:0 0 auto;}' + 225 // Date dividers 226 '.mf2-date-div{display:flex;align-items:center;gap:8px;padding:2px 0 4px;width:100%;}' + 227 '.mf2-date-line{flex:1;height:1px;background:#E8E8EF;}' + 228 '.mf2-date-label{font-size:10.5px;color:#A5A6B3;white-space:nowrap;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:0.03em;}' + 229 // Hover time on messages 230 '.mf2-bub-wrap{position:relative;}' + 231 '.mf2-bub-time{display:none;position:absolute;bottom:-18px;font-size:10px;color:#A5A6B3;white-space:nowrap;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;pointer-events:none;}' + 232 '.mf2-bub-time.left{left:0;}' + 233 '.mf2-bub-time.right{right:0;}' + 234 '.mf2-bub-wrap:hover .mf2-bub-time{display:block;}'; 235 236 var style = document.createElement("style"); 237 style.textContent = css; 238 shadowRoot.appendChild(style); 239 } 240 241 var body, input, sendBtn, panel, dot, humanBtn, menuBtn, menuPanel, menuBack, downloadBtn, emailBtn, leaveBtn, teaser, teaserTimer, gate, foot; 242 var TEASER_DISMISS_KEY = "mf_teaser_dismissed_" + businessId; 243 var pendingQuickReply = null; 244 var typingRow = null; 245 var typingTimeout = null; 246 var lastBubbleDate = null; // tracks date of last rendered bubble for dividers 247 248 // âââ Date/Time Helpers ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 249 function mfFormatTime(d) { 250 if (!d) return ''; 251 return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 252 } 253 function mfDateLabel(d) { 254 if (!d) return ''; 255 var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; 256 var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; 257 return days[d.getDay()] + ', ' + d.getDate() + ' ' + months[d.getMonth()]; 258 } 259 function mfSameDay(a, b) { 260 if (!a || !b) return false; 261 return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); 262 } 263 function addDateDividerIfNeeded(ts) { 264 var d = ts ? new Date(ts) : new Date(); 265 if (!mfSameDay(d, lastBubbleDate)) { 266 lastBubbleDate = d; 267 var div = document.createElement('div'); 268 div.className = 'mf2-date-div'; 269 var line1 = document.createElement('span'); line1.className = 'mf2-date-line'; 270 var label = document.createElement('span'); label.className = 'mf2-date-label'; label.textContent = mfDateLabel(d); 271 var line2 = document.createElement('span'); line2.className = 'mf2-date-line'; 272 div.appendChild(line1); div.appendChild(label); div.appendChild(line2); 273 body.appendChild(div); 274 } 275 } 276 277 function buildLauncherInner() { 278 if (config.launcherType === 'image' && config.launcherMediaUrl) { 279 return '<img class="mf2-launch-media" src="' + API_ROOT + config.launcherMediaUrl + '" alt="">'; 280 } 281 if (config.launcherType === 'video' && config.launcherMediaUrl) { 282 return '<video class="mf2-launch-media" src="' + API_ROOT + config.launcherMediaUrl + '" autoplay muted loop playsinline></video>'; 283 } 284 return DEFAULT_LAUNCH_SVG; 285 } 286 287 function buildDOM() { 288 var launch = document.createElement('button'); 289 launch.className = 'mf2-launch'; 290 launch.setAttribute('aria-label', 'Open chat'); 291 launch.innerHTML = 292 '<span class="mf2-launch-ring"></span>' + 293 '<span class="mf2-launch-ring" style="animation-delay:1.3s"></span>' + 294 '<span class="mf2-launch-inner">' + buildLauncherInner() + '</span>' + 295 '<span class="mf2-dot" id="mf2Dot"></span>'; 296 297 panel = document.createElement('div'); 298 panel.className = 'mf2-panel'; 299 panel.innerHTML = 300 '<div class="mf2-head">' + 301 (config.logoUrl ? '<div class="mf2-head-logo"><img src="' + API_ROOT + config.logoUrl + '" alt=""></div>' : '') + 302 '<div class="mf2-head-text">' + 303 '<div class="mf2-title">' + escapeHtml(config.name) + '</div>' + 304 '<div class="mf2-status">' + 305 (config.isOpen === false 306 ? '<span class="mf2-status-dot away"></span>Currently away' 307 : '<span class="mf2-status-dot"></span>Online now') + 308 '</div>' + 309 '</div>' + 310 '<button class="mf2-menu-btn" id="mf2MenuBtn" aria-label="More options">' + MENU_ICON_SVG + '</button>' + 311 '<button class="mf2-x" aria-label="Close">' + CLOSE_ICON_SVG + '</button>' + 312 '</div>' + 313 '<div class="mf2-body" id="mf2Body"></div>' + 314 '<div class="mf2-gate" id="mf2Gate" style="display:none;"></div>' + 315 '<div class="mf2-menu-panel" id="mf2MenuPanel">' + 316 '<div class="mf2-menu-head"><button class="mf2-menu-back" id="mf2MenuBack" aria-label="Back">←</button><div class="mf2-menu-title">Options</div></div>' + 317 '<div class="mf2-menu-body">' + 318 '<button class="mf2-menu-item" id="mf2DownloadBtn">' + DOWNLOAD_ICON_SVG + 'Download conversation</button>' + 319 '<button class="mf2-menu-item" id="mf2EmailBtn">' + MAIL_ICON_SVG + 'Email me this conversation</button>' + 320 '<button class="mf2-menu-item danger" id="mf2LeaveBtn">' + LEAVE_ICON_SVG + 'Leave this chat</button>' + 321 '</div>' + 322 '</div>' + 323 '<div class="mf2-foot" id="mf2Foot">' + 324 '<div class="mf2-inrow">' + 325 '<textarea class="mf2-input" id="mf2Input" rows="1" placeholder="Type a messageâ¦" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false"></textarea>' + 326 '<button class="mf2-send" id="mf2Send">' + SEND_ICON_SVG + '</button>' + 327 '</div>' + 328 '<div class="mf2-human-row"><button class="mf2-human" id="mf2Human">' + HUMAN_ICON_SVG + '<span>Talk to a Representative â</span></button></div>' + 329 '</div>' + 330 (config.hideBranding ? '' : 331 '<a class="mf2-branding" href="https://mentalforge.ai" target="_blank" rel="noopener">' + 332 '<span class="mf2-branding-mark">M</span>Powered by MentalForge AI' + 333 '</a>'); 334 335 var quickReplies = (config.faqs && config.faqs.length) 336 ? config.faqs.map(function (f) { return f.question; }
336).filter(Boolean).slice(0, 3) 337 : ["What can you help me with?", "Tell me about your services", "I have a question"]; 338 339 teaser = document.createElement('div'); 340 teaser.className = 'mf2-teaser'; 341 var teaserAvatar = config.logoUrl 342 ? '<img src="' + API_ROOT + config.logoUrl + '" alt="">' 343 : botIconSvg(18); 344 teaser.innerHTML = 345 '<button class="mf2-teaser-close" id="mf2TeaserClose" aria-label="Dismiss">' + CLOSE_ICON_SVG.replace(/width="18" height="18"/, 'width="11" height="11"').replace(/stroke="#fff"/, 'stroke="currentColor"') + '</button>' + 346 '<div class="mf2-teaser-row">' + 347 '<div class="mf2-teaser-avatar">' + teaserAvatar + '</div>' + 348 '<div>' + 349 '<div class="mf2-teaser-name">' + escapeHtml(config.name) + '</div>' + 350 '<div class="mf2-teaser-text">' + escapeHtml(greetingText()) + '</div>' + 351 '</div>' + 352 '</div>' + 353 (quickReplies.length 354 ? '<div class="mf2-teaser-chips" id="mf2TeaserChips">' + 355 quickReplies.map(function (q, i) { 356 return '<button class="mf2-teaser-chip" data-q="' + i + '">' + escapeHtml(q) + '</button>'; 357 }).join('') + 358 '</div>' 359 : ''); 360 361 shadowRoot.appendChild(launch); 362 shadowRoot.appendChild(teaser); 363 shadowRoot.appendChild(panel); 364 365 body = panel.querySelector('#mf2Body'); 366 gate = panel.querySelector('#mf2Gate'); 367 foot = panel.querySelector('#mf2Foot'); 368 input = panel.querySelector('#mf2Input'); 369 sendBtn = panel.querySelector('#mf2Send'); 370 dot = launch.querySelector('#mf2Dot'); 371 humanBtn = panel.querySelector('#mf2Human'); 372 menuBtn = panel.querySelector('#mf2MenuBtn'); 373 menuPanel = panel.querySelector('#mf2MenuPanel'); 374 menuBack = panel.querySelector('#mf2MenuBack'); 375 downloadBtn = panel.querySelector('#mf2DownloadBtn'); 376 emailBtn = panel.querySelector('#mf2EmailBtn'); 377 leaveBtn = panel.querySelector('#mf2LeaveBtn'); 378 379 var opened = false; 380 function openPanel() { 381 panel.classList.add('open'); 382 clearDot();
383 dismissTeaser(true); 384 if (!opened) { opened = true; resumeOrStart(); } 385 } 386 launch.addEventListener('click', function () { 387 if (panel.classList.contains('open')) { panel.classList.remove('open'); return; } 388 openPanel(); 389 }); 390 teaser.addEventListener('click', openPanel); 391 teaser.querySelector('#mf2TeaserClose').addEventListener('click', function (e) { 392 e.stopPropagation(); 393 dismissTeaser(true); 394 }); 395 var teaserChips = teaser.querySelectorAll('.mf2-teaser-chip'); 396 for (var qi = 0; qi < teaserChips.length; qi++) { 397 teaserChips[qi].addEventListener('click', function (e) { 398 e.stopPropagation(); 399 pendingQuickReply = quickReplies[Number(e.currentTarget.getAttribute('data-q'))]; 400 dismissTeaser(true); 401 openPanel(); 402 }); 403 } 404 panel.querySelector('.mf2-x').addEventListener('click', function () { panel.classList.remove('open'); }); 405 sendBtn.addEventListener('click', function () { send(); }); 406 input.addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }); 407 humanBtn.addEventListener('click', function () { startLiveChat(); }); 408 menuBtn.addEventListener('click', function () { menuPanel.classList.add('open'); }); 409 menuBack.addEventListener('click', function () { menuPanel.classList.remove('open'); }); 410 downloadBtn.addEventListener('click', downloadConversation); 411 emailBtn.addEventListener('click', emailTranscript); 412 leaveBtn.addEventListener('click', leaveChat); 413 414 maybeShowTeaser(); 415 } 416 417 function maybeShowTeaser() { 418 if (state.messages.length > 0) return; 419 var dismissed = false; 420 try { dismissed = localStorage.getItem(TEASER_DISMISS_KEY) === '1'; } catch (e) {} 421 if (dismissed) return; 422 423 function reveal() { 424 if (panel.classList.contains('open') || teaser.classList.contains('show')) return; 425 teaser.classList.add('show'); 426 } 427 428 teaserTimer = setTimeout(reveal, 20000); 429 430 // Exit intent â mouse moving up toward the browser chrome (tab bar / back button) 431 // usually means the visitor is about to leave the page. 432 function onMouseLeave(e) { 433 if (e.clientY > 0) return; 434 reveal(); 435 document.removeEventListener('mouseleave', onMouseLeave); 436 } 437 document.addEventListener('mouseleave', onMouseLeave); 438 } 439 440 function dismissTeaser(permanent) { 441 clearTimeout(teaserTimer); 442 if (teaser) teaser.classList.remove('show'); 443 if (permanent) { 444 try { localStorage.setItem(TEASER_DISMISS_KEY, '1'); } catch (e) {} 445 } 446 } 447 448 function escapeHtml(s) { return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } 449 function scrollDown() { body.scrollTop = body.scrollHeight; } 450 function showDot() { if (!panel.classList.contains('open')) { dot.classList.add('show'); playBeep(); } } 451 function clearDot() { dot.classList.remove('show'); } 452 453 function playBeep() { 454 try { 455 var Ctx = window.AudioContext || window.webkitAudioContext; 456 var ctx = new Ctx(); 457 var osc = ctx.createOscillator(), gain = ctx.createGain(); 458 osc.type = 'sine'; osc.frequency.value = 760; 459 osc.connect(gain); gain.connect(ctx.destination); 460 gain.gain.setValueAtTime(0.001, ctx.currentTime); 461 gain.gain.exponentialRampToValueAtTime(0.18, ctx.currentTime + 0.01); 462 gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15); 463 osc.start(); osc.stop(ctx.currentTime + 0.17); 464 } catch (e) {} 465 } 466 467 function isSafeCtaUrl(url) { 468 try { 469 var scheme = String(url).trim().split(':')[0].toLowerCase(); 470 return scheme === 'http' || scheme === 'https' || scheme === 'mailto' || scheme === 'tel'; 471 } catch (e) { 472 return false; 473 } 474 } 475 476 function addBubble(role, text, ctaUrl, ctaLabel, silent, ts, chatLogId) { 477 var cls = role === 'user' ? 'user' : (role === 'rep' ? 'rep' : (role === 'system' ? 'system' : 'bot')); 478 var row = document.createElement('div'); 479 row.className = 'mf2-row' + (role === 'user' ? ' user' : ''); 480 481 // Inject date divider if this is a new day 482 addDateDividerIfNeeded(ts); 483 484 if (role !== 'system') { 485 var av = document.createElement('div'); 486 av.className = 'mf2-av' + (role === 'rep' ? ' rep' : ''); 487 if (role === 'user') { 488 av.innerHTML = USER_ICON_SVG; 489 } else if (role === 'rep') { 490 av.innerHTML = HUMAN_ICON_SVG; 491 } else { 492 av.innerHTML = BOT_ICON_SVG; 493 } 494 row.appendChild(av); 495 } 496 497 var col = document.createElement('div'); 498 var wrap = document.createElement('div'); 499 wrap.className = 'mf2-bub-wrap'; 500 501 var bub = document.createElement('div'); 502 bub.className = 'mf2-bub ' + cls; 503 bub.textContent = text; 504 wrap.appendChild(bub); 505 506 // Time tooltip on hover 507 var timeStr = mfFormatTime(ts ? new Date(ts) : new Date()); 508 if (timeStr) { 509 var timeEl = document.createElement('div'); 510 timeEl.className = 'mf2-bub-time ' + (role === 'user' ? 'right' : 'left'); 511 timeEl.textContent = timeStr; 512 wrap.appendChild(timeEl); 513 } 514 515 col.appendChild(wrap); 516 517 if (ctaUrl && isSafeCtaUrl(ctaUrl)) { 518 var a = document.createElement('a'); 519 a.className = 'mf2-cta'; a.href = ctaUrl; a.target = '_blank'; a.rel = 'noopener'; 520 a.textContent = ctaLabel || 'Learn more'; 521 col.appendChild(document.createElement('br')); 522 col.appendChild(a); 523 } 524 525 if (role === 'assistant' && chatLogId) { 526 col.appendChild(buildFeedbackRow(chatLogId)); 527 } 528 529 row.appendChild(col); 530 body.appendChild(row); 531 if (typingRow) body.appendChild(typingRow); 532 scrollDown(); 533 if (!silent && (role === 'assistant' || role === 'rep')) showDot(); 534 } 535 function addSystem(text, ts) { addBubble('system', text, null, null, true, ts); } 536 537 function buildFeedbackRow(chatLogId) { 538 var wrap = document.createElement('div'); 539 wrap.className = 'mf2-feedback'; 540 541 var up = document.createElement('button'); 542 up.className = 'mf2-fb-btn'; up.innerHTML = THUMB_UP_SVG; up.setAttribute('aria-label', 'Good reply'); up.type = 'button'; 543 var down = document.createElement('button'); 544 down.className = 'mf2-fb-btn'; down.innerHTML = THUMB_DOWN_SVG; down.setAttribute('aria-label', 'Bad reply'); down.type = 'button'; 545 546 function rate(rating, btn, otherBtn) { 547 if (up.disabled) return; 548 up.disabled = true; down.disabled = true; 549 btn.classList.add('active', rating); 550 fetch(API_BASE + "/public/feedback/" + chatLogId, { 551 method: 'POST', headers: { 'Content-Type': 'application/json' }, 552 body: JSON.stringify({ rating: rating }) 553 }).catch(function () {}); 554 } 555 up.addEventListener('click', function () { rate('up', up, down); }); 556 down.addEventListener('click', function () { rate('down', down, up); }); 557 558 wrap.appendChild(up); 559 wrap.appendChild(down); 560 return wrap; 561 } 562 563 function showTyping(isLive) { 564 hideTyping(); 565 typingRow = document.createElement('div'); 566 typingRow.className = 'mf2-row'; 567 if (isLive) { 568 typingRow.innerHTML = 569 '<div class="mf2-av rep">' + HUMAN_ICON_SVG + '</div>' + 570 '<div class="mf2-bub rep mf2-typing"><span></span><span></span><span></span></div>'; 571 } else { 572 typingRow.innerHTML = 573 '<div class="mf2-av">' + BOT_ICON_SVG + '</div>' + 574 '<div class="mf2-bub bot mf2-typing"><span></span><span></span><span></span></div>'; 575 } 576 body.appendChild(typingRow); 577 scrollDown(); 578 } 579 function hideTyping() { 580 if (typingRow) { typingRow.remove(); typingRow = null; } 581 } 582 583 // ---------- lead capture ----------
584 var pendingLivePurpose = false; 585 586 function isValidEmail(v) { 587 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v); 588 } 589 function isValidPhone(v) { 590 return /^[0-9+()\-\s]{6,20}$/.test(v) && /\d{5,}/.test(v.replace(/[^0-9]/g, '')); 591 } 592 593 function submitLead(name, email, phone, onDone) { 594 fetch(API_BASE + "/public/lead/" + businessId, { 595 method: 'POST', headers: { 'Content-Type': 'application/json' }, 596 body: JSON.stringify({ name: name, email: email, phone: phone, sessionId: state.sessionId }) 597 }).catch(function () {}).finally(function () { 598 state.leadCaptured = true; 599 state.visitor = { name: name, email: email, phone: phone }; 600 saveState(); 601 onDone(); 602 }); 603 } 604 605 function validateLeadFields(card, errBox) { 606 var name = card.querySelector('#mf2Name').value.trim(); 607 var email = card.querySelector('#mf2Email').value.trim(); 608 var phone = card.querySelector('#mf2Phone').value.trim(); 609 errBox.style.display = 'none'; 610 if (!name) { errBox.textContent = 'Please enter your name.'; errBox.style.display = 'block'; return null; } 611 if (!email) { errBox.textContent = 'Please enter your email address.'; errBox.style.display = 'block'; return null; } 612 if (!isValidEmail(email)) { errBox.textContent = 'Please enter a valid email address.'; errBox.style.display = 'block'; return null; } 613 if (!phone) { errBox.textContent = 'Please enter your phone number.'; errBox.style.display = 'block'; return null; } 614 if (!isValidPhone(phone)) { errBox.textContent = 'Please enter a valid phone number.'; errBox.style.display = 'block'; return null; } 615 return { name: name, email: email, phone: phone }; 616 } 617 618 function showLeadForm(purpose) { 619 pendingLivePurpose = purpose === 'live'; 620 621 if (pendingLivePurpose) { 622 var row = document.createElement('div'); row.className = 'mf2-row'; 623 var card = document.createElement('div'); card.className = 'mf2-lead'; 624 card.innerHTML = 625 '<div style="font-weight:600;margin-bottom:6px;">Hi! Who am I speaking with?</div>' + 626 '<div style="font-size:12px;color:#6E6A63;margin-bottom:8px;">So our team can follow up with you directly.</div>' + 627 '<input type="text" id="mf2Name" placeholder="Your name" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false">' + 628 '<input type="email" id="mf2Email" placeholder="Email address" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false">' + 629 '<input type="tel" id="mf2Phone" placeholder="Phone number" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false">' + 630 '<div class="mf2-err" id="mf2Err" style="display:none;"></div>' + 631 '<button id="mf2LeadBtn">Continue</button>'; 632 row.appendChild(card); body.appendChild(row); scrollDown(); 633 634 card.querySelector('#mf2LeadBtn').addEventListener('click', function () { 635 var fields = validateLeadFields(card, card.querySelector('#mf2Err')); 636 if (!fields) return; 637 submitLead(fields.name, fields.email, fields.phone, function () { 638 row.remove(); 639 actuallyStartLiveChat(); 640 }); 641 }); 642 return; 643 } 644 645 // First-time visitor: block the whole chat behind a prominent, centered gate. 646 body.style.display = 'none'; 647 foot.style.display = 'none'; 648 gate.style.display = 'flex'; 649 gate.innerHTML = 650 '<div class="mf2-gate-card">' + 651 '<div class="mf2-gate-icon">' + botIconSvg(30) + '</div>' + 652 '<div class="mf2-gate-title">Hi! Who am I speaking with?</div>' + 653 '<div class="mf2-gate-desc">Please share your details to start chatting with us.</div>' + 654 '<input type="text" id="mf2Name" placeholder="Your name" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false">' + 655 '<input type="email" id="mf2Email" placeholder="Email address" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false">' + 656 '<input type="tel" id="mf2Phone" placeholder="Phone number" data-gramm="false" data-gramm_editor="false" data-enable-grammarly="false">' + 657 '<button id="mf2LeadBtn">Continue</button>' + 658 '<div class="mf2-gate-err" id="mf2Err" style="display:none;"></div>' + 659 '</div>'; 660 661 gate.querySelector('#mf2LeadBtn').addEventListener('click', function () { 662 var fields = validateLeadFields(gate, gate.querySelector('#mf2Err')); 663 if (!fields) return; 664 submitLead(fields.name, fields.email, fields.phone, function () { 665 gate.style.display = 'none'; 666 gate.innerHTML = ''; 667 body.style.display = ''; 668 foot.style.display = ''; 669 startChat(); 670 }); 671 }); 672 } 673 674 function resumeOrStart() { 675 if (state.messages.length > 0) {
676 state.messages.forEach(function (m) { addBubble(m.role, m.content, null, null, true, m.ts); }); 677 if (state.liveMode && state.chatId) connectLiveChat(state.chatId); 678 return; 679 } 680 if (!state.leadCaptured) { showLeadForm('start'); return; } 681 startChat(); 682 } 683 684 function greetingText() { 685 if (config.isOpen === false && config.awayMessage) return config.awayMessage; 686 return config.welcomeMessage || "Hi! How can I help you today?"; 687 } 688 689 function startChat() { 690 var greeting = greetingText(); 691 addBubble('assistant', greeting, null, null, true); 692 state.messages.push({ role: 'assistant', content: greeting }); 693 saveState(); 694 input.focus(); 695 if (pendingQuickReply) { 696 var q = pendingQuickReply; 697 pendingQuickReply = null; 698 send(q); 699 } 700 } 701 702 var LIVE_TRIGGER_RE = /(talk|connect|speak|chat).{0,20}(human|agent|representative|person|team member)|human agent|live agent|real person/i; 703 704 async function send(text) { 705 var content = (text !== undefined ? text : input.value).trim(); 706 if (!content || busy) return; 707 708 if (state.liveMode) { sendLiveMessage(content); return; } 709 710 if (LIVE_TRIGGER_RE.test(content)) { 711 input.value = ''; input.style.height = 'auto'; 712 startLiveChat(); 713 return; 714 } 715 716 busy = true; sendBtn.disabled = true; 717 var now = new Date(); 718 addBubble('user', content, null, null, true, now); 719 state.messages.push({ role: 'user', content: content, ts: now }); 720 saveState(); 721 input.value = ''; input.style.height = 'auto'; 722 showTyping(); 723 724 try { 725 var res = await fetch(API_BASE + "/chat/" + businessId, { 726 method: 'POST', headers: { 'Content-Type': 'application/json' }, 727 body: JSON.stringify({ 728 messages: state.messages.filter(function (m) { return m.role === 'user' || m.role === 'assistant'; }), 729 sessionId: state.sessionId 730 }) 731 }); 732 var data = await res.json(); 733 hideTyping(); 734 var reply = data.reply || "Sorry, something went wrong. Please try again."; 735 var replyTime = new Date(); 736 var cta = (config.ctaLinks && config.ctaLinks[0]) ? config.ctaLinks[0] : null; 737 addBubble('assistant', reply, cta ? cta.url : null, cta ? cta.label : null, false, replyTime, data.chatLogId); 738 state.messages.push({ role: 'assistant', content: reply, ts: replyTime }); 739 saveState(); 740 } catch (e) { 741 hideTyping(); 742 addBubble('assistant', "Sorry, something went wrong. Please try again.", null, null, false, new Date()); 743 } finally { 744 busy = false; sendBtn.disabled = false; input.focus(); 745 } 746 } 747 748 // ---------- hamburger menu actions ---------- 749 function buildTranscriptText() { 750 var lines = state.messages.map(function (m) { 751 var who = m.role === 'user' ? 'You' : (m.role === 'rep' ? 'Team member' : (m.role === 'system' ? 'System' : (config.name || 'Assistant'))); 752 return who + ': ' + m.content; 753 }); 754 return lines.length ? lines.join('\n\n') : 'No conversation yet.'; 755 } 756 757 function downloadConversation() { 758 var text = buildTranscriptText(); 759 var blob = new Blob([text], { type: 'text/plain' }); 760 var url = URL.createObjectURL(blob); 761 var a = document.createElement('a'); 762 a.href = url; 763 a.download = 'chat-transcript-' + new Date().toISOString().slice(0, 10) + '.txt'; 764 document.body.appendChild(a); 765 a.click(); 766 document.body.removeChild(a); 767 URL.revokeObjectURL(url); 768 menuPanel.classList.remove('open'); 769 } 770 771 function emailTranscript() { 772 menuPanel.classList.remove('open'); 773 var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 774 var email = (state.visitor && state.visitor.email && EMAIL_RE.test(state.visitor.email)) ? state.visitor.email : null; 775 if (!email) { 776 email = window.prompt('Enter your email to receive this conversation:'); 777 if (!email || !EMAIL_RE.test(email)) return; 778 } 779 addSystem('Sending the conversation to ' + email + 'â¦'); 780 fetch(API_BASE + "/public/transcript/" + businessId, { 781 method: 'POST', headers: { 'Content-Type': 'application/json' }, 782 body: JSON.stringify({ email: email, transcript: buildTranscriptText() }) 783 }).then(function (r) { return r.json(); }).then(function (data) { 784 addSystem(data.success ? 'Sent! Check your inbox.' : 'Could not send that right now. Please try again.'); 785 }).catch(function () { 786 addSystem('Could not send that right now. Please try again.'); 787 }); 788 } 789 790 function leaveChat() { 791 if (!confirm('Leave this chat? This will clear your conversation and end any active live chat.')) return; 792 793 if (state.liveMode && state.chatId) { 794 fetch(API_BASE + "/livechat/" + state.chatId + "/leave", { method: 'POST' }).catch(function () {}); 795 if (socket) socket.disconnect(); 796 } 797 798 try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} 799 state = { leadCaptured: false, visitor: { name: "", email: "", phone: "" }, messages: [], liveMode: false, chatId: null, sessionId: genSessionId() }; 800 saveState(); 801 seenMsgIds = {}; 802 socket = null; 803 typingRow = null; 804 body.innerHTML = ''; 805 menuPanel.classList.remove('open'); 806 humanBtn.style.display = 'inline-flex'; 807 humanBtn.disabled = false;
808 resumeOrStart(); 809 } 810 811 // ================= LIVE AGENT HANDOFF ================= 812 813 function loadSocketIO(cb) { 814 if (window.io) { cb(); return; } 815 var s = document.createElement('script'); 816 s.src = API_ROOT + "/socket.io/socket.io.js"; 817 s.onload = cb; 818 document.head.appendChild(s); 819 } 820 821 function startLiveChat() { 822 if (state.liveMode) { addSystem("You're already connected. Send your message below."); return; } 823 if (!state.leadCaptured) { showLeadForm('live'); return; } 824 actuallyStartLiveChat(); 825 } 826 827 function actuallyStartLiveChat() { 828 humanBtn.disabled = true; 829 fetch(API_BASE + "/livechat/create/" + businessId, { 830 method: 'POST', headers: { 'Content-Type': 'application/json' }, 831 body: JSON.stringify({ name: state.visitor.name, email: state.visitor.email, phone: state.visitor.phone, sessionId: state.sessionId }) 832 }).then(function (r) { return r.json(); }) 833 .then(function (data) { 834 if (!data.chatId) { 835 var unavailableMsg = data.message || "Live chat isn't available right now."; 836 addSystem(unavailableMsg); 837 state.messages.push({ role: 'system', content: unavailableMsg }); 838 saveState(); 839 humanBtn.disabled = false; 840 return; 841 } 842 state.liveMode = true; 843 state.chatId = data.chatId; 844 saveState(); 845 var msg = "Connecting you to a team member. Hang tight, someone will join shortly."; 846 addSystem(msg); 847 state.messages.push({ role: 'system', content: msg }); 848 saveState(); 849 connectLiveChat(data.chatId); 850 }); 851 } 852 853 function connectLiveChat(chatId) { 854 humanBtn.style.display = 'none'; 855 loadSocketIO(function () { 856 if (!socket) socket = io(API_ROOT); 857 // Re-join on every (re)connect, not just the first â otherwise a 858 // dropped/reconnected socket silently stops receiving replies. 859 socket.off('connect'); 860 socket.on('connect', function () { socket.emit('join_chat', chatId); }); 861 socket.emit('join_chat', chatId); 862 863 socket.off('new_message'); 864 socket.on('new_message', function (m) { 865 var ts = m.timestamp || m.createdAt || new Date(); 866 if (m.sender === 'system') { 867 hideTyping(); 868 addSystem(m.text, ts); 869 state.messages.push({ role: 'system', content: m.text, ts: ts }); 870 saveState(); 871 return; 872 } 873 if (m.sender === 'rep') hideTyping(); 874 var role = m.sender === 'rep' ? 'rep' : 'user'; 875 addBubble(role, m.text, null, null, role === 'user', ts); 876 state.messages.push({ role: role, content: m.text, ts: ts }); 877 saveState(); 878 }); 879 880 socket.off('agent_typing'); 881 socket.on('agent_typing', function () { 882 showTyping(true); 883 // Auto-hide after 4 seconds if no message arrives 884 clearTimeout(typingTimeout); 885 typingTimeout = setTimeout(hideTyping, 4000); 886 }); 887 888 socket.off('chat_updated'); 889 socket.on('chat_updated', function (chat) { 890 if (chat.status === 'closed' && state.liveMode) { 891 hideTyping(); 892 var msg = "This chat has been closed. Type a message below to chat with our AI guide again."; 893 addSystem(msg); 894 state.messages.push({ role: 'system', content: msg }); 895 state.liveMode = false; 896 saveState(); 897 humanBtn.style.display = 'inline-flex'; 898 humanBtn.disabled = false; 899 } 900 }); 901 }); 902 } 903 904 function sendLiveMessage(content) { 905 input.value = ''; input.style.height = 'auto'; 906 // Do NOT show typing here â typing indicator is for the AGENT, not the visitor 907 fetch(API_BASE + "/livechat/" + state.chatId + "/message", { 908 method: 'POST', headers: { 'Content-Type': 'application/json' }, 909 body: JSON.stringify({ sender: 'visitor', text: content }) 910 }); 911 } 912})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.