1(function() { 2 if (document.readyState === "loading") { 3 document.addEventListener("DOMContentLoaded", initLiveDesk); 4 } else { 5 initLiveDesk(); 6 } 7 8 function initLiveDesk() { 9 var EMBED_TOKEN = "WS4sQCBUnXHPs0CVvEGcEf9I59tv4SOwVmt7Xz3l"; 10 var API_BASE = "https://service.ictdesk.net/api"; 11 var WIDGET_COLOR = "#4F46E5"; 12 var WIDGET_TITLE = "Chat with us"; 13 var WELCOME_MSG = "Hi! How can we help you today?"; 14 var REVERB_HOST = "service.ictdesk.net"; 15 var REVERB_PORT = 443; 16 var REVERB_KEY = "livedesk-key"; 17 18 var LS_KEY = "livedesk_uuid_" + EMBED_TOKEN; 19 var LS_SESSION = "livedesk_session_" + EMBED_TOKEN; 20 var LS_MODE = "livedesk_mode_" + EMBED_TOKEN; 21 var LS_SOUND = "livedesk_sound_" + EMBED_TOKEN; 22 var LS_DEPT = "livedesk_dept_" + EMBED_TOKEN; 23 24 var departments = []; 25 var selectedDeptId = null; 26 27 function getUUID() { 28 var id = localStorage.getItem(LS_KEY); 29 if (!id) { 30 id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { 31 var r = Math.random() * 16 | 0; 32 return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); 33 }); 34 localStorage.setItem(LS_KEY, id); 35 } 36 return id; 37 } 38 39 var visitorUUID = getUUID(); 40 var sessionId = localStorage.getItem(LS_SESSION) || null; 41 var chatMode = localStorage.getItem(LS_MODE) || null; 42 var soundEnabled = localStorage.getItem(LS_SOUND) !== "off"; 43 var isOpen = false; 44 var isFullscreen = false; 45 var pollTimer = null; 46 var waitPollTimer = null; 47 var typingTimer = null; 48 var echoInst = null; 49 var lastMsgId = 0; 50 var unreadCount = 0; 51 var agentsOnline = 0; 52 var widgetReady = false; 53 var typEl; 54 var currentAgentInfo = null; 55 var companyName = WIDGET_TITLE; 56 var menuOpen = false; 57 var isAssigned = false; 58 var agentTimeoutTimer = null; 59 var leadCaptured = false; 60 var leadSkipped = false; 61 var aiEnabled = true; 62 var showBranding = false; 63 var widgetTemplate = 1; 64 var widgetPosition = "bottom-right"; 65 var widgetBorderRadius = "16"; 66 var widgetWidth = "380"; 67 var widgetHeight = "560"; 68 69 var c = WIDGET_COLOR; 70 71 /* ââ Notification Sound ââ */ 72 var audioCtx = null; 73 function playNotifSound() { 74 if (!soundEnabled) return; 75 try { 76 if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 77 var osc = audioCtx.createOscillator(); 78 var gain = audioCtx.createGain(); 79 osc.connect(gain); gain.connect(audioCtx.destination); 80 osc.frequency.setValueAtTime(880, audioCtx.currentTime); 81 osc.frequency.setValueAtTime(660, audioCtx.currentTime + 0.1); 82 gain.gain.setValueAtTime(0.3, audioCtx.currentTime); 83 gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.35); 84 osc.start(audioCtx.currentTime); 85 osc.stop(audioCtx.currentTime + 0.35); 86 } catch(e) {} 87 } 88 89 /* ââ Styles ââ */ 90 var style = document.createElement("style"); 91 style.textContent = [ 92 "#ld-bubble{position:fixed;bottom:24px;right:24px;width:58px;height:58px;border-radius:50%;background:"+c+";box-shadow:0 4px 20px rgba(0,0,0,.28);cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2147483647;transition:transform .2s;}", 93 "#ld-bubble:hover{transform:scale(1.1);}", 94 "#ld-bubble>svg{fill:#fff;width:26px;height:26px;}", 95 "#ld-badge{position:absolute;top:-3px;right:-3px;background:#ef4444;color:#fff;border-radius:50%;min-width:18px;height:18px;font-size:11px;display:none;align-items:center;justify-content:center;font-weight:700;padding:0 3px;border:2px solid #fff;}", 96 "#ld-panel{position:fixed;bottom:96px;right:24px;width:360px;height:min(560px,calc(100vh - 120px));background:#fff;border-radius:18px;box-shadow:0 10px 48px rgba(0,0,0,.2);display:none;flex-direction:column;z-index:2147483646;overflow:hidden;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;animation:ldUp .25s ease;}", 97 "#ld-panel.ld-open{display:flex;}", 98 "#ld-panel.ld-fullscreen{position:fixed!important;top:0!important;left:0!important;right:0!important;bottom:0!important;width:100vw!important;height:100vh!important;max-height:100vh!important;border-radius:0!important;}", 99 "@keyframes ldUp{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}", 100 "#ld-head{background:"+c+";color:#fff;padding:12px 16px;display:flex;align-items:center;gap:10px;flex-shrink:0;position:relative;}", 101 "#ld-head-av{width:40px;height:40px;border-radius:50%;background:rgba(255,255,255,.25);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px;font-weight:700;color:#fff;letter-spacing:.5px;}", 102 "#ld-head-info{flex:1;min-width:0;}", 103 "#ld-head-title{font-weight:700;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}", 104 "#ld-head-sub{font-size:11px;opacity:.85;margin-top:1px;}", 105 "#ld-menu-btn{background:none;border:none;color:rgba(255,255,255,.85);
105cursor:pointer;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s;padding:0;}", 106 "#ld-menu-btn:hover{background:rgba(255,255,255,.2);}", 107 "#ld-menu-btn svg{fill:rgba(255,255,255,.9);width:18px;height:18px;}", 108 "#ld-expand-btn{background:none;border:none;color:rgba(255,255,255,.85);cursor:pointer;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s;padding:0;}", 109 "#ld-expand-btn:hover{background:rgba(255,255,255,.2);}", 110 "#ld-expand-btn svg{fill:rgba(255,255,255,.9);width:18px;height:18px;}", 111 "#ld-close-x{background:none;border:none;color:rgba(255,255,255,.85);cursor:pointer;font-size:22px;line-height:1;padding:0 0 0 4px;flex-shrink:0;}", 112 "#ld-dropdown{position:absolute;top:56px;right:12px;background:#fff;border-radius:12px;box-shadow:0 6px 30px rgba(0,0,0,.15);min-width:190px;z-index:10;overflow:hidden;display:none;}", 113 "#ld-dropdown.show{display:block;}", 114 ".ld-dd-item{display:flex;align-items:center;gap:10px;padding:11px 16px;font-size:13px;color:#374151;cursor:pointer;border:none;background:none;width:100%;text-align:left;transition:background .12s;}", 115 ".ld-dd-item:hover{background:#f3f4f6;}", 116 ".ld-dd-item svg{width:15px;height:15px;fill:#6b7280;flex-shrink:0;}", 117 ".ld-dd-sep{height:1px;background:#f0f0f0;margin:3px 0;}", 118 ".ld-dd-danger{color:#ef4444!important;}", 119 ".ld-dd-danger svg{fill:#ef4444!important;}", 120 "#ld-sw-bar{display:none;padding:7px 10px;background:#f8fafc;border-bottom:1px solid #e9ecef;align-items:center;justify-content:space-between;flex-shrink:0;gap:6px;}", 121 "#ld-sw-bar.show{display:flex;}", 122 ".ld-sw-pill{display:flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;border-radius:20px;padding:5px 12px;cursor:pointer;transition:all .15s;border:1px solid;white-space:nowrap;background:none;}", 123 ".ld-sw-mode{color:"+c+";background:#fff;border-color:"+c+";}", 124 ".ld-sw-mode:hover{background:"+c+";color:#fff;}", 125 ".ld-sw-email{color:#6b7280;background:#f3f4f6;border-color:#e5e7eb;}", 126 ".ld-sw-email:hover{background:#e5e7eb;color:#374151;}", 127 "#ld-msgs{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px;background:#f9fafb;}", 128 ".ld-msg{max-width:82%;padding:9px 14px;border-radius:16px;line-height:1.55;word-break:break-word;font-size:13.5px;}", 129 ".ld-msg.visitor{align-self:flex-end;background:"+c+";color:#fff;border-bottom-right-radius:4px;}", 130 ".ld-msg.agent,.ld-msg.ai,.ld-msg.bot{align-self:flex-start;background:#fff;color:#1f2937;border:1px solid #e5e7eb;border-bottom-left-radius:4px;}", 131 ".ld-msg.ai::before,.ld-msg.bot::before{content:'ð¤ ';font-size:12px;}", 132 ".ld-msg.system{align-self:center;background:#e9ecef;color:#6b7280;font-size:12px;padding:5px 14px;border-radius:20px;max-width:90%;text-align:center;}", 133 ".ld-msg.pending{opacity:0.55;}", 134 ".ld-msg a{color:inherit;text-decoration:underline;word-break:break-all;}", 135 ".ld-msg.agent a,.ld-msg.ai a,.ld-msg.bot a{color:#3b82f6!important;text-decoration:underline;}", 136 ".ld-msg.visitor a{color:rgba(255,255,255,0.92)!important;}", 137 "#ld-typing{align-self:flex-start;background:#fff;border:1px solid #e5e7eb;border-radius:16px;border-bottom-left-radius:4px;padding:10px 14px;display:none;gap:5px;align-items:center;}", 138 "#ld-typing span{display:inline-block;width:7px;height:7px;border-radius:50%;background:#9ca3af;animation:ldBounce 1.3s infinite;}", 139 "#ld-typing span:nth-child(2){animation-delay:.15s}#ld-typing span:nth-child(3){animation-delay:.3s}", 140 "@keyframes ldBounce{0%,60%,100%{transform:scale(0)}30%{transform:scale(1)}}", 141 "#ld-foot{padding:10px 12px;border-top:1px solid #e5e7eb;display:flex;gap:8px;align-items:flex-end;background:#fff;flex-shrink:0;}", 142 "#ld-attach-btn{background:none;border:none;color:#9ca3af;cursor:pointer;padding:4px;border-radius:6px;transition:color .15s;flex-shrink:0;margin-bottom:2px;}", 143 "#ld-attach-btn:hover{color:"+c+";}", 144 "#ld-attach-btn svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}", 145 ".ld-msg-file{display:flex;align-items:center;gap:8px;padding:4px 0;}", 146 ".ld-msg-file svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;flex-shrink:0;}", 147 ".ld-msg-file a{color:inherit;text-decoration:underline;font-size:12.5px;word-break:break-all;}", 148 ".ld-msg img.ld-img-preview{max-width:100%;border-radius:8px;margin-top:4px;cursor:pointer;}", 149 "#ld-input{flex:1;border:1px solid #d1d5db;border-radius:22px;padding:9px 15px;outline:none;font-size:13.5px;resize:none;font-family:inherit;line-height:1.4;max-height:80px;overflow-y:auto;}", 150 "#ld-input:focus{border-color:"+c+";}", 151 "#ld-send-x{width:38px;height:38px;border-radius:50%;background:"+c+";border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:opacity .2s;}", 152 "#ld-send-x:disabled{opacity:.45;cursor:default;}", 153 "#ld-send-x svg{fill:#fff;width:17px;height:17px;}", 154 "#ld-mode-screen{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;gap:13px;background:#f9fafb;overflow-y:auto;}", 155 ".ld-mode-intro{font-size:13.5px;color:#4b5563;text-align:center;margin:0;line-height:1.6;}", 156 ".ld-mode-btn{width:100%;padding:13px 18px;border-radius:14px;border:2px solid "+c+";background:#fff;cursor:pointer;display:flex;align-items:center;gap:12px;font-size:13.5px;font-weight:600;color:#374151;transition:all .18s;text-align:left;}", 157 ".ld-mode-btn:hover{background:"+c+";color:#fff;}", 158 ".ld-mode-btn .ld-icon{font-size:20px;line-height:1;flex-shrink:0;}", 159 ".ld-btn-sub{font-size:11.5px;font-weight:400;opacity:.75;margin-top:2px;}", 160 ".ld-mode-btn.email-btn{border-color:#d1d5db;}", 161 ".ld-mode-btn.email-btn:hover{background:#6b7280;color:#fff;}", 162 "#ld-wait-screen{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px;gap:16px;background:#f9fafb;}", 163 ".ld-wait-spinner{width:44px;height:44px;border:4px solid #e5e7eb;border-top-color:"+c+";border-radius:50%;animation:ldSpin .9s linear infinite;}", 164 "@keyframes ldSpin{to{transform:rotate(360deg)}}", 165 "#ld-wait-screen p{font-size:13.5px;color:#4b5563;text-align:center;margin:0;line-height:1.6;}", 166 "#ld-wait-switch{background:none;border:1px solid #d1d5db;border-radius:10px;padding:8px 18px;font-size:12.5px;color:#6b7280;cursor:pointer;transition:all .15s;}", 167 "#ld-wait-switch:hover{border-color:"+c+";color:"+c+";}", 168 "#ld-offline-form{padding:18px;background:#fff;flex:1;display:flex;flex-direction:column;gap:10px;overflow-y:auto;}", 169 "#ld-offline-form select{border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;outline:none;font-family:inherit;background:#fff;color:#374151;width:100%;box-sizing:border-box;}", 170 "#ld-rating-screen{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px 24px;gap:14px;background:#f9fafb;}", 171 ".ld-stars{display:flex;gap:8px;
171cursor:pointer;}", 172 ".ld-star{font-size:32px;color:#d1d5db;transition:color .15s;cursor:pointer;line-height:1;-webkit-user-select:none;user-select:none;}", 173 ".ld-star.active{color:#f59e0b;}", 174 "#ld-rating-comment{width:100%;border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;resize:none;outline:none;font-family:inherit;box-sizing:border-box;}", 175 "#ld-rating-comment:focus{border-color:"+c+";}", 176 ".ld-rating-submit{width:100%;padding:12px;border-radius:11px;border:none;background:"+c+";color:#fff;font-size:13.5px;font-weight:600;cursor:pointer;}", 177 ".ld-rating-submit:disabled{opacity:.5;cursor:default;}", 178 ".ld-rating-skip{background:none;border:none;font-size:12px;color:#9ca3af;cursor:pointer;text-decoration:underline;}", 179 "#ld-thankyou-screen{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:28px;gap:14px;background:#f9fafb;text-align:center;}", 180 "#ld-thankyou-screen .ld-ty-icon{font-size:48px;}", 181 "#ld-thankyou-screen h3{margin:0;font-size:17px;color:#111827;}", 182 "#ld-thankyou-screen p{margin:0;font-size:13px;color:#6b7280;line-height:1.6;}", 183 "#ld-transcript-row{display:flex;gap:6px;padding:10px 16px;border-top:1px solid #f0f0f0;background:#fff;flex-shrink:0;}", 184 "#ld-transcript-email{flex:1;border:1px solid #d1d5db;border-radius:8px;padding:7px 11px;font-size:12.5px;outline:none;font-family:inherit;}", 185 "#ld-transcript-email:focus{border-color:"+c+";}", 186 "#ld-transcript-send{padding:7px 12px;border-radius:8px;background:"+c+";color:#fff;border:none;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;}", 187 "#ld-dept-screen{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:20px;gap:10px;background:#f9fafb;overflow-y:auto;}", 188 ".ld-dept-btn{width:100%;padding:12px 16px;border-radius:12px;border:1.5px solid #e5e7eb;background:#fff;cursor:pointer;display:flex;align-items:center;gap:10px;font-size:13.5px;color:#374151;transition:all .15s;text-align:left;}", 189 ".ld-dept-btn:hover{border-color:"+c+";background:#f5f3ff;color:"+c+";}", 190 ".ld-dept-intro{font-size:13px;color:#6b7280;text-align:center;margin:0;}", 191 ".ld-dept-skip{background:none;border:none;font-size:12px;color:#9ca3af;cursor:pointer;text-decoration:underline;margin-top:4px;}", 192 "#ld-end-bar{display:none !important;}", 193 "@media(max-width:480px){#ld-panel{width:calc(100vw - 24px);right:12px;bottom:80px;height:min(480px,calc(100vh - 100px));}}" 194 ].join(""); 195 document.head.appendChild(style); 196 197 /* ââ DOM ââ */ 198 var bubble = document.createElement("div"); 199 bubble.id = "ld-bubble"; 200 bubble.innerHTML = '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg><div id="ld-badge"></div>'; 201 document.body.appendChild(bubble); 202 203 var EXPAND_ICON = '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>'; 204 var SHRINK_ICON = '<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>'; 205 206 var panel = document.createElement("div"); 207 panel.id = "ld-panel"; 208 panel.innerHTML = 209 '<div id="ld-head">' + 210 '<div id="ld-head-av"><svg viewBox="0 0 24 24" fill="white" width="20" height="20"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg></div>' + 211 '<div id="ld-head-info"><div id="ld-head-title">' + WIDGET_TITLE + '</div><div id="ld-head-sub">We reply quickly</div></div>' + 212 '<button id="ld-menu-btn" title="Options"><svg viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg></button>' + 213 '<button id="ld-expand-btn" title="Expand to full screen">' + EXPAND_ICON + '</button>' + 214 '<button id="ld-close-x">×</button>' + 215 '<div id="ld-dropdown">' + 216 '<button class="ld-dd-item" id="ld-dd-sound"><svg viewBox="0 0 24 24">
216<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg><span id="ld-dd-sound-lbl">Sound: On</span></button>' + 217 '<button class="ld-dd-item" id="ld-dd-transcript"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>Email Transcript</button>' + 218 '<div class="ld-dd-sep"></div>' + 219 '<button class="ld-dd-item ld-dd-danger" id="ld-dd-endchat"><svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>End Chat</button>' + 220 '</div>' + 221 '</div>' + 222 '<div id="ld-sw-bar">' + 223 '<button class="ld-sw-pill ld-sw-mode" id="ld-sw-mode-btn"></button>' + 224 '<button class="ld-sw-pill ld-sw-email" id="ld-sw-email-btn">ð© Email</button>' + 225 '</div>' + 226 '<div id="ld-msgs"><div id="ld-typing"><span></span><span></span><span></span></div></div>' + 227 '<div id="ld-end-bar"><button id="ld-end-btn" title="End Chat"><svg viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>' + 228 '<div id="ld-foot">' + 229 '<input type="file" id="ld-file-input" style="display:none" accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.txt,.zip" />' + 230 '<button id="ld-attach-btn" title="Attach file"><svg viewBox="0 0 24 24"><path d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg></button>' + 231 '<textarea id="ld-input" rows="1" placeholder="Type a message..."></textarea>' + 232 '<button id="ld-send-x" disabled><svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button>' + 233 '</div>'; 234 document.body.appendChild(panel); 235 236 var msgsEl = panel.querySelector("#ld-msgs"); 237 typEl = panel.querySelector("#ld-typing"); 238 var inputEl = panel.querySelector("#ld-input"); 239 var sendBtn = panel.querySelector("#ld-send-x"); 240 var badgeEl = bubble.querySelector("#ld-badge"); 241 var closeX = panel.querySelector("#ld-close-x"); 242 var footEl = panel.querySelector("#ld-foot"); 243 var swBar = panel.querySelector("#ld-sw-bar"); 244 var swModeBtn = panel.querySelector("#ld-sw-mode-btn"); 245 var swEmailBtn = panel.querySelector("#ld-sw-email-btn"); 246 var menuBtn = panel.querySelector("#ld-menu-btn"); 247 var dropdown = panel.querySelector("#ld-dropdown"); 248 var ddSound = panel.querySelector("#ld-dd-sound"); 249 var ddSoundLbl = panel.querySelector("#ld-dd-sound-lbl"); 250 var ddTranscript = panel.querySelector("#ld-dd-transcript"); 251 var ddEndChat = panel.querySelector("#ld-dd-endchat"); 252 var endBar = panel.querySelector("#ld-end-bar"); 253 var headAv = panel.querySelector("#ld-head-av"); 254 var headTitle = panel.querySelector("#ld-head-title"); 255 var headSub = panel.querySelector("#ld-head-sub"); 256 var expandBtn = panel.querySelector("#ld-expand-btn"); 257 258 /* ââ HTTP helpers ââ */ 259 function post(url, data, cb) { 260 fetch(API_BASE + url, { 261 method:"POST", headers:{"Content-Type":"application/json","Accept":"application/json"}, 262 body:JSON.stringify(data) 263 }).then(function(r){ var s=r.status; return r.json().then(function(d){d.__s=s;return d;}); }) 264 .then(cb || function(){}).catch(function(e){ console.warn("[LD]", e); }); 265 } 266 function get(url, cb) { 267 fetch(API_BASE + url, {headers:{"Accept":"application/json"}}) 268 .then(function(r){ return r.json(); }).then(cb).catch(function(e){ console.warn("[LD]", e); }); 269 } 270 271 /* ââ Fullscreen toggle ââ */ 272 function toggleFullscreen() { 273 isFullscreen = !isFullscreen; 274 if (isFullscreen) { 275 panel.classList.add("ld-fullscreen"); 276 expandBtn.innerHTML = SHRINK_ICON; 277 expandBtn.title = "Shrink to original size"; 278 } else { 279 panel.classList.remove("ld-fullscreen"); 280 expandBtn.innerHTML = EXPAND_ICON; 281 expandBtn.title = "Expand to full screen"; 282 applyWidgetStyles(); 283 } 284 } 285 expandBtn.addEventListener("click", toggleFullscreen); 286 287 /* ââ Header ââ */ 288 function updateHeader(agentInfo) { 289 headTitle.textContent = companyName || WIDGET_TITLE; 290 if (agentInfo && agentInfo.name) { 291 currentAgentInfo = agentInfo; 292 headSub.textContent = "Chatting with " + agentInfo.name; 293 headSub.style.color = "#e0f2fe"; 294 if (agentInfo.avatar_url) { 295 headAv.innerHTML = '<img src="' + agentInfo.avatar_url + '" style="width:40px;height:40px;border-radius:50%;object-fit:cover;" />'; 296 } else { 297 var nameParts = agentInfo.name.trim().split(/\s+/); 298 var initials = nameParts.length >= 2 299 ? (nameParts[0][0] + nameParts[nameParts.length - 1][0]).toUpperCase() 300 : nameParts[0].substring(0, 2).toUpperCase(); 301 headAv.textContent = initials; 302 headAv.style.fontSize = "14px"; 303 headAv.style.fontWeight = "700"; 304 } 305 } else { 306 currentAgentInfo = null; 307 headAv.textContent = ""; 308 headAv.innerHTML = '<svg viewBox="0 0 24 24" fill="white" width="20" height="20"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>'; 309 if (agentsOnline > 0) { 310 headSub.textContent = agentsOnline + " agent" + (agentsOnline === 1 ? "" : "s") + " online"; 311 headSub.style.color = "#86efac"; 312 } else { 313 headSub.textContent = "We reply quickly"; 314 headSub.style.color = ""; 315 } 316 } 317 } 318 319 /* ââ Message helpers ââ */ 320 function formatMessage(text) { 321 var s = String(text) 322 .replace(/&/g, '&') 323 .replace(/</g, '<') 324 .replace(/>/g, '>') 325 .replace(/"/g, '"'); 326 s = s.replace(/(https?:\/\/[^\s<>"&]+)/g, 327 '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'); 328 s = s.replace(/([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/g, function(match, p1, offset, str) { 329 var before = str.substring(0, offset); 330 var opens = (before.match(/</g) || []).length; 331 var closes = (before.match(/>/g) || []).length; 332 if (opens > closes) return match; 333 return '<a href="mailto:' + match + '">' + match + '</a>'; 334 }); 335 s = s.replace(/\n/g, '<br>'); 336 return s; 337 } 338 339 function appendMsg(type, text, id, agentInfo) { 340 if (id && msgsEl.querySelector('[data-ld="' + id + '"]')) return false;
341 var isAgent = (type === "agent"); 342 var d = document.createElement("div"); 343 d.className = "ld-msg " + type; 344 if (id) d.setAttribute("data-ld", id); 345 if (text && text.indexOf("[image]") === 0) { 346 var imgUrl = text.replace("[image]", ""); 347 var a = document.createElement("a"); a.href = imgUrl; a.target = "_blank"; 348 var img = document.createElement("img"); img.src = imgUrl; img.className = "ld-img-preview"; 349 a.appendChild(img); d.appendChild(a); 350 } else if (text && text.indexOf("[file]") === 0) { 351 var parts = text.replace("[file]", "").split("|"); 352 var fname = parts[0]; var furl = parts[1] || parts[0]; 353 var fw = document.createElement("div"); fw.className = "ld-msg-file"; 354 fw.innerHTML = '<svg viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg><a href="' + furl + '" target="_blank">' + fname + "</a>"; 355 d.appendChild(fw); 356 } else { 357 d.innerHTML = formatMessage(text); 358 } 359 if (isAgent) { 360 var info = agentInfo || currentAgentInfo; 361 var wrapper = document.createElement("div"); 362 wrapper.style.cssText = "display:flex;flex-direction:column;align-items:flex-start;gap:3px;"; 363 var agRow = document.createElement("div"); 364 agRow.style.cssText = "display:flex;align-items:center;gap:5px;"; 365 var av = document.createElement("div"); 366 av.style.cssText = "width:22px;height:22px;border-radius:50%;background:#e0e7ff;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:#4f46e5;flex-shrink:0;overflow:hidden;"; 367 if (info && info.avatar_url) { 368 av.innerHTML = '<img src="' + info.avatar_url + '" style="width:100%;height:100%;object-fit:cover;" />'; 369 } else { 370 var avParts = info && info.name ? info.name.trim().split(/\s+/) : ["A"]; 371 av.textContent = (avParts.length >= 2 ? avParts[0][0] + avParts[avParts.length-1][0] : (avParts[0] ? avParts[0].substring(0,2) : "A")).toUpperCase(); 372 } 373 var agName = document.createElement("span"); 374 agName.style.cssText = "font-size:10px;color:#9ca3af;font-weight:600;"; 375 agName.textContent = (info && info.name) ? info.name + (info.title ? " ⢠" + info.title : "") : "Agent"; 376 agRow.appendChild(av); agRow.appendChild(agName); 377 wrapper.appendChild(agRow); wrapper.appendChild(d); 378 msgsEl.insertBefore(wrapper, typEl); 379 } else { 380 msgsEl.insertBefore(d, typEl); 381 } 382 msgsEl.scrollTop = msgsEl.scrollHeight; 383 return true; 384 } 385 386 function showTyping() { 387 typEl.style.display = "flex"; msgsEl.scrollTop = msgsEl.scrollHeight; 388 clearTimeout(typingTimer); 389 typingTimer = setTimeout(function() { typEl.style.display = "none"; }, 4000); 390 } 391 392 /* ââ Menu / Dropdown ââ */ 393 ddSoundLbl.textContent = "Sound: " + (soundEnabled ? "On" : "Off"); 394 395 function toggleMenu(e) { 396 if (e) e.stopPropagation(); 397 menuOpen = !menuOpen; 398 dropdown.classList.toggle("show", menuOpen); 399 } 400 function closeMenu() { 401 menuOpen = false; 402 dropdown.classList.remove("show"); 403 } 404 405 ddSound.addEventListener("click", function() { 406 soundEnabled = !soundEnabled; 407 localStorage.setItem(LS_SOUND, soundEnabled ? "on" : "off"); 408 ddSoundLbl.textContent = "Sound: " + (soundEnabled ? "On" : "Off"); 409 closeMenu(); 410 }); 411 412 ddTranscript.addEventListener("click", function(e) { 413 e.stopPropagation(); 414 if (!sessionId) { alert("No active chat session to send transcript."); closeMenu(); return; } 415 var existing = document.getElementById("ld-transcript-row"); 416 if (existing) { existing.remove(); closeMenu(); return; } 417 dropdown.classList.remove("show"); 418 menuOpen = false; 419 var row = document.createElement("div"); 420 row.id = "ld-transcript-row"; 421 row.innerHTML = '<input type="email" id="ld-transcript-email" placeholder="Your email address" /><button id="ld-transcript-send">Send</button>'; 422 var swBarEl = panel.querySelector("#ld-sw-bar"); 423 panel.insertBefore(row, swBarEl.nextSibling); 424 var emailIn = row.querySelector("#ld-transcript-email"); 425 emailIn.focus(); 426 row.querySelector("#ld-transcript-send").addEventListener("click", function() { 427 var em = emailIn.value.trim(); 428 if (!em) { emailIn.style.borderColor = "#ef4444"; return; } 429 this.disabled = true; this.textContent = "Sending..."; 430 post("/chat/" + sessionId + "/transcript", {embed_token: EMBED_TOKEN, email: em}, function(d) { 431 row.remove(); 432 appendMsg("system", d.message || "Transcript sent! Check your email."); 433 }); 434 }); 435 }); 436 437 ddEndChat.addEventListener("click", function() { 438 closeMenu(); 439 if (!sessionId) return; 440 if (!confirm("End this chat session?")) return; 441 var sid = sessionId; 442 post("/chat/" + sid + "/close", {embed_token: EMBED_TOKEN}, function() { 443 stopPoll(); stopWaitPoll(); 444 if (echoInst) { try { echoInst.disconnect(); } catch(e) {} echoInst = null; } 445 var trRow = document.getElementById("ld-transcript-row"); 446 if (trRow) trRow.remove(); 447 appendMsg("system", "You ended the chat."); 448 showRejoinScreen(sid, "visitor"); 449 }); 450 }); 451 452 /* ââ Rejoin Screen ââ */ 453 function showRejoinScreen(sid, closedBy) { 454 hideSwitchBar(); 455 footEl.style.display = "none"; 456 endBar.classList.remove("show"); 457 msgsEl.style.display = ""; 458 var existing = document.getElementById("ld-rejoin-notice"); 459 if (existing) existing.remove(); 460 var notice = document.createElement("div"); 461 notice.id = "ld-rejoin-notice"; 462 notice.style.cssText = "align-self:center;background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:14px 16px;max-width:88%;text-align:center;margin:6px auto;"; 463 var label = closedBy === "agent" ? "The agent has ended this chat." : (closedBy === "tenant" ? "The chat has ended." : "You ended this chat."); 464 notice.innerHTML = 465 '<p style="margin:0 0 10px;font-size:13px;color:#374151;">' + label + '</p>' + 466 '<button id="ld-rejoin-btn" style="background:' + c + ';color:#fff;border:none;border-radius:10px;padding:9px 18px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;margin-right:6px;">↻ Rejoin Chat</button>' + 467 '<button id="ld-rejoin-rate" style="background:none;border:1px solid #e5e7eb;border-radius:10px;padding:8px 14px;font-size:12.5px;color:#6b7280;cursor:pointer;font-family:inherit;">★ Rate & End</button>'; 468 msgsEl.appendChild(notice); 469 msgsEl.scrollTop = msgsEl.scrollHeight; 470 notice.querySelector("#ld-rejoin-btn").addEventListener("click", function() { 471 this.disabled = true; this.textContent = "Rejoining..."; 472 post("/chat/" + sid + "/rejoin", {embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID}, function(data) { 473 if (data && data.session_id) { 474 sessionId = data.session_id; 475 chatMode = data.chat_mode || chatMode; 476 localStorage.setItem(LS_SESSION, sessionId); 477 localStorage.setItem(LS_MODE, chatMode); 478 notice.remove(); 479 if (data.agent) updateHeader(data.agent); 480 showChatUI(); updateSwitchBar(); 481 loadMessages(); connectEcho(); startPoll(); 482 } else { 483 alert((data && data.error) || "Could not rejoin the chat."); 484 notice.querySelector("#ld-rejoin-btn").disabled = false;
485 notice.querySelector("#ld-rejoin-btn").innerHTML = "↻ Rejoin Chat"; 486 } 487 }); 488 }); 489 notice.querySelector("#ld-rejoin-rate").addEventListener("click", function() { 490 notice.remove(); 491 resetSession(); 492 showRatingScreen(sid); 493 }); 494 } 495 496 panel.querySelector("#ld-end-btn").addEventListener("click", function() { ddEndChat.click(); }); 497 swEmailBtn.addEventListener("click", function() { ddTranscript.click(); }); 498 menuBtn.addEventListener("click", toggleMenu); 499 document.addEventListener("click", function(e) { 500 if (menuOpen && !dropdown.contains(e.target) && e.target !== menuBtn && !menuBtn.contains(e.target)) closeMenu(); 501 }); 502 503 /* ââ Switch bar ââ */ 504 function updateSwitchBar() { 505 if (!chatMode) { swBar.classList.remove("show"); return; } 506 swBar.classList.add("show"); 507 if (chatMode === "ai") { 508 swModeBtn.style.display = ""; 509 swModeBtn.textContent = "\u{1F4AC} Switch to Agent"; 510 swModeBtn.onclick = function() { switchChatMode("agent"); }; 511 } else { 512 if (aiEnabled) { 513 swModeBtn.style.display = ""; 514 swModeBtn.textContent = "\u{1F916} Switch to AI"; 515 swModeBtn.onclick = function() { switchChatMode("ai"); }; 516 } else { 517 swModeBtn.style.display = "none"; 518 } 519 } 520 } 521 function hideSwitchBar() { swBar.classList.remove("show"); } 522 523 function switchChatMode(newMode) { 524 if (!sessionId) { pickMode(newMode); return; } 525 if (newMode === chatMode) return; 526 post("/chat/" + sessionId + "/switch-mode", {embed_token: EMBED_TOKEN, chat_mode: newMode}, function(data) { 527 if (!data || data.error) return; 528 chatMode = newMode; 529 localStorage.setItem(LS_MODE, newMode); 530 cancelAgentTimeout(); 531 var waitEl = document.getElementById("ld-wait-screen"); 532 if (waitEl) { waitEl.remove(); msgsEl.style.display = ""; showChatUI(); } 533 var noAgentEl = document.getElementById("ld-no-agent-notice"); 534 if (noAgentEl) noAgentEl.remove(); 535 updateSwitchBar(); 536 if (newMode === "ai") { 537 stopWaitPoll(); 538 appendMsg("system", "ð¤ Switched to AI mode"); 539 } else { 540 appendMsg("system", "ð¬ Connecting you with an agentâ¦"); 541 if (!isAssigned) { startWaitPoll(); startAgentTimeout(); } 542 } 543 }); 544 } 545 546 /* ââ Department Selection ââ */ 547 function fetchDepartments(cb) { 548 get("/departments/" + EMBED_TOKEN, function(data) { 549 departments = (data && data.departments) ? data.departments : []; 550 if (typeof cb === "function") cb(departments); 551 }); 552 } 553 554 function showDeptScreen(mode) { 555 clearScreens(); hideSwitchBar(); 556 footEl.style.display = "none"; 557 msgsEl.style.display = "none"; 558 var screen = document.createElement("div"); 559 screen.id = "ld-dept-screen"; 560 var html = "<p class='ld-dept-intro'><strong>Select a department</strong><br>Choose the team you want to chat with:</p>"; 561 departments.forEach(function(d) { 562 html += "<button class='ld-dept-btn' data-id='" + d.id + "'>" + 563 "<svg width='18' height='18' fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'><path stroke-linecap='round' stroke-linejoin='round' d='M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'/></svg>" + 564 "<div><div style='font-weight:600'>" + d.name + "</div></div></button>"; 565 }); 566 html += "<button class='ld-dept-skip'>Skip → No preference</button>"; 567 screen.innerHTML = html; 568 panel.insertBefore(screen, footEl); 569 screen.querySelectorAll(".ld-dept-btn").forEach(function(btn) { 570 btn.onclick = function() { 571 selectedDeptId = parseInt(btn.getAttribute("data-id"), 10); 572 localStorage.setItem(LS_DEPT, selectedDeptId); 573 clearScreens(); 574 pickMode(mode); 575 }; 576 }); 577 screen.querySelector(".ld-dept-skip").onclick = function() { 578 selectedDeptId = null; 579 localStorage.removeItem(LS_DEPT); 580 clearScreens(); 581 pickMode(mode); 582 }; 583 } 584 585 function showModeScreen() { 586 clearScreens(); hideSwitchBar(); 587 footEl.style.display = "none"; 588 msgsEl.style.display = "none"; 589 updateHeader(null); 590 if (!aiEnabled) { 591 if (departments.length > 1) { showDeptScreen("agent"); } else { pickMode("agent"); } 592 return; 593 } 594 var screen = document.createElement("div"); 595 screen.id = "ld-mode-screen"; 596 screen.innerHTML = 597 '<p class="ld-mode-intro"><strong>Hi there! ð</strong><br>
597How can we help you today?</p>' + 598 '<button class="ld-mode-btn" id="ld-btn-ai"><span class="ld-icon">ð¤</span><div><div>Chat with AI Assistant</div><div class="ld-btn-sub">Instant answers, 24/7</div></div></button>' + 599 '<button class="ld-mode-btn" id="ld-btn-agent"><span class="ld-icon">ð¬</span><div><div>Chat with a Human Agent</div><div class="ld-btn-sub">Connect with our support team</div></div></button>' + 600 '<button class="ld-mode-btn email-btn" id="ld-btn-email"><span class="ld-icon">ð©</span><div><div>Send us a Message</div><div class="ld-btn-sub">Get a reply via email</div></div></button>'; 601 panel.insertBefore(screen, footEl); 602 screen.querySelector("#ld-btn-ai").onclick = function() { pickMode("ai"); }; 603 screen.querySelector("#ld-btn-agent").onclick = function() { 604 if (agentsOnline > 0) { 605 if (departments.length > 1) { showDeptScreen("agent"); } else { pickMode("agent"); } 606 } else { 607 clearScreens(); showOfflineForm(); 608 } 609 }; 610 screen.querySelector("#ld-btn-email").onclick = function() { 611 clearScreens(); showOfflineForm(); 612 }; 613 } 614 615 function pickMode(mode) { 616 chatMode = mode; 617 localStorage.setItem(LS_MODE, mode); 618 clearScreens(); 619 msgsEl.style.display = ""; 620 621 if (mode === "ai") { 622 startChatSession("ai", function(data) { 623 updateHeader(data.agent || null); 624 showChatUI(); updateSwitchBar(); 625 appendMsg("system", WELCOME_MSG || "Hi! I'm your AI assistant. Ask me anything ð¤"); 626 if (data.messages) data.messages.forEach(function(m) { 627 appendMsg(m.sender_type === "bot" ? "ai" : m.sender_type, m.message, m.id); 628 lastMsgId = Math.max(lastMsgId, m.id); 629 }); 630 connectEcho(); startPoll(); 631 }); 632 } else { 633 startChatSession("agent", function(data) { 634 isAssigned = !!(data.agent); 635 updateHeader(data.agent || null); 636 updateSwitchBar(); 637 if (data.status === "assigned" || agentsOnline > 0) { 638 showChatUI(); 639 appendMsg("system", "Connecting you with an agent..."); 640 if (data.messages) data.messages.forEach(function(m) { 641 appendMsg(m.sender_type === "visitor" ? "visitor" : "agent", m.message, m.id, data.agent); 642 lastMsgId = Math.max(lastMsgId, m.id); 643 }); 644 connectEcho(); startPoll(); 645 if (!isAssigned) { startAgentTimeout(); startWaitPoll(); } 646 maybeFireTrigger(); 647 } else { 648 showWaitScreen(); 649 startAgentTimeout(); 650 maybeFireTrigger(); 651 } 652 }); 653 } 654 } 655 656 /* ââ Waiting Screen ââ */ 657 function showWaitScreen() { 658 clearScreens(); hideSwitchBar(); 659 footEl.style.display = "flex"; 660 msgsEl.style.display = "none"; 661 var screen = document.createElement("div"); 662 screen.id = "ld-wait-screen"; 663 screen.innerHTML = 664 '<div class="ld-wait-spinner"></div>' + 665 '<p><strong>Waiting for an agent...</strong><br>Send a message below and an agent will reply shortly.</p>' + 666 (aiEnabled ? '<button id="ld-wait-switch">ð¤ Switch to AI Assistant</button>' : ''); 667 panel.insertBefore(screen, footEl); 668 var wsBtn = screen.querySelector("#ld-wait-switch"); if (wsBtn) wsBtn.onclick = function() { switchChatMode("ai"); }; 669 startWaitPoll(); 670 } 671 672 function startWaitPoll() { 673 stopWaitPoll(); 674 waitPollTimer = setInterval(function() { 675 if (!sessionId) return; 676 get("/chat/" + sessionId + "/status?embed_token=" + EMBED_TOKEN, function(data) { 677 if (data.agent || data.status === "assigned") { 678 stopWaitPoll(); clearScreens(); msgsEl.style.display = ""; 679 isAssigned = true; cancelAgentTimeout(); 680 updateHeader(data.agent || null); 681 showChatUI(); updateSwitchBar(); 682 appendMsg("system", "â " + (data.agent ? data.agent.name : "An agent") + " has joined!"); 683 loadMessages(); connectEcho(); startPoll(); 684 } 685 }); 686 }, 3000); 687 } 688 function stopWaitPoll() { if (waitPollTimer) { clearInterval(waitPollTimer); waitPollTimer = null; } } 689 690 /* ââ Chat UI ââ */ 691 function showChatUI() { 692 footEl.style.display = "flex"; 693 msgsEl.style.display = ""; 694 endBar.classList.add("show"); 695 setTimeout(function() { inputEl.focus(); }, 80); 696 } 697 698 function clearScreens() { 699 ["ld-mode-screen", "ld-dept-screen", "ld-wait-screen", "ld-offline-form", "ld-rating-screen", "ld-thankyou-screen", "ld-busy-screen", "ld-agentleft-screen"].forEach(function(id) { 700 var el = document.getElementById(id); 701 if (el) el.remove(); 702 }); 703 } 704 705 /* ââ Session Management ââ */ 706 function startChatSession(mode, cb) { 707 var startPayload = {embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID, chat_mode: mode}; 708 if (selectedDeptId) startPayload.department_id = selectedDeptId; 709 post("/chat/start", startPayload, function(data) { 710 if (data.session_id) { 711 sessionId = data.session_id; chatMode = data.chat_mode || mode; 712 localStorage.setItem(LS_SESSION, sessionId); 713 localStorage.setItem(LS_MODE, chatMode); 714 if (typeof cb === "function") cb(data); 715 } else if (data.error === "all_agents_busy" || data.__s === 429) { 716 showAgentsBusyScreen(); 717 } 718 }); 719 } 720 721 function loadMessages() { 722 if (!sessionId) return; 723 get("/chat/" + sessionId + "/messages?embed_token=" + EMBED_TOKEN, function(data) { 724 if (!data.messages) return; 725 var msgAgent = data.agent || currentAgentInfo;
726 data.messages.forEach(function(m) { 727 if (m.id > lastMsgId) { 728 // If this is a visitor message and we have a pending element, promote it 729 // instead of inserting a second element (eliminates the 1-2s flicker) 730 if (m.sender_type === "visitor") { 731 var pendingEl = msgsEl.querySelector('.ld-msg.visitor.pending'); 732 if (pendingEl) { 733 pendingEl.setAttribute("data-ld", m.id); 734 pendingEl.classList.remove("pending"); 735 lastMsgId = m.id; 736 return; 737 } 738 } 739 var type = m.sender_type === "visitor" ? "visitor" 740 : (m.sender_type === "bot" ? "ai" 741 : (m.sender_type === "tenant" ? "agent" : m.sender_type)); 742 var senderInfo = m.sender_type === "tenant" ? {name: "Owner"} : msgAgent; 743 var isNew = appendMsg(type, m.message, m.id, senderInfo); 744 if (isNew && m.sender_type !== "visitor") { 745 // Agent or bot replied â cancel the "no agents" timeout immediately 746 if (m.sender_type === "agent" || m.sender_type === "tenant" || m.sender_type === "bot" || m.sender_type === "ai") { 747 isAssigned = true; 748 cancelAgentTimeout(); 749 if (msgAgent && !currentAgentInfo) { currentAgentInfo = msgAgent; updateHeader(msgAgent); } 750 } 751 if (!isOpen) { 752 unreadCount++; 753 badgeEl.textContent = unreadCount; 754 badgeEl.style.display = "flex"; 755 } 756 playNotifSound(); 757 } 758 lastMsgId = m.id; 759 } 760 }); 761 }); 762 } 763 764 /* ââ Send Message ââ */ 765 function handleSend(skipLeadCheck) { 766 var text = inputEl.value.trim(); 767 if (!text || !sessionId) return; 768 769 if (!skipLeadCheck) { 770 if (chatMode === "ai" && !leadCaptured) { showLeadForm(text); return; } 771 if (chatMode === "agent" && !leadCaptured && !leadSkipped) { showLeadForm(text); return; } 772 } 773 774 inputEl.value = ""; inputEl.style.height = "auto"; sendBtn.disabled = true; 775 776 var tempId = "t" + Date.now(); 777 appendMsg("visitor pending", text, tempId); 778 779 if (chatMode === "ai") showTyping(); 780 781 post("/chat/message", { 782 embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID, 783 chat_session_id: sessionId, message: text, sender_type: "visitor" 784 }, function(data) { 785 typEl.style.display = "none"; 786 var tempEl = msgsEl.querySelector('[data-ld="' + tempId + '"]'); 787 if (tempEl && data.message) { 788 // Polling may have already promoted this pending element to the real id 789 var existingReal = msgsEl.querySelector('[data-ld="' + data.message.id + '"]'); 790 if (existingReal) { 791 // Polling got here first â pending was already promoted or a duplicate exists; clean up 792 if (tempEl !== existingReal) tempEl.remove(); 793 } else { 794 tempEl.setAttribute("data-ld", data.message.id); 795 tempEl.classList.remove("pending"); 796 } 797 lastMsgId = Math.max(lastMsgId, data.message.id); 798 } 799 if (data.bot_message) { 800 appendMsg("ai", data.bot_message.message, data.bot_message.id); 801 lastMsgId = Math.max(lastMsgId, data.bot_message.id); 802 playNotifSound(); 803 } 804 }); 805 } 806 807 /* ââ Open/Close ââ */ 808 function openPanel() { 809 isOpen = true; unreadCount = 0; 810 badgeEl.style.display = "none"; 811 panel.classList.add("ld-open"); 812 813 if (sessionId && chatMode) { 814 if (chatMode === "agent") { 815 get("/chat/" + sessionId + "/status?embed_token=" + EMBED_TOKEN, function(data) { 816 if (data.status === "closed") { 817 updateHeader(data.agent || null); 818 msgsEl.style.display = ""; 819 loadMessages(); 820 showRejoinScreen(sessionId, data.closed_by || null); 821 } else if (data.status === "open" && !data.agent) { 822 msgsEl.style.display = "none"; showWaitScreen(); 823 } else { 824 updateHeader(data.agent || null); 825 msgsEl.style.display = ""; 826 showChatUI(); updateSwitchBar(); loadMessages(); connectEcho(); startPoll(); 827 } 828 }); 829 } else { 830 msgsEl.style.display = ""; 831 showChatUI(); updateSwitchBar(); loadMessages(); connectEcho(); startPoll(); 832 } 833 } else { 834 msgsEl.style.display = ""; 835 showModeScreen(); 836 } 837 } 838 839 function closePanel() { 840 isOpen = false;
841 panel.classList.remove("ld-open"); 842 stopPoll(); stopWaitPoll(); closeMenu(); 843 endBar.classList.remove("show"); 844 } 845 846 function resetSession() { 847 localStorage.removeItem(LS_SESSION); localStorage.removeItem(LS_MODE); 848 sessionId = null; chatMode = null; lastMsgId = 0; 849 while (msgsEl.firstChild) msgsEl.removeChild(msgsEl.firstChild); 850 msgsEl.appendChild(typEl); typEl.style.display = "none"; 851 var trRow = document.getElementById("ld-transcript-row"); 852 if (trRow) trRow.remove(); 853 updateHeader(null); 854 isAssigned = false; leadCaptured = false; leadSkipped = false; 855 cancelAgentTimeout(); 856 endBar.classList.remove("show"); 857 } 858 859 function startPoll() { stopPoll(); pollTimer = setInterval(loadMessages, 3000); } 860 function stopPoll() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } 861 862 function showAgentsBusyScreen() { 863 clearScreens(); hideSwitchBar(); 864 footEl.style.display = "none"; msgsEl.style.display = "none"; 865 var screen = document.createElement("div"); 866 screen.id = "ld-busy-screen"; 867 screen.style.cssText = "flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px 20px;gap:14px;text-align:center;background:#fff;"; 868 screen.innerHTML = 869 '<div style="width:64px;height:64px;border-radius:50%;background:#fef3c7;display:flex;align-items:center;justify-content:center;">' + 870 '<svg width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="#f59e0b" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>' + 871 '</div>' + 872 '<div style="font-size:15px;font-weight:700;color:#111827;">All Agents Are Busy</div>' + 873 '<p style="font-size:13px;color:#6b7280;line-height:1.6;margin:0;">Our agents are helping other customers.<br>Please leave a message and we\'ll reply shortly.</p>' + 874 '<button id="ld-busy-msg" style="background:' + c + ';color:#fff;border:none;border-radius:12px;padding:12px 0;font-size:13.5px;font-weight:600;cursor:pointer;font-family:inherit;width:100%;">ð© Leave a Message</button>' + 875 '<button id="ld-busy-wait" style="background:none;border:1px solid #e5e7eb;border-radius:12px;padding:10px 0;font-size:13px;color:#6b7280;cursor:pointer;font-family:inherit;width:100%;">â© Go Back</button>'; 876 panel.insertBefore(screen, footEl); 877 screen.querySelector("#ld-busy-msg").onclick = function() { screen.remove(); msgsEl.style.display = ""; showOfflineForm(); }; 878 screen.querySelector("#ld-busy-wait").onclick = function() { screen.remove(); msgsEl.style.display = ""; showModeScreen(); }; 879 } 880 881 function showAgentLeftScreen(sid) { 882 clearScreens(); hideSwitchBar(); 883 footEl.style.display = "none"; msgsEl.style.display = "none"; 884 var screen = document.createElement("div"); 885 screen.id = "ld-agentleft-screen"; 886 screen.style.cssText = "flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px 20px;gap:16px;text-align:center;background:#fff;"; 887 screen.innerHTML = 888 '<div style="width:64px;height:64px;border-radius:50%;background:#f3f4f6;display:flex;align-items:center;justify-content:center;">' + 889 '<svg width="28" height="28" fill="none" viewBox="0 0 24 24" stroke="#6b7280" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>' + 890 '</div>' + 891 '<div style="font-size:15px;font-weight:700;color:#111827;">Agent has left the chat</div>' + 892 '<p style="font-size:13px;color:#6b7280;line-height:1.6;margin:0;">The session has ended. Would you like to connect again or rate this conversation?</p>' + 893 '<button id="ld-reconnect-btn" style="background:' + c + ';color:#fff;border:none;border-radius:12px;padding:12px 0;font-size:13.5px;font-weight:600;cursor:pointer;font-family:inherit;width:100%;">↻ Connect Again</button>' + 894 (sid ? '<button id="ld-rate-chat-btn" style="background:none;border:1px solid #e5e7eb;border-radius:12px;padding:10px 0;font-size:13px;color:#6b7280;cursor:pointer;font-family:inherit;width:100%;">★ Rate this Chat</button>' : ''); 895 panel.insertBefore(screen, footEl); 896 screen.querySelector("#ld-reconnect-btn").onclick = function() { screen.remove(); msgsEl.style.display = ""; showModeScreen(); }; 897 var rateBtn = screen.querySelector("#ld-rate-chat-btn"); 898 if (rateBtn) rateBtn.onclick = function() { screen.remove(); showRatingScreen(sid); }; 899 } 900 901 /* ââ Offline Form ââ */ 902 function showOfflineForm() { 903 clearScreens(); hideSwitchBar(); 904 footEl.style.display = "none"; msgsEl.style.display = "none"; 905 var form = document.createElement("div"); 906 form.id = "ld-offline-form"; 907 form.innerHTML = 908 '<div style="display:flex;align-items:center;gap:8px;margin-bottom:2px;">' + 909 '<button id="ld-of-back" style="background:#f3f4f6;border:none;border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer;color:#6b7280;">← Back</button>' + 910 '<span style="font-size:13.5px;font-weight:600;color:#374151;">Leave a Message</span>' + 911 '</div>' + 912 '<p style="font-size:12.5px;color:#6b7280;margin:0 0 4px;">We\'ll reply to your email as soon as possible.</p>' + 913 '<input type="text" id="ld-of-nm" placeholder="Your Name *" style="border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;outline:none;font-family:inherit;">' + 914 '<input type="email" id="ld-of-em" placeholder="Your Email *" style="border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;outline:none;font-family:inherit;">' + 915 '<select id="ld-of-dept"><option value="">Department (optional)</option><option value="Sales">Sales</option><option value="Support">Support</option></select>' + 916 '<textarea id="ld-of-ms" placeholder="Your message *" rows="4" style="border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;resize:none;outline:none;font-family:inherit;"></textarea>' + 917 '<button id="ld-of-btn" style="background:' + c + ';color:#fff;border:none;border-radius:11px;padding:12px;font-size:13.5px;font-weight:600;cursor:pointer;">Send Message</button>'; 918 panel.insertBefore(form, footEl); 919 form.querySelector("#ld-of-back").onclick = function() { 920 form.remove(); msgsEl.style.display = ""; showModeScreen(); 921 }; 922 form.querySelector("#ld-of-btn").onclick = function() { 923 var nm = document.getElementById("ld-of-nm").value.trim(); 924 var em = document.getElementById("ld-of-em").value.trim(); 925 var dept = document.getElementById("ld-of-dept").value; 926 var ms = document.getElementById("ld-of-ms").value.trim(); 927 if (!nm || !em || !ms) { alert("Please fill in all required fields."); return; } 928 this.disabled = true; this.textContent = "Sending..."; 929 var msgText = dept ? "[" + dept + "] " + ms : ms; 930 post("/chat/offline-message", {embed_token: EMBED_TOKEN, name: nm, email: em, message: msgText}, function(d) { 931 clearScreens(); 932 var screen = document.createElement("div"); 933 screen.id = "ld-thankyou-screen"; 934 screen.innerHTML = 935 '<div class="ld-ty-icon">â </div>' + 936 '<h3>Message Sent!</h3>' + 937 '<p>Thanks for reaching out. We\'ll reply to <strong>' + em + '</strong> as soon as possible.</p>'; 938 panel.insertBefore(screen, footEl); 939 footEl.style.display = "none"; 940 }); 941 }; 942 } 943 944 /* ââ Rating Screen ââ */ 945 function showRatingScreen(sid) { 946 hideSwitchBar(); 947 footEl.style.display = "none"; 948 endBar.classList.remove("show"); 949 msgsEl.style.display = ""; 950 var currentRating = 0; 951 var ratingEl = document.createElement("div"); 952 ratingEl.style.cssText = "align-self:center;background:#fff;border:1px solid #e5e7eb;border-radius:16px;padding:14px 16px;max-width:88%;text-align:center;margin:6px auto;"; 953 ratingEl.innerHTML = 954 '<p style="margin:0 0 8px;font-size:13.5px;font-weight:700;color:#111827;">How was your experience?</p>' + 955 '<div style="display:flex;gap:7px;justify-content:center;margin-bottom:10px;">' + 956 '<span class="ld-star" data-v="1" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">★</span>' + 957 '<span class="ld-star" data-v="2" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">★</span>' + 958 '<span class="ld-star" data-v="3" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">★</span>' + 959 '<span class="ld-star" data-v="4" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">★</span>' + 960 '<span class="ld-star" data-v="5" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">★</span>' + 961 '</div>' + 962 '<textarea id="ld-rb-comment" rows="2" placeholder="Optional comment..." style="width:100%;box-sizing:border-box;border:1px solid #e5e7eb;border-radius:8px;padding:7px 10px;font-size:12px;resize:none;outline:none;font-family:inherit;margin-bottom:8px;"></textarea>' + 963 '<button id="ld-rb-submit" disabled style="width:100%;padding:9px;border-radius:9px;border:none;background:' + c + ';color:#fff;font-size:13px;font-weight:600;cursor:pointer;opacity:0.5;margin-bottom:6px;">Submit Rating</button>' + 964 '<button id="ld-rb-skip" style="background:none;border:none;color:#9ca3af;font-size:11px;cursor:pointer;text-decoration:underline;font-family:inherit;">Skip</button>'; 965 msgsEl.insertBefore(ratingEl, typEl); 966 msgsEl.scrollTop = msgsEl.scrollHeight; 967 968 var stars = ratingEl.querySelectorAll(".ld-star"); 969 var submitBtn = ratingEl.querySelector("#ld-rb-submit"); 970 971 function highlightStars(val) {
972 stars.forEach(function(s) { 973 s.style.color = parseInt(s.getAttribute("data-v")) <= val ? "#f59e0b" : "#d1d5db"; 974 }); 975 } 976 stars.forEach(function(star) { 977 star.addEventListener("click", function() { 978 currentRating = parseInt(this.getAttribute("data-v")); 979 highlightStars(currentRating); 980 submitBtn.disabled = false; submitBtn.style.opacity = "1"; 981 }); 982 star.addEventListener("mouseover", function() { highlightStars(parseInt(this.getAttribute("data-v"))); }); 983 star.addEventListener("mouseout", function() { highlightStars(currentRating); }); 984 }); 985 986 submitBtn.addEventListener("click", function() { 987 if (!currentRating || !sid) return; 988 var comment = document.getElementById("ld-rb-comment") ? document.getElementById("ld-rb-comment").value.trim() : ""; 989 submitBtn.disabled = true; submitBtn.textContent = "Submitting..."; 990 post("/chat/" + sid + "/rate", {embed_token: EMBED_TOKEN, rating: currentRating, rating_comment: comment || null}, function() { 991 ratingEl.remove(); 992 var starsStr = "â".repeat(currentRating); 993 var ratingMsg = starsStr + " Visitor rated this chat " + currentRating + "/5" + (comment ? ": \"" + comment + "\"" : ""); 994 post("/chat/" + sid + "/system-msg", {embed_token: EMBED_TOKEN, message: ratingMsg}, function(){}); 995 appendMsg("system", "â Thanks for your " + currentRating + "-star rating!"); 996 setTimeout(function() { resetSession(); showModeScreen(); }, 3000); 997 }); 998 }); 999 1000 ratingEl.querySelector("#ld-rb-skip").addEventListener("click", function() { 1001 ratingEl.remove(); 1002 appendMsg("system", "Session ended. Start a new chat anytime!"); 1003 setTimeout(function() { resetSession(); showModeScreen(); }, 3000); 1004 }); 1005 } 1006 1007 function showThankYouScreen() { 1008 clearScreens(); 1009 var screen = document.createElement("div"); 1010 screen.id = "ld-thankyou-screen"; 1011 screen.innerHTML = 1012 '<div class="ld-ty-icon">ð</div>' + 1013 '<h3>Thank You!</h3>' + 1014 '<p>Your feedback helps us improve our service.<br>Feel free to start a new chat anytime.</p>'; 1015 panel.insertBefore(screen, footEl); 1016 footEl.style.display = "none"; 1017 setTimeout(function() { 1018 clearScreens(); resetSession(); showModeScreen(); 1019 }, 4000); 1020 } 1021 1022 /* ââ Agent Timeout ââ */ 1023 function startAgentTimeout() { 1024 cancelAgentTimeout(); 1025 agentTimeoutTimer = setTimeout(showNoAgentNotice, 60000); 1026 } 1027 function cancelAgentTimeout() { 1028 if (agentTimeoutTimer) { clearTimeout(agentTimeoutTimer); agentTimeoutTimer = null; } 1029 var el = document.getElementById("ld-no-agent-notice"); 1030 if (el) el.remove(); 1031 } 1032 function showNoAgentNotice() { 1033 if (isAssigned || document.getElementById("ld-no-agent-notice")) return; 1034 var notice = document.createElement("div"); 1035 notice.id = "ld-no-agent-notice"; 1036 notice.style.cssText = "background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:12px 14px;margin:4px 0;align-self:stretch;"; 1037 var aiBtnHtml = aiEnabled 1038 ? '<button id="ld-noa-ai" style="background:#6366f1;color:#fff;border:none;border-radius:8px;padding:7px 12px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;">ð¤ Talk with AI</button>' 1039 : ''; 1040 notice.innerHTML = 1041 '<p style="margin:0 0 6px;font-size:13px;color:#92400e;font-weight:600;">â³ No agents available right now</p>' + 1042 '<p style="margin:0 0 10px;font-size:12px;color:#b45309;line-height:1.5;">All agents are busy. You can switch to AI for instant answers or leave us a message.</p>' + 1043 '<div style="display:flex;gap:6px;flex-wrap:wrap;">' + aiBtnHtml + 1044 '<button id="ld-noa-email" style="background:none;border:1px solid #d1d5db;border-radius:8px;padding:7px 12px;font-size:12.5px;color:#374151;cursor:pointer;font-family:inherit;">ð© Email us</button>' + 1045 '</div>'; 1046 msgsEl.insertBefore(notice, typEl); 1047 msgsEl.scrollTop = msgsEl.scrollHeight; 1048 var aiBtn = document.getElementById("ld-noa-ai"); 1049 if (aiBtn) aiBtn.addEventListener("click", function() { notice.remove(); switchChatMode("ai"); }); 1050 document.getElementById("ld-noa-email").addEventListener("click", function() { 1051 notice.remove();
1051 clearScreens(); msgsEl.style.display = "none"; showOfflineForm(); 1052 }); 1053 } 1054 1055 /* ââ Lead Capture Form ââ */ 1056 function showLeadForm(pendingMessage) { 1057 if (document.getElementById("ld-lead-overlay")) return; 1058 var isAI = (chatMode === "ai"); 1059 var overlay = document.createElement("div"); 1060 overlay.id = "ld-lead-overlay"; 1061 overlay.style.cssText = "position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:20;display:flex;align-items:flex-end;"; 1062 var form = document.createElement("div"); 1063 form.style.cssText = "background:#fff;border-radius:16px 16px 0 0;padding:20px 16px 24px;width:100%;box-sizing:border-box;"; 1064 var titleText = isAI ? "One quick thingâ¦" : "Before we connect youâ¦"; 1065 var subText = isAI 1066 ? "Share your email so we can follow up. We'll keep asking until you do ð" 1067 : "Email is optional â you can skip if you prefer."; 1068 var emailLabel = isAI ? "Your email *" : "Your email (optional)"; 1069 form.innerHTML = 1070 '<h4 style="margin:0 0 4px;font-size:15px;color:#111827;font-weight:700;">' + titleText + '</h4>' + 1071 '<p style="margin:0 0 14px;font-size:12.5px;color:#6b7280;">' + subText + '</p>' + 1072 '<input id="ld-lead-name" type="text" placeholder="Your name (optional)" style="width:100%;box-sizing:border-box;border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;outline:none;font-family:inherit;display:block;margin-bottom:8px;">' + 1073 '<input id="ld-lead-email" type="email" placeholder="' + emailLabel + '" style="width:100%;box-sizing:border-box;border:1px solid #d1d5db;border-radius:10px;padding:9px 13px;font-size:13px;outline:none;font-family:inherit;display:block;margin-bottom:14px;">' + 1074 '<p id="ld-lead-err" style="margin:-10px 0 10px;font-size:12px;color:#ef4444;display:none;">Please enter a valid email address.</p>' + 1075 '<button id="ld-lead-submit" style="width:100%;background:' + c + ';color:#fff;border:none;border-radius:11px;padding:11px;font-size:13.5px;font-weight:600;cursor:pointer;font-family:inherit;">Continue â</button>' + 1076 '<div style="text-align:center;margin-top:10px;">' + 1077 '<button id="ld-lead-skip" style="background:none;border:none;color:#9ca3af;font-size:12px;cursor:pointer;text-decoration:underline;padding:2px 4px;font-family:inherit;">' + 1078 (isAI ? 'Skip for now (we\'ll ask again)' : 'Skip â') + 1079 '</button>' + 1080 '</div>'; 1081 overlay.appendChild(form); 1082 panel.appendChild(overlay); 1083 form.querySelector("#ld-lead-email").focus(); 1084 1085 function doSubmit() { 1086 var name = form.querySelector("#ld-lead-name").value.trim(); 1087 var email = form.querySelector("#ld-lead-email").value.trim(); 1088 var errEl = form.querySelector("#ld-lead-err"); 1089 if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { 1090 form.querySelector("#ld-lead-email").style.borderColor = "#ef4444"; 1091 errEl.style.display = "block"; 1092 return; 1093 } 1094 overlay.remove(); 1095 leadCaptured = true; 1096 post("/chat/update-visitor-info", {embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID, name: name || null, email: email}, function() {}); 1097 inputEl.value = pendingMessage; 1098 handleSend(true); 1099 } 1100 1101 form.querySelector("#ld-lead-submit").addEventListener("click", doSubmit); 1102 form.querySelector("#ld-lead-email").addEventListener("keydown", function(e) { 1103 if (e.key === "Enter") { e.preventDefault(); doSubmit(); } 1104 }); 1105 form.querySelector("#ld-lead-email").addEventListener("input", function() { 1106 this.style.borderColor = "#d1d5db"; 1107 form.querySelector("#ld-lead-err").style.display = "none"; 1108 }); 1109 form.querySelector("#ld-lead-skip").addEventListener("click", function() { 1110 overlay.remove(); 1111 if (!isAI) leadSkipped = true; // agent: never ask again 1112 // ai: leadSkipped stays false â form reappears next message 1113 inputEl.value = pendingMessage; 1114 handleSend(true); 1115 }); 1116 } 1117 1118 /* ââ WebSocket (Reverb / Echo) ââ */ 1119 function connectEcho() { 1120 if (!sessionId || echoInst) return; 1121 if (typeof window.Pusher === "undefined") { 1122 loadScript("https://cdn.jsdelivr.net/npm/[email protected]/dist/web/pusher.js", function() { 1123 loadScript("https://cdn.jsdelivr.net/npm/[email protected]/dist/echo.iife.js", initEcho); 1124 }); 1125 } else if (typeof window.LaravelEcho === "undefined" && typeof window.Echo === "undefined") { 1126 loadScript("https://cdn.jsdelivr.net/npm/[email protected]/dist/echo.iife.js", initEcho); 1127 } else { initEcho(); } 1128 } 1129 function initEcho() { 1130 try { 1131 var EC = window.LaravelEcho || window.Echo; 1132 if (!EC) return; 1133 echoInst = new EC({broadcaster:"reverb",key:REVERB_KEY,wsHost:REVERB_HOST,wsPort:REVERB_PORT,wssPort:REVERB_PORT,forceTLS:true,enabledTransports:["ws","wss"],disableStats:true}); 1134 echoInst.channel("chat-public." + sessionId) 1135 .listen(".new.message", function(e) { 1136 if (!e.messageData || e.messageData.sender_type === "visitor") return; 1137 var rawType = e.messageData.sender_type; 1138 var type = rawType === "bot" ? "ai" : (rawType === "tenant" ? "agent" : rawType); 1139 var ownerInfo = rawType === "tenant" ? {name: "Owner"} : currentAgentInfo; 1140 var isNew = appendMsg(type, e.messageData.message, e.messageData.id, ownerInfo); 1141 if (isNew) { 1142 lastMsgId = Math.max(lastMsgId, e.messageData.id || 0); 1143 playNotifSound(); 1144 } 1145 if (isNew && !isOpen) { unreadCount++; badgeEl.textContent = unreadCount; badgeEl.style.display = "flex"; } 1146 if (type === "agent") { 1147 stopWaitPoll(); clearScreens(); msgsEl.style.display = ""; 1148 showChatUI(); updateSwitchBar(); 1149 } 1150 }) 1151 .listen(".agent.typing", showTyping) 1152 .listen(".chat.closed", function(e) { 1153 var sid = sessionId; 1154 var closedBy = e && e.closed_by ? e.closed_by : null; 1155 stopPoll(); stopWaitPoll(); 1156 if (echoInst) { try { echoInst.disconnect(); } catch(e2) {} echoInst = null; } 1157 appendMsg("system", closedBy === "agent" ? "The agent has ended the chat." : "Chat has been closed."); 1158 showRejoinScreen(sid, closedBy); 1159 }) 1160 .listen(".chat.assigned", function(e) { 1161 stopWaitPoll(); clearScreens(); msgsEl.style.display = ""; 1162 isAssigned = true; cancelAgentTimeout(); 1163 if (e.agent_name) updateHeader({name: e.agent_name}); 1164 showChatUI(); updateSwitchBar(); 1165 appendMsg("system", "â " + (e.agent_name || "An agent") + " has joined!"); 1166 loadMessages(); 1167 }); 1168 } catch(err) { console.warn("[LD Echo]", err); } 1169 } 1170 function loadScript(src, cb) { 1171 var s = document.createElement("script"); s.src = src; s.onload = cb; document.head.appendChild(s); 1172 } 1173 1174 /* ââ Widget Settings ââ */ 1175 var triggerMessage = null; 1176 var triggerDelay = 0; 1177 var triggerFired = false;
1178 1179 function applyWidgetStyles() { 1180 var isLeft = widgetPosition === "bottom-left"; 1181 var bub = document.getElementById("ld-bubble"); 1182 var pnl = document.getElementById("ld-panel"); 1183 if (bub) { 1184 bub.style.right = isLeft ? "auto" : "24px"; 1185 bub.style.left = isLeft ? "24px" : "auto"; 1186 } 1187 if (pnl && !isFullscreen) { 1188 pnl.style.right = isLeft ? "auto" : "24px"; 1189 pnl.style.left = isLeft ? "24px" : "auto"; 1190 pnl.style.width = widgetWidth + "px"; 1191 pnl.style.maxHeight = widgetHeight + "px"; 1192 pnl.style.borderRadius = widgetBorderRadius + "px"; 1193 } 1194 } 1195 1196 function fetchSettings() { 1197 fetchDepartments(function() {}); 1198 get("/widget-settings/" + EMBED_TOKEN, function(data) { 1199 if (!data) return; 1200 agentsOnline = data.agents_online || 0; 1201 if (data.title) companyName = data.title; 1202 triggerMessage = data.trigger_message || null; 1203 triggerDelay = parseInt(data.trigger_delay) || 0; 1204 widgetReady = true; 1205 aiEnabled = data.ai_enabled === true; 1206 showBranding = data.show_branding === true; 1207 var brandEl = document.getElementById("ld-branding"); 1208 if (showBranding && !brandEl) { 1209 var br = document.createElement("div"); 1210 br.id = "ld-branding"; 1211 br.style.cssText = "text-align:center;font-size:10px;color:#9ca3af;padding:3px 0 4px;background:#fff;border-top:1px solid #f3f4f6;"; 1212 br.innerHTML = 'Powered by <a href="https://ictdesk.com" target="_blank" style="color:#9ca3af;text-decoration:none;font-weight:600;">ICTDesk</a>'; 1213 panel.appendChild(br); 1214 } else if (!showBranding && brandEl) { 1215 brandEl.remove(); 1216 } 1217 widgetTemplate = parseInt(data.template) || 1; 1218 widgetPosition = data.position || "bottom-right"; 1219 widgetBorderRadius = data.border_radius || "16"; 1220 widgetWidth = data.widget_width || "380"; 1221 widgetHeight = data.widget_height || "560"; 1222 applyWidgetStyles(); 1223 if (!currentAgentInfo) { 1224 headTitle.textContent = companyName; 1225 if (agentsOnline > 0) { 1226 headSub.textContent = agentsOnline + " agent" + (agentsOnline === 1 ? "" : "s") + " online"; 1227 headSub.style.color = "#86efac"; 1228 } else { 1229 headSub.textContent = "AI available 24/7 ⢠Agents offline"; 1230 headSub.style.color = ""; 1231 } 1232 } 1233 if (triggerDelay > 0 && !sessionId && !triggerFired) { 1234 triggerFired = true; 1235 setTimeout(function() { 1236 if (!isOpen && !sessionId) { 1237 openPanel(); 1238 if (triggerMessage) setTimeout(showTriggerProactive, 200); 1239 } 1240 }, triggerDelay * 1000); 1241 } 1242 }); 1243 } 1244 1245 function showTriggerProactive() { 1246 var existing = document.getElementById("ld-trigger-proactive"); 1247 if (existing) return; 1248 var d = document.createElement("div"); 1249 d.id = "ld-trigger-proactive"; 1250 d.style.cssText = "padding:10px 14px 0 12px;display:flex;flex-direction:column;gap:8px;background:#f9fafb;"; 1251 var bub = document.createElement("div"); 1252 bub.className = "ld-msg agent"; 1253 bub.style.maxWidth = "82%"; 1254 bub.textContent = triggerMessage; 1255 d.appendChild(bub); 1256 var modeScreen = document.getElementById("ld-mode-screen"); 1257 if (modeScreen) panel.insertBefore(d, modeScreen); 1258 playNotifSound(); 1259 } 1260 1261 function maybeFireTrigger() { 1262 if (triggerFired || !triggerMessage || triggerDelay <= 0) return; 1263 triggerFired = true; 1264 setTimeout(function() { 1265 var hasAgentMsg = msgsEl.querySelector(".ld-msg.agent"); 1266 if (!hasAgentMsg) { 1267 appendMsg("agent", triggerMessage, "trigger-" + Date.now(), null); 1268 playNotifSound(); 1269 if (!isOpen) { unreadCount++; badgeEl.textContent = unreadCount; badgeEl.style.display = "flex"; } 1270 } 1271 }, triggerDelay * 1000); 1272 } 1273 1274 /* ââ Device & Network Info ââ */ 1275 function getCountryFromTimezone() { 1276 var tzMap = {"Asia/Karachi":"PK","Asia/Kolkata":"IN","Asia/Dhaka":"BD","Asia/Kabul":"AF","America/New_York":"US","America/Chicago":"US","America/Los_Angeles":"US","America/Toronto":"CA","Europe/London":"GB","Europe/Paris":"FR","Europe/Berlin":"DE","Asia/Dubai":"AE","Asia/Riyadh":"SA","Asia/Tokyo":"JP","Asia/Shanghai":"CN","Asia/Singapore":"SG","Australia/Sydney":"AU","Africa/Cairo":"EG","Africa/Lagos":"NG","America/Sao_Paulo":"BR","Asia/Tehran":"IR","Asia/Baghdad":"IQ","Asia/Istanbul":"TR","Europe/Moscow":"RU","Asia/Tashkent":"UZ","Asia/Almaty":"KZ","Asia/Colombo":"LK","Asia/Kathmandu":"NP","Asia/Yangon":"MM","Asia/Bangkok":"TH","Asia/Jakarta":"ID","Asia/Manila":"PH","Asia/Seoul":"KR","Asia/Taipei":"TW"}; 1277 try { return tzMap[Intl.DateTimeFormat().resolvedOptions().timeZone] || ""; } catch(e) { return ""; } 1278 } 1279 function sendPing() { 1280 var tz = ""; try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch(e) {} 1281 var conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; 1282 var netType = "", netSpeed = ""; 1283 try { if (conn) { netType = conn.effectiveType || conn.type || ""; netSpeed = conn.downlink ? conn.downlink + " Mbps" : ""; } } catch(e) {} 1284 var ram = ""; try { if (navigator.deviceMemory) ram = navigator.deviceMemory + " GB"; } catch(e) {} 1285 post("/visitor/ping", { 1286 embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID, 1287 page_url: window.location.href, page_title: document.title, 1288 referrer: document.referrer || "", timezone: tz, 1289 country: getCountryFromTimezone(), 1290 network_type: netType, network_speed: netSpeed, ram: ram, 1291 screen_width: window.screen.width, screen_height: window.screen.height, 1292 platform: navigator.platform || "", language: navigator.language || "", 1293 user_agent: navigator.userAgent 1294 }, function() {}); 1295 } 1296 function sendHeartbeat() { 1297 post("/visitor/heartbeat", {embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID, page_url: window.location.href}, function() {}); 1298 } 1299 function sendOffline() { 1300 var d = JSON.stringify({embed_token: EMBED_TOKEN, visitor_uuid: visitorUUID}); 1301 if (navigator.sendBeacon) navigator.sendBeacon(API_BASE + "/visitor/offline", new Blob([d], {type: "application/json"})); 1302 } 1303 1304 /* ââ Events ââ */ 1305 bubble.addEventListener("click", function() { isOpen ? closePanel() : openPanel
1305(); }); 1306 closeX.addEventListener("click", closePanel); 1307 var visitorTypingDebounce = null; 1308 function sendVisitorTyping() { 1309 if (!sessionId) return; 1310 clearTimeout(visitorTypingDebounce); 1311 visitorTypingDebounce = setTimeout(function() { 1312 fetch(API_BASE + "/chat/" + sessionId + "/typing", { 1313 method: "POST", 1314 headers: {"Content-Type":"application/json","Accept":"application/json"}, 1315 body: JSON.stringify({embed_token: EMBED_TOKEN}) 1316 }).catch(function(){}); 1317 }, 300); 1318 } 1319 1320 inputEl.addEventListener("input", function() { 1321 this.style.height = "auto"; 1322 this.style.height = Math.min(this.scrollHeight, 80) + "px"; 1323 sendBtn.disabled = this.value.trim() === ""; 1324 if (this.value.trim()) sendVisitorTyping(); 1325 }); 1326 inputEl.addEventListener("keydown", function(e) { 1327 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (!sendBtn.disabled) handleSend(); } 1328 }); 1329 sendBtn.addEventListener("click", handleSend); 1330 1331 /* ââ File Upload ââ */ 1332 function initFileUpload() { 1333 var fileInputEl = panel.querySelector("#ld-file-input"); 1334 var attachBtnEl = panel.querySelector("#ld-attach-btn"); 1335 if (!attachBtnEl || !fileInputEl) return; 1336 attachBtnEl.addEventListener("click", function() { 1337 if (!sessionId) { alert("Start a chat first before attaching files."); return; } 1338 fileInputEl.value = ""; 1339 fileInputEl.click(); 1340 }); 1341 fileInputEl.addEventListener("change", function() { 1342 var file = fileInputEl.files[0]; 1343 if (!file) return; 1344 if (file.size > 5 * 1024 * 1024) { alert("File too large. Maximum 5MB allowed."); return; } 1345 var fd = new FormData(); 1346 fd.append("file", file); 1347 fd.append("session_id", sessionId); 1348 fd.append("embed_token", EMBED_TOKEN); 1349 attachBtnEl.disabled = true; attachBtnEl.style.opacity = "0.4"; 1350 fetch(API_BASE + "/chat/upload", { method: "POST", body: fd }) 1351 .then(function(r) { return r.json(); }) 1352 .then(function(data) { 1353 attachBtnEl.disabled = false; attachBtnEl.style.opacity = ""; 1354 if (data.message) { 1355 appendMsg("visitor", data.message.message, data.message.id); 1356 lastMsgId = Math.max(lastMsgId, data.message.id || 0); 1357 } else if (data.error) { alert(data.error); } 1358 fileInputEl.value = ""; 1359 }) 1360 .catch(function() { attachBtnEl.disabled = false; attachBtnEl.style.opacity = ""; alert("Upload failed. Please try again."); }); 1361 }); 1362 } 1363 initFileUpload(); 1364 1365 sendPing(); 1366 fetchSettings(); 1367 setInterval(sendHeartbeat, 25000); 1368 window.addEventListener("beforeunload", sendOffline); 1369 window.addEventListener("pagehide", sendOffline); 1370 1371 } // end initLiveDesk 1372})();
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.