1/* Talk-to-Francis embeddable chat widget. 2 * Usage: 3 * <script src="https://chat-with-ceo.gemseducationindia.com/widget.js" 4 * data-school="GEMS Millennium School, Gurugram" 5 * data-grade="Primary (Grade 1â5)" 6 * data-color="#075E54" 7 * data-position="bottom-right" 8 * data-label="Talk to Francis" 9 * data-icon="https://.../avatar.png" 10 * async></script> 11 */ 12(function () { 13 if (window.__gemsFrancisWidgetLoaded) return; 14 window.__gemsFrancisWidgetLoaded = true; 15 16 var script = 17 document.currentScript || 18 (function () { 19 var s = document.getElementsByTagName("script"); 20 return s[s.length - 1]; 21 })(); 22 23 var origin = new URL(script.src).origin; 24 25 // --- session id (stable across the page visit, fresh per tab) --- 26 function uuidv4() { 27 if (window.crypto && crypto.randomUUID) return crypto.randomUUID(); 28 return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 29 var r = (Math.random() * 16) | 0; 30 var v = c === "x" ? r : (r & 0x3) | 0x8; 31 return v.toString(16); 32 }); 33 } 34 var sessionId; 35 try { 36 sessionId = sessionStorage.getItem("gemsFrancisWidgetSid"); 37 if (!sessionId) { 38 sessionId = uuidv4(); 39 sessionStorage.setItem("gemsFrancisWidgetSid", sessionId); 40 } 41 } catch (_) { 42 sessionId = uuidv4(); 43 } 44 45 var cfg = { 46 school: script.getAttribute("data-school") || "", 47 grade: script.getAttribute("data-grade") || "", 48 parentName: script.getAttribute("data-parent-name") || "", 49 childName: script.getAttribute("data-child-name") || "", 50 color: script.getAttribute("data-color") || "#075E54", 51 position: script.getAttribute("data-position") || "bottom-right", 52 label: script.getAttribute("data-label") || "Talk to Francis", 53 icon: script.getAttribute("data-icon") || origin + "/francis.png", 54 }; 55 56 // --- analytics shipping (cross-origin to our origin) --- 57 var TRACK_URL = origin + "/api/public/widget-track"; 58 var hasCompleted = false; 59 function track(event, extra) { 60 try { 61 var payload = { 62 sessionId: sessionId, 63 event: event, 64 hostOrigin: location.origin, 65 hostPath: location.pathname + location.search, 66 referrer: document.referrer || undefined, 67 school: cfg.school || undefined, 68 grade: cfg.grade || undefined, 69 metadata: extra || undefined, 70 }; 71 var body = JSON.stringify(payload); 72 if (navigator.sendBeacon) { 73 var ok = navigator.sendBeacon( 74 TRACK_URL, 75 new Blob([body], { type: "application/json" }), 76 ); 77 if (ok) return; 78 } 79 fetch(TRACK_URL, { 80 method: "POST", 81 headers: { "Content-Type": "application/json" }, 82 body: body, 83 keepalive: true, 84 mode: "cors", 85 }).catch(function () {}); 86 } catch (_) {} 87 } 88
89 var Z = 2147483000; 90 var isMobile = function () { return window.matchMedia("(max-width: 640px)").matches; }; 91 var posStyle = cfg.position === "bottom-left" 92 ? "left:20px;right:auto;" 93 : "right:20px;left:auto;"; 94 95 // Launcher button 96 var btn = document.createElement("button"); 97 btn.setAttribute("aria-label", cfg.label); 98 btn.style.cssText = 99 "position:fixed;bottom:20px;" + posStyle + 100 "width:60px;height:60px;border-radius:50%;border:none;cursor:pointer;" + 101 "background:" + cfg.color + ";box-shadow:0 6px 20px rgba(0,0,0,0.25);" + 102 "z-index:" + Z + ";padding:0;overflow:hidden;display:flex;align-items:center;justify-content:center;" + 103 "transition:transform 0.2s ease;"; 104 var onBtnEnter = function () { btn.style.transform = "scale(1.06)"; }; 105 var onBtnLeave = function () { btn.style.transform = "scale(1)"; }; 106 btn.addEventListener("mouseenter", onBtnEnter); 107 btn.addEventListener("mouseleave", onBtnLeave); 108 109 var img = document.createElement("img"); 110 img.src = cfg.icon; 111 img.alt = ""; 112 img.style.cssText = "width:100%;height:100%;object-fit:cover;display:block;"; 113 img.onerror = function () { 114 btn.removeChild(img); 115 btn.innerHTML = 116 '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>'; 117 }; 118 btn.appendChild(img); 119 120 // Tooltip (desktop only) 121 var tip = document.createElement("div"); 122 tip.textContent = cfg.label; 123 tip.style.cssText = 124 "position:fixed;bottom:32px;" + 125 (cfg.position === "bottom-left" ? "left:90px;" : "right:90px;") + 126 "background:#111;color:#fff;font:500 13px/1 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;" + 127 "padding:8px 12px;border-radius:6px;z-index:" + Z + ";pointer-events:none;" + 128 "opacity:0;transform:translateY(4px);transition:opacity 0.15s,transform 0.15s;white-space:nowrap;"; 129 var onTipShow = function () { 130 if (isMobile() || open) return; 131 tip.style.opacity = "1"; 132 tip.style.transform = "translateY(0)"; 133 }; 134 var onTipHide = function () { 135 tip.style.opacity = "0"; 136 tip.style.transform = "translateY(4px)"; 137 }; 138 btn.addEventListener("mouseenter", onTipShow); 139 btn.addEventListener("mouseleave", onTipHide); 140 141 // Panel container 142 var panel = document.createElement("div"); 143 panel.style.cssText = 144 "position:fixed;z-index:" + Z + ";display:none;overflow:hidden;background:#fff;" + 145 "box-shadow:0 12px 40px rgba(0,0,0,0.3);border-radius:14px;"; 146 147 // Close button 148 var close = document.createElement("button"); 149 close.setAttribute("aria-label", "Close"); 150 close.innerHTML = 151 '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>'; 152 close.style.cssText = 153 "position:absolute;top:10px;right:10px;width:32px;height:32px;border-radius:50%;" + 154 "border:none;background:rgba(0,0,0,0.25);cursor:pointer;z-index:2;display:flex;align-items:center;justify-content:center;padding:0;"; 155 156 // Iframe 157 var iframe = document.createElement("iframe"); 158 var params = new URLSearchParams(); 159 if (cfg.school) params.set("school", cfg.school); 160 if (cfg.grade) params.set("grade", cfg.grade); 161 if (cfg.parentName) params.set("parentName", cfg.parentName); 162 if (cfg.childName) params.set("childName", cfg.childName); 163 params.set("sid", sessionId); 164 params.set("host", location.origin); 165 params.set("hpath", location.pathname); 166 iframe.src = origin + "/embed?" + params.toString(); 167 iframe.setAttribute("allow", "microphone; autoplay; clipboard-write"); 168 iframe.setAttribute("title", cfg.label); 169 iframe.style.cssText = "width:100%;height:100%;border:0;display:block;background:#ECE5DD;"; 170 171 panel.appendChild(iframe); 172 panel.appendChild(close); 173 174 var open = false; 175 function applySize() { 176 if (isMobile()) { 177 panel.style.cssText += 178 ";top:0;left:0;right:0;bottom:0;width:100%;height:100%;border-radius:0;"; 179 } else { 180 panel.style.top = "auto"; 181 panel.style.left = cfg.position === "bottom-left" ? "20px" : "auto"; 182 panel.style.right = cfg.position === "bottom-left" ? "auto" : "20px"; 183 panel.style.bottom = "96px"; 184 panel.style.width = "400px"; 185 panel.style.height = "min(640px, calc(100dvh - 120px))"; 186 panel.style.borderRadius = "14px"; 187 } 188 } 189 190 function setOpen(v) { 191 var wasOpen = open; 192 open = v; 193 panel.style.display = v ? "block" : "none"; 194 btn.style.display = v && isMobile() ? "none" : "flex"; 195 if (v) applySize(); 196 tip.style.opacity = "0"; 197 if (v && !wasOpen) { 198 track("opened"); 199 } else if (!v && wasOpen) { 200 track("closed", { completed: hasCompleted }); 201 } 202 } 203 204 var onBtnClick = function () { setOpen(!open); }; 205 var onCloseClick = function () { setOpen(false); }; 206 var onResize = function () { if (open) applySize(); }; 207 var onMessage = function (e) { 208 var d = e.data; 209 if (!d || d.source !== "gems-francis-widget") return;
210 if (d.type === "close") setOpen(false); 211 if (d.type === "submitted") { 212 hasCompleted = true; 213 setTimeout(function () { setOpen(false); }, 6000); 214 } 215 }; 216 var onPagehide = function () { 217 if (open) track("closed", { completed: hasCompleted, unload: true }); 218 }; 219 220 btn.addEventListener("click", onBtnClick); 221 close.addEventListener("click", onCloseClick); 222 window.addEventListener("resize", onResize); 223 window.addEventListener("message", onMessage); 224 window.addEventListener("pagehide", onPagehide); 225 226 function mount() { 227 document.body.appendChild(btn); 228 document.body.appendChild(tip); 229 document.body.appendChild(panel); 230 track("loaded"); 231 } 232 if (document.body) mount(); 233 else document.addEventListener("DOMContentLoaded", mount); 234 235 function destroy() { 236 try { 237 window.removeEventListener("resize", onResize); 238 window.removeEventListener("message", onMessage); 239 window.removeEventListener("pagehide", onPagehide); 240 if (btn.parentNode) btn.parentNode.removeChild(btn); 241 if (tip.parentNode) tip.parentNode.removeChild(tip); 242 if (panel.parentNode) panel.parentNode.removeChild(panel); 243 } catch (_) {} 244 try { delete window.__gemsFrancisWidgetLoaded; } catch (_) { window.__gemsFrancisWidgetLoaded = false; } 245 try { delete window.GemsFrancisWidget; } catch (_) { window.GemsFrancisWidget = undefined; } 246 } 247 248 // Public API 249 window.GemsFrancisWidget = { 250 open: function () { setOpen(true); }, 251 close: function () { setOpen(false); }, 252 toggle: function () { setOpen(!open); }, 253 destroy: destroy, 254 sessionId: sessionId, 255 }; 256})();
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.