PageSourceSearch

https://service.ictdesk.net/api/widget/WS4sQCBUnXHPs0CVvEGcEf9I59tv4SOwVmt7Xz3l/livedesk.js

js ictdesk.net collected 2026-10-04 02:03:40 UTC 75,541 bytes, 1,372 lines download raw bytes

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">&times;</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, '&amp;')
323      .replace(/</g, '&lt;')
324      .replace(/>/g, '&gt;')
325      .replace(/"/g, '&quot;');
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;">&#8635; 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;">&#9733; Rate &amp; 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 = "&#8635; 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 &rarr; 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%;">&#8635; 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%;">&#9733; 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;">&#8592; 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;">&#9733;</span>' +
957        '<span class="ld-star" data-v="2" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">&#9733;</span>' +
958        '<span class="ld-star" data-v="3" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">&#9733;</span>' +
959        '<span class="ld-star" data-v="4" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">&#9733;</span>' +
960        '<span class="ld-star" data-v="5" style="font-size:26px;color:#d1d5db;cursor:pointer;line-height:1;user-select:none;">&#9733;</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.