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.