brainerce 1.28.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,532 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/bot/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BrainerceBot: () => BrainerceBot
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/bot/widget.ts
28
+ var DEFAULT_BASE_URL = "https://api.brainerce.com";
29
+ var RTL_LOCALES = /* @__PURE__ */ new Set(["he", "ar"]);
30
+ var CHROME = {
31
+ en: {
32
+ online: "Online",
33
+ placeholder: "Ask anything\u2026",
34
+ error: "Something went wrong \u2014 please try again.",
35
+ leaveMessage: "Leave a message for the team",
36
+ yourEmail: "Your email",
37
+ yourMessage: "Your message",
38
+ send: "Send",
39
+ sent: "Thanks! The team will get back to you by email.",
40
+ close: "Close"
41
+ },
42
+ he: {
43
+ online: "\u05DE\u05D7\u05D5\u05D1\u05E8",
44
+ placeholder: "\u05E9\u05D0\u05DC\u05D5 \u05D0\u05D5\u05EA\u05D9 \u05D4\u05DB\u05DC\u2026",
45
+ error: "\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9 \u2014 \u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.",
46
+ leaveMessage: "\u05D4\u05E9\u05D0\u05D9\u05E8\u05D5 \u05D4\u05D5\u05D3\u05E2\u05D4 \u05DC\u05E6\u05D5\u05D5\u05EA",
47
+ yourEmail: "\u05D4\u05D0\u05D9\u05DE\u05D9\u05D9\u05DC \u05E9\u05DC\u05DB\u05DD",
48
+ yourMessage: "\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",
49
+ send: "\u05E9\u05DC\u05D9\u05D7\u05D4",
50
+ sent: "\u05EA\u05D5\u05D3\u05D4! \u05D4\u05E6\u05D5\u05D5\u05EA \u05D9\u05D7\u05D6\u05D5\u05E8 \u05D0\u05DC\u05D9\u05DB\u05DD \u05D1\u05DE\u05D9\u05D9\u05DC.",
51
+ close: "\u05E1\u05D2\u05D9\u05E8\u05D4"
52
+ }
53
+ };
54
+ function randomId(prefix) {
55
+ const bytes = new Uint8Array(16);
56
+ crypto.getRandomValues(bytes);
57
+ const b64 = btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
58
+ return `${prefix}${b64}`;
59
+ }
60
+ var BrainerceBot = class _BrainerceBot {
61
+ constructor(options) {
62
+ this.settings = { enabled: false };
63
+ this.locale = "en";
64
+ this.sessionId = null;
65
+ this.conversationId = null;
66
+ this.busy = false;
67
+ this.opened = false;
68
+ this.destroyed = false;
69
+ this.connectionId = options.connectionId;
70
+ this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
71
+ this.storageKey = `brainerce-bot:${this.connectionId}`;
72
+ }
73
+ /** Boot the widget. Resolves to null when the bot is disabled server-side. */
74
+ static async mount(options) {
75
+ if (!options?.connectionId) {
76
+ console.warn("[BrainerceBot] connectionId is required");
77
+ return null;
78
+ }
79
+ const bot = new _BrainerceBot(options);
80
+ const ok = await bot.boot(options.target ?? document.body);
81
+ return ok ? bot : null;
82
+ }
83
+ destroy() {
84
+ this.destroyed = true;
85
+ this.host?.remove();
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ async boot(target) {
89
+ try {
90
+ const res = await fetch(
91
+ `${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/settings`
92
+ );
93
+ if (!res.ok) return false;
94
+ this.settings = await res.json();
95
+ } catch {
96
+ return false;
97
+ }
98
+ if (!this.settings.enabled) return false;
99
+ this.locale = this.settings.languages?.[0] ?? "en";
100
+ this.restoreIds();
101
+ this.render(target);
102
+ if (this.settings.displayMode === "auto_open") {
103
+ setTimeout(() => !this.destroyed && this.open(), 3e3);
104
+ }
105
+ return true;
106
+ }
107
+ t(key) {
108
+ return (CHROME[this.locale] ?? CHROME.en)[key] ?? CHROME.en[key] ?? key;
109
+ }
110
+ restoreIds() {
111
+ try {
112
+ const raw = localStorage.getItem(this.storageKey);
113
+ if (raw) {
114
+ const parsed = JSON.parse(raw);
115
+ this.sessionId = parsed.sessionId ?? null;
116
+ this.conversationId = parsed.conversationId ?? null;
117
+ }
118
+ } catch {
119
+ }
120
+ }
121
+ persistIds() {
122
+ try {
123
+ localStorage.setItem(
124
+ this.storageKey,
125
+ JSON.stringify({ sessionId: this.sessionId, conversationId: this.conversationId })
126
+ );
127
+ } catch {
128
+ }
129
+ }
130
+ // --------------------------------------------------------------------------
131
+ // Rendering
132
+ // --------------------------------------------------------------------------
133
+ render(target) {
134
+ const accent = this.settings.accentColor || "#6366F1";
135
+ const dir = RTL_LOCALES.has(this.locale) ? "rtl" : "ltr";
136
+ const radius = this.settings.bubbleShape === "square" ? "8px" : "16px";
137
+ const side = this.settings.position === "start" ? "left" : "right";
138
+ const sideRtlAware = dir === "rtl" ? side === "left" ? "right" : "left" : side;
139
+ this.host = document.createElement("div");
140
+ this.host.setAttribute("data-brainerce-bot", this.connectionId);
141
+ this.root = this.host.attachShadow({ mode: "open" });
142
+ const style = document.createElement("style");
143
+ style.textContent = `
144
+ :host { all: initial; }
145
+ * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
146
+ .bb-root { position: fixed; bottom: 20px; ${sideRtlAware}: 20px; z-index: 2147483000; direction: ${dir}; }
147
+ .bb-launcher {
148
+ width: 56px; height: 56px; border: none; cursor: pointer; display: flex;
149
+ align-items: center; justify-content: center; color: #fff; background: ${accent};
150
+ border-radius: ${this.settings.bubbleShape === "square" ? "14px" : "9999px"};
151
+ box-shadow: 0 8px 24px rgba(0,0,0,.22); transition: transform .15s ease;
152
+ overflow: hidden; padding: 0;
153
+ }
154
+ .bb-launcher:hover { transform: scale(1.06); }
155
+ .bb-launcher img { width: 100%; height: 100%; object-fit: cover; }
156
+ .bb-window {
157
+ position: absolute; bottom: 70px; ${sideRtlAware}: 0; width: 360px; max-width: calc(100vw - 32px);
158
+ height: 540px; max-height: calc(100vh - 110px); display: none; flex-direction: column;
159
+ background: #fff; border-radius: 16px; overflow: hidden;
160
+ box-shadow: 0 16px 48px rgba(0,0,0,.24); border: 1px solid rgba(0,0,0,.06);
161
+ }
162
+ .bb-window.open { display: flex; }
163
+ .bb-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: ${accent}; color: #fff; }
164
+ .bb-avatar { width: 34px; height: 34px; border-radius: 9999px; background: rgba(255,255,255,.25);
165
+ display: flex; align-items: center; justify-content: center; font-weight: 600; overflow: hidden; flex-shrink: 0; }
166
+ .bb-avatar img { width: 100%; height: 100%; object-fit: cover; }
167
+ .bb-head-main { flex: 1; min-width: 0; }
168
+ .bb-name { font-size: 14px; font-weight: 600; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
169
+ .bb-status { font-size: 11px; opacity: .9; display: flex; align-items: center; gap: 5px; }
170
+ .bb-dot { width: 6px; height: 6px; border-radius: 9999px; background: #34d399; }
171
+ .bb-iconbtn { background: none; border: none; color: #fff; cursor: pointer; opacity: .85; font-size: 16px; padding: 4px; }
172
+ .bb-iconbtn:hover { opacity: 1; }
173
+ .bb-messages { flex: 1; overflow-y: auto; padding: 14px; background: #f7f7f9; display: flex; flex-direction: column; gap: 8px; }
174
+ .bb-msg { max-width: 80%; padding: 9px 12px; border-radius: ${radius}; font-size: 13.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
175
+ .bb-msg.bot { align-self: flex-start; background: #fff; border: 1px solid rgba(0,0,0,.07); border-end-start-radius: 4px; }
176
+ .bb-msg.user { align-self: flex-end; background: ${accent}; color: #fff; border-end-end-radius: 4px; }
177
+ .bb-msg.err { align-self: flex-start; background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; }
178
+ .bb-typing { align-self: flex-start; font-size: 11.5px; color: #6b7280; padding: 2px 4px; }
179
+ .bb-card { align-self: flex-start; width: 230px; background: #fff; border: 1px solid rgba(0,0,0,.08);
180
+ border-radius: 12px; overflow: hidden; text-decoration: none; color: inherit; display: block; }
181
+ .bb-card img { width: 100%; height: 120px; object-fit: cover; display: block; background: #eee; }
182
+ .bb-card-body { padding: 9px 11px; }
183
+ .bb-card-title { font-size: 13px; font-weight: 600; margin: 0 0 3px; }
184
+ .bb-card-price { font-size: 13px; color: ${accent}; font-weight: 600; }
185
+ .bb-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px 10px; background: #f7f7f9; }
186
+ .bb-chip { border: 1px solid ${accent}; color: ${accent}; background: #fff; border-radius: 9999px;
187
+ font-size: 12px; padding: 5px 11px; cursor: pointer; }
188
+ .bb-inputrow { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid rgba(0,0,0,.07); background: #fff; }
189
+ .bb-input { flex: 1; border: none; outline: none; font-size: 13.5px; background: #f1f1f4; border-radius: 9999px; padding: 9px 14px; }
190
+ .bb-send { border: none; background: ${accent}; color: #fff; width: 36px; height: 36px; border-radius: 9999px; cursor: pointer; font-size: 15px; flex-shrink: 0; }
191
+ .bb-send:disabled { opacity: .5; cursor: default; }
192
+ .bb-esc { padding: 12px 14px; background: #fff; border-top: 1px solid rgba(0,0,0,.07); display: none; flex-direction: column; gap: 8px; }
193
+ .bb-esc.open { display: flex; }
194
+ .bb-esc input, .bb-esc textarea { border: 1px solid rgba(0,0,0,.12); border-radius: 8px; padding: 8px 10px; font-size: 13px; outline: none; resize: none; }
195
+ .bb-esc button { border: none; background: ${accent}; color: #fff; border-radius: 8px; padding: 8px; font-size: 13px; cursor: pointer; }
196
+ .bb-esc-note { font-size: 12px; color: #047857; }
197
+ `;
198
+ this.root.appendChild(style);
199
+ const rootEl = document.createElement("div");
200
+ rootEl.className = "bb-root";
201
+ this.root.appendChild(rootEl);
202
+ this.windowEl = document.createElement("div");
203
+ this.windowEl.className = "bb-window";
204
+ rootEl.appendChild(this.windowEl);
205
+ const name = this.settings.displayName || "Assistant";
206
+ const header = document.createElement("div");
207
+ header.className = "bb-header";
208
+ const avatar = document.createElement("span");
209
+ avatar.className = "bb-avatar";
210
+ if (this.settings.avatarUrl && isSafeUrl(this.settings.avatarUrl)) {
211
+ const img = document.createElement("img");
212
+ img.src = this.settings.avatarUrl;
213
+ img.alt = "";
214
+ avatar.appendChild(img);
215
+ } else {
216
+ avatar.textContent = name.charAt(0).toUpperCase();
217
+ }
218
+ const headMain = document.createElement("span");
219
+ headMain.className = "bb-head-main";
220
+ const nameEl = document.createElement("span");
221
+ nameEl.className = "bb-name";
222
+ nameEl.textContent = name;
223
+ const statusEl = document.createElement("span");
224
+ statusEl.className = "bb-status";
225
+ const dot = document.createElement("span");
226
+ dot.className = "bb-dot";
227
+ statusEl.appendChild(dot);
228
+ statusEl.appendChild(document.createTextNode(this.t("online")));
229
+ headMain.appendChild(nameEl);
230
+ headMain.appendChild(statusEl);
231
+ const escBtn = document.createElement("button");
232
+ escBtn.className = "bb-iconbtn";
233
+ escBtn.dataset.act = "esc";
234
+ escBtn.title = this.t("leaveMessage");
235
+ escBtn.textContent = "\u2709";
236
+ const closeBtn = document.createElement("button");
237
+ closeBtn.className = "bb-iconbtn";
238
+ closeBtn.dataset.act = "close";
239
+ closeBtn.title = this.t("close");
240
+ closeBtn.textContent = "\u2715";
241
+ header.appendChild(avatar);
242
+ header.appendChild(headMain);
243
+ header.appendChild(escBtn);
244
+ header.appendChild(closeBtn);
245
+ this.windowEl.appendChild(header);
246
+ header.querySelector('[data-act="close"]')?.addEventListener("click", () => this.close());
247
+ header.querySelector('[data-act="esc"]')?.addEventListener("click", () => this.toggleEscalation());
248
+ this.messagesEl = document.createElement("div");
249
+ this.messagesEl.className = "bb-messages";
250
+ this.windowEl.appendChild(this.messagesEl);
251
+ this.chipsEl = document.createElement("div");
252
+ this.chipsEl.className = "bb-chips";
253
+ for (const q of this.settings.starterQuestions ?? []) {
254
+ const chip = document.createElement("button");
255
+ chip.className = "bb-chip";
256
+ chip.textContent = q;
257
+ chip.addEventListener("click", () => this.send(q));
258
+ this.chipsEl.appendChild(chip);
259
+ }
260
+ this.windowEl.appendChild(this.chipsEl);
261
+ const esc = document.createElement("div");
262
+ esc.className = "bb-esc";
263
+ const escEmail = document.createElement("input");
264
+ escEmail.type = "email";
265
+ escEmail.name = "email";
266
+ escEmail.placeholder = this.t("yourEmail");
267
+ const escMsg = document.createElement("textarea");
268
+ escMsg.name = "message";
269
+ escMsg.rows = 2;
270
+ escMsg.placeholder = this.t("yourMessage");
271
+ const escSend = document.createElement("button");
272
+ escSend.type = "button";
273
+ escSend.textContent = this.t("send");
274
+ esc.appendChild(escEmail);
275
+ esc.appendChild(escMsg);
276
+ esc.appendChild(escSend);
277
+ esc.querySelector("button")?.addEventListener("click", () => this.submitEscalation(esc));
278
+ this.windowEl.appendChild(esc);
279
+ const inputRow = document.createElement("div");
280
+ inputRow.className = "bb-inputrow";
281
+ this.inputEl = document.createElement("input");
282
+ this.inputEl.className = "bb-input";
283
+ this.inputEl.placeholder = this.t("placeholder");
284
+ this.inputEl.addEventListener("keydown", (e) => {
285
+ if (e.key === "Enter") this.send(this.inputEl?.value ?? "");
286
+ });
287
+ const sendBtn = document.createElement("button");
288
+ sendBtn.className = "bb-send";
289
+ sendBtn.textContent = "\u27A4";
290
+ sendBtn.addEventListener("click", () => this.send(this.inputEl?.value ?? ""));
291
+ inputRow.appendChild(this.inputEl);
292
+ inputRow.appendChild(sendBtn);
293
+ this.windowEl.appendChild(inputRow);
294
+ const launcher = document.createElement("button");
295
+ launcher.className = "bb-launcher";
296
+ launcher.setAttribute("aria-label", name);
297
+ if (this.settings.avatarUrl && isSafeUrl(this.settings.avatarUrl)) {
298
+ const img = document.createElement("img");
299
+ img.src = this.settings.avatarUrl;
300
+ img.alt = "";
301
+ launcher.appendChild(img);
302
+ } else {
303
+ launcher.textContent = "\u{1F4AC}";
304
+ }
305
+ launcher.addEventListener("click", () => this.opened ? this.close() : this.open());
306
+ rootEl.appendChild(launcher);
307
+ target.appendChild(this.host);
308
+ }
309
+ open() {
310
+ if (!this.windowEl || this.opened) return;
311
+ this.opened = true;
312
+ this.windowEl.classList.add("open");
313
+ if (this.messagesEl && this.messagesEl.childElementCount === 0) {
314
+ void this.primeThread();
315
+ }
316
+ this.inputEl?.focus();
317
+ }
318
+ close() {
319
+ this.opened = false;
320
+ this.windowEl?.classList.remove("open");
321
+ }
322
+ /** First open: restore the server thread, or show the greeting. */
323
+ async primeThread() {
324
+ if (this.conversationId && this.sessionId) {
325
+ try {
326
+ const res = await fetch(
327
+ `${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/conversations/${encodeURIComponent(this.conversationId)}?limit=50`,
328
+ { headers: { "X-Bot-Session": this.sessionId } }
329
+ );
330
+ if (res.ok) {
331
+ const data = await res.json();
332
+ for (const m of data.data) {
333
+ this.appendMessage(m.role === "assistant" ? "bot" : "user", m.content);
334
+ }
335
+ if (data.data.length > 0) {
336
+ this.chipsEl?.remove();
337
+ return;
338
+ }
339
+ } else {
340
+ this.conversationId = null;
341
+ this.sessionId = null;
342
+ this.persistIds();
343
+ }
344
+ } catch {
345
+ }
346
+ }
347
+ if (this.settings.greeting) this.appendMessage("bot", this.settings.greeting);
348
+ }
349
+ // --------------------------------------------------------------------------
350
+ // The chat turn
351
+ // --------------------------------------------------------------------------
352
+ async send(text) {
353
+ const message = text.trim();
354
+ if (!message || this.busy) return;
355
+ this.busy = true;
356
+ if (this.inputEl) this.inputEl.value = "";
357
+ this.chipsEl?.remove();
358
+ this.appendMessage("user", message);
359
+ const typing = this.appendTyping();
360
+ let botBubble = null;
361
+ try {
362
+ const res = await fetch(
363
+ `${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,
364
+ {
365
+ method: "POST",
366
+ headers: { "Content-Type": "application/json" },
367
+ body: JSON.stringify({
368
+ message,
369
+ turnId: randomId("trn_"),
370
+ ...this.conversationId ? { conversationId: this.conversationId } : {},
371
+ ...this.sessionId ? { anonymousSessionId: this.sessionId } : {},
372
+ locale: this.locale
373
+ })
374
+ }
375
+ );
376
+ if (!res.ok || !res.body) {
377
+ throw new Error(`chat failed (${res.status})`);
378
+ }
379
+ const reader = res.body.getReader();
380
+ const decoder = new TextDecoder();
381
+ let buffer = "";
382
+ for (; ; ) {
383
+ const { value, done } = await reader.read();
384
+ if (done) break;
385
+ buffer += decoder.decode(value, { stream: true });
386
+ let idx;
387
+ while ((idx = buffer.indexOf("\n\n")) >= 0) {
388
+ const raw = buffer.slice(0, idx);
389
+ buffer = buffer.slice(idx + 2);
390
+ if (!raw.startsWith("data: ")) continue;
391
+ let frame;
392
+ try {
393
+ frame = JSON.parse(raw.slice(6));
394
+ } catch {
395
+ continue;
396
+ }
397
+ botBubble = this.handleFrame(frame, typing, botBubble);
398
+ }
399
+ }
400
+ } catch {
401
+ this.appendMessage("err", this.t("error"));
402
+ } finally {
403
+ typing.remove();
404
+ this.busy = false;
405
+ }
406
+ }
407
+ handleFrame(frame, typing, botBubble) {
408
+ switch (frame.type) {
409
+ case "connected":
410
+ this.conversationId = frame.conversationId || this.conversationId;
411
+ this.sessionId = frame.anonymousSessionId || this.sessionId;
412
+ this.persistIds();
413
+ return botBubble;
414
+ case "token": {
415
+ if (!botBubble) {
416
+ typing.remove();
417
+ botBubble = this.appendMessage("bot", "");
418
+ }
419
+ botBubble.textContent = (botBubble.textContent ?? "") + frame.text;
420
+ this.scrollDown();
421
+ return botBubble;
422
+ }
423
+ case "tool":
424
+ typing.textContent = frame.status === "running" ? "\u22EF" : "";
425
+ return botBubble;
426
+ case "card":
427
+ this.appendCard(frame.card);
428
+ return botBubble;
429
+ case "error":
430
+ this.appendMessage("err", frame.message || this.t("error"));
431
+ return botBubble;
432
+ case "done":
433
+ default:
434
+ return botBubble;
435
+ }
436
+ }
437
+ // --------------------------------------------------------------------------
438
+ // Escalation + attribution
439
+ // --------------------------------------------------------------------------
440
+ toggleEscalation() {
441
+ this.root?.querySelector(".bb-esc")?.classList.toggle("open");
442
+ }
443
+ async submitEscalation(form) {
444
+ const email = form.querySelector('input[name="email"]')?.value.trim();
445
+ const message = form.querySelector('textarea[name="message"]')?.value.trim();
446
+ if (!email || !message || !this.conversationId || !this.sessionId) return;
447
+ try {
448
+ const res = await fetch(
449
+ `${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/escalate`,
450
+ {
451
+ method: "POST",
452
+ headers: { "Content-Type": "application/json" },
453
+ body: JSON.stringify({
454
+ email,
455
+ message,
456
+ conversationId: this.conversationId,
457
+ anonymousSessionId: this.sessionId,
458
+ locale: this.locale
459
+ })
460
+ }
461
+ );
462
+ if (res.ok) {
463
+ const note = document.createElement("span");
464
+ note.className = "bb-esc-note";
465
+ note.textContent = this.t("sent");
466
+ form.replaceChildren(note);
467
+ }
468
+ } catch {
469
+ }
470
+ }
471
+ appendCard(card) {
472
+ if (!this.messagesEl) return;
473
+ const a = document.createElement("a");
474
+ a.className = "bb-card";
475
+ a.href = isSafeUrl(card.url) ? card.url : "#";
476
+ if (card.imageUrl && isSafeUrl(card.imageUrl)) {
477
+ const img = document.createElement("img");
478
+ img.src = card.imageUrl;
479
+ img.alt = "";
480
+ a.appendChild(img);
481
+ }
482
+ const body = document.createElement("span");
483
+ body.className = "bb-card-body";
484
+ const title = document.createElement("p");
485
+ title.className = "bb-card-title";
486
+ title.textContent = card.title;
487
+ const priceEl = document.createElement("span");
488
+ priceEl.className = "bb-card-price";
489
+ priceEl.textContent = card.price.formatted;
490
+ body.appendChild(title);
491
+ body.appendChild(priceEl);
492
+ a.appendChild(body);
493
+ a.addEventListener("click", () => {
494
+ try {
495
+ navigator.sendBeacon?.(
496
+ `${this.baseUrl}/api/storefront-bot/attribution/click`,
497
+ new Blob([JSON.stringify({ botRef: card.botRef })], { type: "application/json" })
498
+ );
499
+ } catch {
500
+ }
501
+ });
502
+ this.messagesEl.appendChild(a);
503
+ this.scrollDown();
504
+ }
505
+ // --------------------------------------------------------------------------
506
+ appendMessage(kind, text) {
507
+ const el = document.createElement("div");
508
+ el.className = `bb-msg ${kind}`;
509
+ el.textContent = text;
510
+ this.messagesEl?.appendChild(el);
511
+ this.scrollDown();
512
+ return el;
513
+ }
514
+ appendTyping() {
515
+ const el = document.createElement("div");
516
+ el.className = "bb-typing";
517
+ el.textContent = "\u22EF";
518
+ this.messagesEl?.appendChild(el);
519
+ this.scrollDown();
520
+ return el;
521
+ }
522
+ scrollDown() {
523
+ if (this.messagesEl) this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
524
+ }
525
+ };
526
+ function isSafeUrl(url) {
527
+ return /^\/(?!\/)/.test(url) || /^https?:\/\//i.test(url);
528
+ }
529
+ // Annotate the CommonJS export names for ESM import in node:
530
+ 0 && (module.exports = {
531
+ BrainerceBot
532
+ });