PageSourceSearch

https://yp-systems-api.yellowpageskenya.com/v1/embed/ypk_live_0a06230dcf5c3086a6be4e1bf45e1002/widget.js

js yellowpageskenya.com collected 2026-10-02 17:03:56 UTC 8,964 bytes, 170 lines download raw bytes

1/* YP Chatbot — YP Kenya */
2(function(){var __YP_CONFIG__={"embedKey":"ypk_live_0a06230dcf5c3086a6be4e1bf45e1002","apiBase":"","profileName":"YP Kenya","primaryColor":"#FFCC00","logoUrl":"","greeting":"Hi! How can I help you with YP Kenya today?"};/*
3 * YP embedded chat widget (public, anonymous). Runs on an EXTERNAL customer
4 * site. Loaded by GET /v1/embed/:embedKey/widget.js, which wraps this file in an
5 * IIFE with `__YP_CONFIG__` (embedKey, apiBase, profileName, primaryColor,
6 * logoUrl, greeting) in scope.
7 *
8 * Unlike the legacy public/chatbot.js (which called the AUTHENTICATED
9 * /chat/sessions* endpoints and cannot work for an anonymous visitor), this
10 * talks to the PUBLIC embed endpoint POST /v1/embed/chat with the X-Embed-Key
11 * header and no cookies/session. Non-streaming fetch is used deliberately:
12 * EventSource cannot set the X-Embed-Key header, and one round trip keeps the
13 * widget dependency-free and robust.
14 */
15/* eslint-env browser */
16/* global __YP_CONFIG__ */
17'use strict';
18(function () {
19  var cfg = (typeof __YP_CONFIG__ !== 'undefined' && __YP_CONFIG__) || {};
20  if (!cfg.embedKey) return; // nothing to talk to
21  if (window.__YP_EMBED_MOUNTED__) return; // guard against double-injection
22  window.__YP_EMBED_MOUNTED__ = true;
23
24  // Resolve the API base. Prefer the server-injected value; otherwise derive it
25  // from THIS script's own <script src> origin — the widget was loaded from the
26  // backend host (…/v1/embed/<key>/widget.js), so its origin is the API base.
27  // This means the widget works even if API_PUBLIC_URL is unset on the server.
28  function resolveApiBase() {
29    var base = (cfg.apiBase || '').trim().replace(/\/+$/, '');
30    if (base) return base;
31    try {
32      var el = document.currentScript
33        || document.querySelector('script[src*="/v1/embed/"]');
34      if (el && el.src) return new URL(el.src).origin;
35    } catch (e) { /* ignore */ }
36    return '';
37  }
38  var API_BASE = resolveApiBase();
39  // Sanitize the injected colour before it goes into a stylesheet — only accept
40  // a hex or rg(a)/hsl(a) form, else fall back. Prevents CSS injection via a
41  // malformed theme value.
42  function safeColor(c) {
43    c = String(c || '').trim();
44    return /^#[0-9a-f]{3,8}$/i.test(c) || /^(rgb|hsl)a?\([0-9.,%\s/]+\)$/i.test(c) ? c : '#FFCC00';
45  }
46  // Only allow http(s) image URLs for the logo (no data:/javascript:).
47  function safeHttpUrl(u) {
48    u = String(u || '').trim();
49    return /^https?:\/\//i.test(u) ? u : '';
50  }
51  var PRIMARY = safeColor(cfg.primaryColor);
52  var LOGO = safeHttpUrl(cfg.logoUrl);
53  var NAME = cfg.profileName || 'Chat';
54  var GREETING = cfg.greeting || ('Hi! How can I help you with ' + NAME + ' today?');
55
56  // Per-visitor identity + conversation, persisted best-effort.
57  function store(k, v) { try { if (v === undefined) return localStorage.getItem(k); localStorage.setItem(k, v); } catch (e) { return null; } }
58  var visitorId = store('yp_embed_visitor') || ('v_' + Math.random().toString(36).slice(2) + Date.now().toString(36));
59  store('yp_embed_visitor', visitorId);
60  var sessionId = null;
61  var busy = false;
62
63  // --- styles (scoped by the yp-embed- prefix; no external CSS) ---------------
64  var css = ''
65    + '.yp-embed-btn{position:fixed;bottom:20px;right:20px;z-index:2147483000;width:56px;height:56px;border-radius:50%;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.25);background:' + PRIMARY + ';display:flex;align-items:center;justify-content:center;font-size:26px;line-height:1}'
66    + '.yp-embed-panel{position:fixed;bottom:88px;right:20px;z-index:2147483000;width:360px;max-width:calc(100vw - 40px);height:520px;max-height:calc(100vh - 120px);background:#fff;border-radius:14px;box-shadow:0 12px 40px rgba(0,0,0,.28);display:none;flex-direction:column;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}'
67    + '.yp-embed-panel.open{display:flex}'
68    + '.yp-embed-hd{background:' + PRIMARY + ';color:#111;padding:12px 14px;font-weight:600;display:flex;align-items:center;gap:8px}'
69    + '.yp-embed-hd img{height:22px;width:auto;border-radius:4px}'
70    + '.yp-embed-hd .yp-x{margin-left:auto;background:transparent;border:none;font-size:20px;cursor:pointer;color:#111;line-height:1}'
71    + '.yp-embed-log{flex:1;overflow-y:auto;padding:12px;background:#f7f7f8;display:flex;flex-direction:column;gap:8px}'
72    + '.yp-embed-msg{max-width:80%;padding:8px 11px;border-radius:12px;font-size:14px;line-height:1.4;white-space:pre-wrap;word-break:break-word}'
73    + '.yp-embed-msg.bot{align-self:flex-start;background:#fff;color:#111;border:1px solid #e5e5e5}'
74    + '.yp-embed-msg.user{align-self:flex-end;background:' + PRIMARY + ';color:#111}'
75    + '.yp-embed-msg.err{align-self:flex-start;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}'
76    + '.yp-embed-ft{display:flex;gap:6px;padding:10px;border-top:1px solid #eee;background:#fff}'
77    + '.yp-embed-ft input{flex:1;border:1px solid #ddd;border-radius:8px;padding:9px 10px;font-size:14px;outline:none}'
78    + '.yp-embed-ft input:focus{border-color:' + PRIMARY + '}'
79    + '.yp-embed-ft button{border:none;border-radius:8px;padding:0 14px;background:' + PRIMARY + ';color:#111;font-weight:600;cursor:pointer}'
80    + '.yp-embed-ft button:disabled{opacity:.5;cursor:default}';
81  var style = document.createElement('style');
82  style.textContent = css;
83  document.head.appendChild(style);
84
85  // --- DOM --------------------------------------------------------------------
86  var btn = document.createElement('button');
87  btn.className = 'yp-embed-btn';
88  btn.type = 'button';
89  btn.setAttribute('aria-label', 'Open ' + NAME + ' chat');
90  btn.innerHTML = '<span aria-hidden="true">💬</span>';
91
92  var panel = document.createElement('div');
93  panel.className = 'yp-embed-panel';
94  panel.setAttribute('role', 'dialog');
95  panel.setAttribute('aria-label', NAME + ' chat');
96  // Static structure only via innerHTML; the dynamic name/logo are set below via
97  // DOM properties (textContent / img.src), never string-concatenated into HTML.
98  panel.innerHTML = ''
99    + '<div class="yp-embed-hd"><span class="yp-nm"></span>'
100    + '<button class="yp-x" type="button" aria-label="Close chat">
100×</button></div>'
101    + '<div class="yp-embed-log" role="log" aria-live="polite"></div>'
102    + '<form class="yp-embed-ft"><input type="text" placeholder="Type a message…" aria-label="Message" autocomplete="off" maxlength="4000"><button type="submit">Send</button></form>';
103  var hd = panel.querySelector('.yp-embed-hd');
104  if (LOGO) {
105    var logo = document.createElement('img');
106    logo.src = LOGO; logo.alt = '';
107    hd.insertBefore(logo, hd.firstChild);
108  }
109  panel.querySelector('.yp-nm').textContent = NAME;
110
111  document.body.appendChild(btn);
112  document.body.appendChild(panel);
113
114  var logEl = panel.querySelector('.yp-embed-log');
115  var formEl = panel.querySelector('.yp-embed-ft');
116  var inputEl = panel.querySelector('.yp-embed-ft input');
117  var sendEl = panel.querySelector('.yp-embed-ft button');
118
119  function addMsg(text, kind) {
120    var m = document.createElement('div');
121    m.className = 'yp-embed-msg ' + kind;
122    m.textContent = text;
123    logEl.appendChild(m);
124    logEl.scrollTop = logEl.scrollHeight;
125    return m;
126  }
127
128  var opened = false;
129  function toggle(open) {
130    opened = open == null ? !opened : open;
131    panel.classList.toggle('open', opened);
132    if (opened) {
133      if (!logEl.childNodes.length) addMsg(GREETING, 'bot');
134      inputEl.focus();
135    }
136  }
137  btn.addEventListener('click', function () { toggle(); });
138  panel.querySelector('.yp-x').addEventListener('click', function () { toggle(false); });
139
140  function send(text) {
141    if (busy || !text.trim()) return;
142    busy = true; sendEl.disabled = true;
143    addMsg(text, 'user');
144    inputEl.value = '';
145    var typing = addMsg('…', 'bot');
146    fetch(API_BASE + '/v1/embed/chat', {
147      method: 'POST',
148      headers: { 'Content-Type': 'application/json', 'X-Embed-Key': cfg.embedKey },
149      body: JSON.stringify({ message: text, sessionId: sessionId, visitorId: visitorId }),
150    })
151      .then(function (r) { return r.json().then(function (b) { return { ok: r.ok, status: r.status, body: b }; }); })
152      .then(function (res) {
153        typing.remove();
154        if (!res.ok) {
155          addMsg((res.body && res.body.error) || 'Sorry, something went wrong.', 'err');
156          return;
157        }
158        var b = res.body || {};
159        if (b.sessionId) sessionId = b.sessionId;
160        var reply = (b.message && b.message.content) || b.reply || b.content || '';
161        addMsg(reply || 'Sorry, I did not catch that.', 'bot');
162      })
163      .catch(function () { typing.remove(); addMsg('Network error. Please try again.', 'err'); })
164      .finally(function () { busy = false; sendEl.disabled = false; inputEl.focus(); });
165  }
166
167  formEl.addEventListener('submit', function (e) { e.preventDefault(); send(inputEl.value); });
168})();
169
170})();

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.