PageSourceSearch

https://chat-with-ceo.gemseducationindia.com/widget.js

js gemseducationindia.com collected 2026-10-09 22:01:27 UTC 9,712 bytes, 256 lines download raw bytes

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.