brainerce 1.31.0 → 1.33.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.
@@ -11,7 +11,17 @@ var CHROME = {
11
11
  yourMessage: "Your message",
12
12
  send: "Send",
13
13
  sent: "Thanks! The team will get back to you by email.",
14
- close: "Close"
14
+ close: "Close",
15
+ expand: "Expand",
16
+ collapse: "Collapse",
17
+ searching: "Searching the store\u2026",
18
+ addToCart: "Add to cart",
19
+ added: "Added",
20
+ view: "View",
21
+ chooseOptions: "View product",
22
+ results: "From the store",
23
+ poweredBy: "Powered by Brainerce",
24
+ addFailed: "I couldn\u2019t add that to the cart \u2014 try the button on the product card."
15
25
  },
16
26
  he: {
17
27
  online: "\u05DE\u05D7\u05D5\u05D1\u05E8",
@@ -22,15 +32,111 @@ var CHROME = {
22
32
  yourMessage: "\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",
23
33
  send: "\u05E9\u05DC\u05D9\u05D7\u05D4",
24
34
  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.",
25
- close: "\u05E1\u05D2\u05D9\u05E8\u05D4"
35
+ close: "\u05E1\u05D2\u05D9\u05E8\u05D4",
36
+ expand: "\u05D4\u05E8\u05D7\u05D1\u05D4",
37
+ collapse: "\u05DB\u05D9\u05D5\u05D5\u05E5",
38
+ searching: "\u05DE\u05D7\u05E4\u05E9 \u05D1\u05D7\u05E0\u05D5\u05EA\u2026",
39
+ addToCart: "\u05D4\u05D5\u05E1\u05E4\u05D4 \u05DC\u05E1\u05DC",
40
+ added: "\u05E0\u05D5\u05E1\u05E3",
41
+ view: "\u05E6\u05E4\u05D9\u05D9\u05D4",
42
+ chooseOptions: "\u05DC\u05E6\u05E4\u05D5\u05EA \u05D1\u05DE\u05D5\u05E6\u05E8",
43
+ results: "\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA",
44
+ poweredBy: "\u05DE\u05D5\u05E4\u05E2\u05DC \u05E2\u05DC \u05D9\u05D3\u05D9 Brainerce",
45
+ addFailed: "\u05DC\u05D0 \u05D4\u05E6\u05DC\u05D7\u05EA\u05D9 \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05DC\u05E1\u05DC \u2014 \u05E0\u05E1\u05D5 \u05D3\u05E8\u05DA \u05D4\u05DB\u05E4\u05EA\u05D5\u05E8 \u05D1\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D4\u05DE\u05D5\u05E6\u05E8."
26
46
  }
27
47
  };
48
+ var ICONS = {
49
+ chat: '<svg viewBox="0 0 24 24" fill="none"><path d="M12 3C7.03 3 3 6.58 3 11c0 2.04.86 3.9 2.28 5.32-.15 1.23-.62 2.39-1.1 3.21-.13.23.05.52.31.47 1.56-.27 3.07-.93 4.13-1.62A10.6 10.6 0 0 0 12 19c4.97 0 9-3.58 9-8s-4.03-8-9-8Z" fill="currentColor"/></svg>',
50
+ close: '<svg viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
51
+ expand: '<svg viewBox="0 0 24 24" fill="none"><path d="M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
52
+ collapse: '<svg viewBox="0 0 24 24" fill="none"><path d="M20 10h-6V4M4 14h6v6M20 4l-6 6M4 20l6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
53
+ mail: '<svg viewBox="0 0 24 24" fill="none"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke="currentColor" stroke-width="1.7"/><path d="m4.5 7.5 7.5 5.5 7.5-5.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
54
+ send: '<svg viewBox="0 0 24 24" fill="none"><path d="M4.4 11.2 19 4.6c.7-.3 1.4.4 1.1 1.1l-6.6 14.6c-.3.7-1.3.6-1.5-.1l-1.7-5.4a1 1 0 0 0-.6-.6l-5.4-1.7c-.7-.2-.8-1.2-.1-1.5Z" fill="currentColor"/></svg>',
55
+ cart: '<svg viewBox="0 0 24 24" fill="none"><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.36 17H17.5a2 2 0 0 0 1.95-1.55L21 8H6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="10" cy="20.5" r="1.4" fill="currentColor"/><circle cx="17" cy="20.5" r="1.4" fill="currentColor"/></svg>',
56
+ check: '<svg viewBox="0 0 24 24" fill="none"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
57
+ arrow: '<svg viewBox="0 0 24 24" fill="none"><path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>',
58
+ // Brainerce brand mark for the Powered-by footer: the gradient "B"
59
+ // (emerald→violet), matching the dashboard BrandedLoader identity.
60
+ brand: '<svg viewBox="0 0 24 24"><defs><linearGradient id="bb-brand-g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs><rect x="1.5" y="1.5" width="21" height="21" rx="6" fill="url(#bb-brand-g)"/><text x="12" y="17" text-anchor="middle" font-family="ui-sans-serif,system-ui,sans-serif" font-size="14" font-weight="700" fill="#fff">B</text></svg>'
61
+ };
62
+ var svgParser = typeof DOMParser !== "undefined" ? new DOMParser() : null;
63
+ function icon(name) {
64
+ const markup = (ICONS[name] ?? ICONS.chat).replace(
65
+ "<svg ",
66
+ '<svg xmlns="http://www.w3.org/2000/svg" '
67
+ );
68
+ const doc = svgParser?.parseFromString(markup, "image/svg+xml");
69
+ const el = doc?.documentElement;
70
+ if (!el || el.nodeName === "parsererror") return document.createTextNode("");
71
+ el.setAttribute("aria-hidden", "true");
72
+ el.setAttribute("class", "bb-ic");
73
+ return el;
74
+ }
28
75
  function randomId(prefix) {
29
76
  const bytes = new Uint8Array(16);
30
77
  crypto.getRandomValues(bytes);
31
78
  const b64 = btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
32
79
  return `${prefix}${b64}`;
33
80
  }
81
+ function isSafeUrl(url) {
82
+ return /^\/(?!\/)/.test(url) || /^https?:\/\//i.test(url);
83
+ }
84
+ var INLINE_RE = /\*\*([^*\n]+)\*\*|\[([^\]\n]+)\]\(([^)\s]+)\)/g;
85
+ function appendInline(parent, text) {
86
+ let last = 0;
87
+ INLINE_RE.lastIndex = 0;
88
+ for (let m = INLINE_RE.exec(text); m; m = INLINE_RE.exec(text)) {
89
+ if (m.index > last) parent.appendChild(document.createTextNode(text.slice(last, m.index)));
90
+ if (m[1] !== void 0) {
91
+ const b = document.createElement("strong");
92
+ b.textContent = m[1];
93
+ parent.appendChild(b);
94
+ } else if (isSafeUrl(m[3])) {
95
+ const a = document.createElement("a");
96
+ a.href = m[3];
97
+ a.target = "_blank";
98
+ a.rel = "noopener noreferrer";
99
+ a.textContent = m[2];
100
+ parent.appendChild(a);
101
+ } else {
102
+ parent.appendChild(document.createTextNode(m[2]));
103
+ }
104
+ last = m.index + m[0].length;
105
+ }
106
+ if (last < text.length) parent.appendChild(document.createTextNode(text.slice(last)));
107
+ }
108
+ function renderRich(el, text) {
109
+ el.replaceChildren();
110
+ const lines = text.split("\n");
111
+ let list = null;
112
+ let para = null;
113
+ for (const line of lines) {
114
+ const bullet = /^\s*[-•*]\s+(.*)$/.exec(line);
115
+ if (bullet) {
116
+ para = null;
117
+ if (!list) {
118
+ list = document.createElement("ul");
119
+ el.appendChild(list);
120
+ }
121
+ const li = document.createElement("li");
122
+ appendInline(li, bullet[1]);
123
+ list.appendChild(li);
124
+ continue;
125
+ }
126
+ list = null;
127
+ if (!line.trim()) {
128
+ para = null;
129
+ continue;
130
+ }
131
+ if (!para) {
132
+ para = document.createElement("p");
133
+ el.appendChild(para);
134
+ } else {
135
+ para.appendChild(document.createElement("br"));
136
+ }
137
+ appendInline(para, line);
138
+ }
139
+ }
34
140
  var BrainerceBot = class _BrainerceBot {
35
141
  constructor(options) {
36
142
  this.settings = { enabled: false };
@@ -39,10 +145,21 @@ var BrainerceBot = class _BrainerceBot {
39
145
  this.conversationId = null;
40
146
  this.busy = false;
41
147
  this.opened = false;
148
+ this.expanded = false;
42
149
  this.destroyed = false;
150
+ /** Per-turn streaming state. */
151
+ this.pendingText = "";
152
+ this.cardsRow = null;
153
+ this.cardIds = /* @__PURE__ */ new Set();
154
+ /**
155
+ * The large dialog owns the screen — the page behind must not scroll.
156
+ * Locks <body> while a big surface is open; restores on close/collapse.
157
+ */
158
+ this.prevBodyOverflow = null;
43
159
  this.connectionId = options.connectionId;
44
160
  this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
45
161
  this.storageKey = `brainerce-bot:${this.connectionId}`;
162
+ this.onAddToCart = options.onAddToCart;
46
163
  }
47
164
  /** Boot the widget. Resolves to null when the bot is disabled server-side. */
48
165
  static async mount(options) {
@@ -56,6 +173,10 @@ var BrainerceBot = class _BrainerceBot {
56
173
  }
57
174
  destroy() {
58
175
  this.destroyed = true;
176
+ if (this.prevBodyOverflow !== null) {
177
+ document.body.style.overflow = this.prevBodyOverflow;
178
+ this.prevBodyOverflow = null;
179
+ }
59
180
  this.host?.remove();
60
181
  }
61
182
  // ---------------------------------------------------------------------------
@@ -102,81 +223,290 @@ var BrainerceBot = class _BrainerceBot {
102
223
  }
103
224
  }
104
225
  // --------------------------------------------------------------------------
105
- // Rendering
226
+ // Rendering — surface system
106
227
  // --------------------------------------------------------------------------
228
+ css(accent, dir, side) {
229
+ const square = this.settings.bubbleShape === "square";
230
+ const rWindow = square ? "14px" : "22px";
231
+ const rBubble = square ? "8px" : "15px";
232
+ const rTight = "5px";
233
+ const mode = this.settings.displayMode ?? "floating";
234
+ const isRail = mode === "side_rail";
235
+ const isFull = mode === "full_screen";
236
+ return `
237
+ :host { all: initial; }
238
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0;
239
+ font-family: -apple-system, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI", system-ui, "Helvetica Neue", sans-serif;
240
+ -webkit-font-smoothing: antialiased; }
241
+ button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; }
242
+ .bb { position: fixed; bottom: 24px; ${side}: 24px; z-index: 2147483000; direction: ${dir};
243
+ /* product-card density scales with the window size */
244
+ --bb-card-w: 148px; --bb-img-h: 84px; --bb-title-fs: 11.5px; --bb-price-fs: 12.5px; --bb-btn-h: 27px; }
245
+ /* .big = any large surface (expanded, full-screen mode, mobile takeover) */
246
+ .bb.big { --bb-card-w: 216px; --bb-img-h: 132px; --bb-title-fs: 13px; --bb-price-fs: 14px; --bb-btn-h: 33px; }
247
+ .bb.big .bb-msgs, .bb.big .bb-chips, .bb.big .bb-esc {
248
+ padding-inline: max(20px, calc((100% - 1340px) / 2)); }
249
+ .bb.big .bb-msgs { gap: 12px; padding-top: 22px; }
250
+ .bb.big .bb-msg { font-size: 14px; padding: 11px 15px; }
251
+ .bb.big .bb-header { padding: 15px 22px; }
252
+ /* Big-surface composer: a full-width message box with the send inside */
253
+ .bb.big .bb-composer { position: relative; padding: 16px 20px 20px; }
254
+ .bb.big .bb-input { min-height: 116px; border-radius: 18px; background: #fff;
255
+ border: 1px solid #e3e5ec; padding: 16px 20px 52px; font-size: 15px;
256
+ overflow-y: auto; box-shadow: 0 2px 8px -2px rgba(15,18,34,.06); }
257
+ .bb.big .bb-send { position: absolute; bottom: 34px; inset-inline-end: 36px;
258
+ width: 42px; height: 42px; }
259
+ .bb.big .bb-send .bb-ic { width: 19px; height: 19px; }
260
+ /* Backdrop for the large dialog: dim + blur the storefront behind it */
261
+ .bb-scrim { position: fixed; inset: 0; background: rgba(15,18,34,.35);
262
+ -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
263
+ opacity: 0; pointer-events: none; transition: opacity .25s ease; }
264
+ .bb.open.big .bb-scrim { opacity: 1; pointer-events: auto; }
265
+
266
+ /* ---- launcher ------------------------------------------------------ */
267
+ .bb-launcher {
268
+ width: 58px; height: 58px; position: relative; display: flex; align-items: center;
269
+ justify-content: center; color: #fff; background: ${accent};
270
+ border-radius: ${square ? "16px" : "999px"}; overflow: hidden;
271
+ box-shadow: 0 6px 16px -4px color-mix(in srgb, ${accent} 55%, rgba(10,12,30,.4)), 0 2px 6px rgba(10,12,30,.18);
272
+ transition: transform .22s cubic-bezier(.34,1.56,.64,1), box-shadow .22s ease;
273
+ }
274
+ .bb-launcher:hover { transform: scale(1.07) translateY(-1px); }
275
+ .bb-launcher:active { transform: scale(.96); }
276
+ .bb-launcher img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
277
+ transition: opacity .18s ease, transform .25s ease; }
278
+ .bb-launcher .bb-ic { width: 26px; height: 26px; position: absolute;
279
+ transition: opacity .18s ease, transform .25s ease; }
280
+ .bb-launcher .bb-l-close { opacity: 0; transform: rotate(-90deg) scale(.6); }
281
+ .bb.open .bb-launcher .bb-l-chat, .bb.open .bb-launcher img { opacity: 0; transform: rotate(90deg) scale(.6); }
282
+ .bb.open .bb-launcher img { transform: scale(1.15); }
283
+ .bb.open .bb-launcher .bb-l-close { opacity: 1; transform: rotate(0) scale(1); }
284
+
285
+ /* ---- window -------------------------------------------------------- */
286
+ .bb-window {
287
+ position: absolute; bottom: 74px; ${side}: 0;
288
+ width: 384px; max-width: calc(100vw - 32px);
289
+ height: min(620px, calc(100vh - 122px));
290
+ display: flex; flex-direction: column; background: #fff;
291
+ border-radius: ${rWindow}; overflow: hidden;
292
+ box-shadow: 0 24px 64px -16px rgba(15,18,34,.28), 0 6px 20px -6px rgba(15,18,34,.14), 0 0 0 1px rgba(15,18,34,.05);
293
+ opacity: 0; transform: translateY(10px) scale(.97);
294
+ transform-origin: bottom ${side === "left" ? "left" : "right"};
295
+ pointer-events: none;
296
+ transition: opacity .2s ease, transform .24s cubic-bezier(.22,1.2,.36,1);
297
+ }
298
+ .bb.open .bb-window { opacity: 1; transform: none; pointer-events: auto; }
299
+ /* Shopper-expanded = the large centered dialog (same as full-screen mode) */
300
+ .bb.expanded .bb-window { position: fixed;
301
+ inset: min(6vh, 60px) max(24px, calc((100vw - 1680px) / 2));
302
+ width: auto; max-width: none; height: auto; }
303
+ ${isRail ? `.bb-window { bottom: 0; top: auto; ${side}: 0; height: calc(100vh - 98px); border-end-start-radius: ${rWindow}; }
304
+ .bb.expanded .bb-window { width: min(560px, calc(100vw - 40px)); }` : ""}
305
+ ${isFull ? `.bb.open .bb-window { position: fixed;
306
+ inset: min(6vh, 60px) max(24px, calc((100vw - 1680px) / 2));
307
+ width: auto; max-width: none; height: auto; }
308
+ .bb.open .bb-launcher { opacity: 0; pointer-events: none; }` : ""}
309
+ @media (max-width: 520px) {
310
+ .bb { bottom: 16px; ${side}: 16px; }
311
+ .bb.open .bb-window { position: fixed; inset: 0; width: 100%; max-width: none; height: 100%; max-height: none; border-radius: 0; }
312
+ .bb.open .bb-launcher { opacity: 0; pointer-events: none; }
313
+ }
314
+
315
+ /* ---- header -------------------------------------------------------- */
316
+ .bb-header { display: flex; align-items: center; gap: 11px; padding: 13px 16px;
317
+ background: #fff; border-bottom: 1px solid #eef0f4; flex-shrink: 0; }
318
+ .bb-avatar { width: 38px; height: 38px; position: relative; flex-shrink: 0;
319
+ border-radius: 999px; background: color-mix(in srgb, ${accent} 14%, #fff);
320
+ color: ${accent}; font-size: 15px; font-weight: 700;
321
+ display: flex; align-items: center; justify-content: center;
322
+ box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px color-mix(in srgb, ${accent} 35%, #fff); }
323
+ .bb-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
324
+ .bb-avatar::after { content: ''; position: absolute; bottom: -1px; inset-inline-end: -1px;
325
+ width: 10px; height: 10px; border-radius: 999px; background: #22c55e; border: 2px solid #fff; }
326
+ .bb-head-main { flex: 1; min-width: 0; }
327
+ .bb-name { display: block; font-size: 13.5px; font-weight: 650; color: #14161f;
328
+ letter-spacing: -.01em; line-height: 1.25;
329
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
330
+ .bb-status { display: block; font-size: 11px; color: #8a8f9e; line-height: 1.3; }
331
+ .bb-actions { display: flex; align-items: center; gap: 2px; }
332
+ .bb-iconbtn { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
333
+ border-radius: 8px; color: #9aa0ae; transition: background .12s ease, color .12s ease; }
334
+ .bb-iconbtn:hover { background: #f3f4f7; color: #4d5260; }
335
+ .bb-iconbtn .bb-ic { width: 17px; height: 17px; }
336
+
337
+ /* ---- messages ------------------------------------------------------ */
338
+ .bb-msgs { flex: 1; overflow-y: auto; overscroll-behavior: contain;
339
+ padding: 16px 14px 10px; display: flex; flex-direction: column; gap: 10px;
340
+ background: linear-gradient(color-mix(in srgb, ${accent} 4%, #f7f8fa), #f7f8fa 140px); }
341
+ .bb-msgs::-webkit-scrollbar { width: 5px; }
342
+ .bb-msgs::-webkit-scrollbar-thumb { background: rgba(20,22,31,.12); border-radius: 99px; }
343
+ .bb-msg { max-width: 84%; padding: 9px 13px; font-size: 13px; line-height: 1.55;
344
+ word-break: break-word; animation: bb-in .2s ease both; unicode-bidi: plaintext; }
345
+ .bb-msg p + p, .bb-msg p + ul, .bb-msg ul + p { margin-top: 6px; }
346
+ .bb-msg ul { padding-inline-start: 18px; }
347
+ .bb-msg li { margin: 2px 0; }
348
+ .bb-msg a { color: ${accent}; font-weight: 550; text-decoration: underline; text-underline-offset: 2px; }
349
+ .bb-msg.bot { align-self: flex-start; background: #fff; color: #232633;
350
+ border: 1px solid #eceef3; border-radius: ${rBubble}; border-end-start-radius: ${rTight};
351
+ box-shadow: 0 1px 2px rgba(15,18,34,.04); }
352
+ .bb-msg.user { align-self: flex-end; background: ${accent}; color: #fff;
353
+ border-radius: ${rBubble}; border-end-end-radius: ${rTight};
354
+ box-shadow: 0 2px 6px -2px color-mix(in srgb, ${accent} 50%, rgba(10,12,30,.3)); }
355
+ .bb-msg.err { align-self: flex-start; background: #fef2f2; color: #b91c1c;
356
+ border: 1px solid #fecaca; border-radius: ${rBubble}; }
357
+ @keyframes bb-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
358
+
359
+ /* typing / tool status */
360
+ .bb-typing { align-self: flex-start; display: flex; align-items: center; gap: 8px;
361
+ background: #fff; border: 1px solid #eceef3; border-radius: ${rBubble};
362
+ border-end-start-radius: ${rTight}; padding: 10px 13px; animation: bb-in .2s ease both; }
363
+ .bb-typing .bb-dots { display: flex; gap: 4px; }
364
+ .bb-typing .bb-dots i { width: 6px; height: 6px; border-radius: 99px;
365
+ background: color-mix(in srgb, ${accent} 65%, #aab); animation: bb-bounce 1.2s ease-in-out infinite; }
366
+ .bb-typing .bb-dots i:nth-child(2) { animation-delay: .15s; }
367
+ .bb-typing .bb-dots i:nth-child(3) { animation-delay: .3s; }
368
+ .bb-typing .bb-tool { font-size: 11.5px; color: #8a8f9e; display: none; }
369
+ .bb-typing.searching .bb-tool { display: block; }
370
+ @keyframes bb-bounce { 0%, 60%, 100% { transform: none; opacity: .55; } 30% { transform: translateY(-4px); opacity: 1; } }
371
+
372
+ /* ---- product cards ------------------------------------------------- */
373
+ .bb-shelf { align-self: stretch; animation: bb-in .25s ease both; }
374
+ .bb-shelf-cap { font-size: 10.5px; font-weight: 650; letter-spacing: .07em; text-transform: uppercase;
375
+ color: #9aa0ae; padding: 2px 4px 6px; }
376
+ .bb-cards { display: flex; gap: 10px; overflow-x: auto; padding: 2px 2px 8px;
377
+ scroll-snap-type: x proximity; scrollbar-width: none; }
378
+ .bb-cards::-webkit-scrollbar { display: none; }
379
+ .bb-card { flex: 0 0 var(--bb-card-w); scroll-snap-align: start; background: #fff;
380
+ border: 1px solid #eceef3; border-radius: 12px; overflow: hidden;
381
+ display: flex; flex-direction: column;
382
+ box-shadow: 0 1px 2px rgba(15,18,34,.04);
383
+ transition: transform .16s ease, box-shadow .16s ease; }
384
+ .bb-card:hover { transform: translateY(-2px); box-shadow: 0 8px 20px -8px rgba(15,18,34,.18); }
385
+ .bb-card-img { display: flex; width: 100%; height: var(--bb-img-h); flex: 0 0 var(--bb-img-h); overflow: hidden; background: #f1f2f5; cursor: pointer; }
386
+ .bb-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
387
+ .bb-card-img .bb-ic { width: 22px; height: 22px; color: #c6c9d4; margin: auto; }
388
+ .bb-card-body { padding: 8px 10px 10px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
389
+ .bb-card-title { font-size: var(--bb-title-fs); font-weight: 600; color: #1c1e29; line-height: 1.35;
390
+ letter-spacing: -.005em; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
391
+ overflow: hidden; min-height: 2.7em; cursor: pointer; unicode-bidi: plaintext; }
392
+ .bb-card-title:hover { color: ${accent}; }
393
+ .bb-card-price { font-size: var(--bb-price-fs); font-weight: 700; color: #14161f;
394
+ font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
395
+ .bb-card-cta { display: flex; gap: 5px; margin-top: auto; }
396
+ .bb-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 5px;
397
+ height: var(--bb-btn-h); border-radius: 8px; font-size: 11px; font-weight: 650;
398
+ white-space: nowrap; padding: 0 6px;
399
+ transition: filter .12s ease, background .12s ease, color .12s ease; }
400
+ .bb-btn .bb-ic { width: 13px; height: 13px; }
401
+ .bb-btn-add { background: ${accent}; color: #fff; }
402
+ .bb-btn-add:hover { filter: brightness(1.08); }
403
+ .bb-btn-add[data-state="busy"] { opacity: .65; pointer-events: none; }
404
+ .bb-btn-add[data-state="done"] { background: #059669; pointer-events: none; }
405
+ .bb-btn-ghost { background: #fff; border: 1px solid #e3e5ec; color: #3c4150; flex: 0 0 auto; padding: 0 11px; }
406
+ .bb-btn-ghost:hover { background: #f6f7f9; border-color: #d5d8e1; }
407
+ .bb-btn-ghost.bb-wide { flex: 1; color: ${accent}; border-color: color-mix(in srgb, ${accent} 35%, #e3e5ec); }
408
+ .bb-btn-ghost.bb-wide:hover { background: color-mix(in srgb, ${accent} 6%, #fff); }
409
+ .bb-btn:disabled { opacity: .45; pointer-events: none; }
410
+
411
+ /* in-card variant picker */
412
+ .bb-card.picking .bb-card-cta { display: none; }
413
+ .bb-pick { display: flex; flex-direction: column; gap: 6px; margin-top: 4px;
414
+ padding-top: 8px; border-top: 1px dashed #e9ebf0; position: relative; }
415
+ .bb-pick-close { position: absolute; top: 6px; inset-inline-end: 0;
416
+ width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
417
+ border-radius: 6px; color: #9aa0ae; }
418
+ .bb-pick-close:hover { background: #f3f4f7; color: #4d5260; }
419
+ .bb-pick-close .bb-ic { width: 11px; height: 11px; }
420
+ .bb-pick-key { font-size: 9.5px; font-weight: 700; text-transform: uppercase;
421
+ letter-spacing: .06em; color: #9aa0ae; unicode-bidi: plaintext; }
422
+ .bb-pick-vals { display: flex; flex-wrap: wrap; gap: 4px; }
423
+ .bb-chipv { border: 1px solid #e3e5ec; border-radius: 7px; padding: 3px 9px;
424
+ font-size: 11px; font-weight: 550; color: #3c4150; background: #fff;
425
+ transition: border-color .12s ease, background .12s ease, color .12s ease; }
426
+ .bb-chipv:hover { border-color: color-mix(in srgb, ${accent} 50%, #e3e5ec); }
427
+ .bb-chipv.sel { background: ${accent}; border-color: ${accent}; color: #fff; }
428
+ .bb-chipv.off { opacity: .35; pointer-events: none; }
429
+ .bb-pick-foot { display: flex; align-items: center; justify-content: space-between;
430
+ gap: 6px; margin-top: 2px; }
431
+ .bb-pick-foot .bb-btn { flex: 0 0 auto; padding: 0 11px; }
432
+ .bb-pick-price { font-size: 12.5px; font-weight: 700; color: #14161f;
433
+ font-variant-numeric: tabular-nums; }
434
+
435
+ /* ---- starter chips -------------------------------------------------- */
436
+ .bb-chips { display: flex; flex-wrap: wrap; gap: 7px; padding: 4px 14px 12px; background: #f7f8fa; }
437
+ .bb-chip { border: 1px solid color-mix(in srgb, ${accent} 30%, #e3e5ec);
438
+ color: color-mix(in srgb, ${accent} 85%, #000); background: #fff; border-radius: 999px;
439
+ font-size: 12px; font-weight: 550; padding: 6px 13px;
440
+ transition: background .12s ease, transform .12s ease; }
441
+ .bb-chip:hover { background: color-mix(in srgb, ${accent} 7%, #fff); transform: translateY(-1px); }
442
+
443
+ /* ---- escalation sheet ----------------------------------------------- */
444
+ .bb-esc { display: none; flex-direction: column; gap: 8px; padding: 12px 14px;
445
+ background: #fff; border-top: 1px solid #eef0f4; flex-shrink: 0; }
446
+ .bb-esc.open { display: flex; animation: bb-in .18s ease both; }
447
+ .bb-esc-title { font-size: 12px; font-weight: 650; color: #3c4150; }
448
+ .bb-esc input, .bb-esc textarea { border: 1px solid #e3e5ec; border-radius: 10px;
449
+ padding: 8px 11px; font-size: 12.5px; color: #1c1e29; outline: none; resize: none; background: #fbfbfd; }
450
+ .bb-esc input:focus, .bb-esc textarea:focus { border-color: color-mix(in srgb, ${accent} 55%, #e3e5ec);
451
+ box-shadow: 0 0 0 3px color-mix(in srgb, ${accent} 12%, transparent); background: #fff; }
452
+ .bb-esc-send { height: 33px; border-radius: 10px; background: ${accent}; color: #fff;
453
+ font-size: 12.5px; font-weight: 650; }
454
+ .bb-esc-send:hover { filter: brightness(1.08); }
455
+ .bb-esc-note { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: #047857; font-weight: 550; }
456
+ .bb-esc-note .bb-ic { width: 15px; height: 15px; }
457
+
458
+ /* ---- composer ------------------------------------------------------- */
459
+ .bb-composer { display: flex; align-items: center; gap: 8px; padding: 11px 12px;
460
+ background: #fff; border-top: 1px solid #eef0f4; flex-shrink: 0; }
461
+ .bb-input { flex: 1; border: 1px solid transparent; outline: none; font-size: 13px; color: #1c1e29;
462
+ background: #f1f2f5; border-radius: 999px; padding: 9px 15px; min-width: 0;
463
+ resize: none; overflow-y: hidden; height: 38px; line-height: 1.45;
464
+ transition: border-color .12s ease, box-shadow .12s ease, background .12s ease; }
465
+ .bb-input::placeholder { color: #9aa0ae; }
466
+ .bb-input:focus { background: #fff; border-color: color-mix(in srgb, ${accent} 55%, #e3e5ec);
467
+ box-shadow: 0 0 0 3px color-mix(in srgb, ${accent} 12%, transparent); }
468
+ .bb-send { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center;
469
+ justify-content: center; border-radius: 999px; background: ${accent}; color: #fff;
470
+ transition: transform .15s cubic-bezier(.34,1.56,.64,1), opacity .12s ease, filter .12s ease; }
471
+ .bb-send .bb-ic { width: 17px; height: 17px; ${dir === "rtl" ? "transform: scaleX(-1);" : ""} }
472
+ .bb-send:hover { filter: brightness(1.08); transform: scale(1.06); }
473
+ .bb-send:disabled { opacity: .4; pointer-events: none; }
474
+
475
+ /* Powered-by footer */
476
+ .bb-foot { flex-shrink: 0; display: flex; justify-content: center; align-items: center;
477
+ padding: 5px 12px 7px; background: #fff; }
478
+ .bb-foot a { display: inline-flex; align-items: center; gap: 5px; font-size: 10.5px;
479
+ font-weight: 500; color: #aab0bd; text-decoration: none; letter-spacing: .005em;
480
+ transition: color .12s ease; }
481
+ .bb-foot a:hover { color: #6b7280; }
482
+ .bb-foot .bb-ic { width: 13px; height: 13px; border-radius: 4px; }
483
+ `;
484
+ }
107
485
  render(target) {
108
486
  const accent = this.settings.accentColor || "#6366F1";
109
487
  const dir = RTL_LOCALES.has(this.locale) ? "rtl" : "ltr";
110
- const radius = this.settings.bubbleShape === "square" ? "8px" : "16px";
111
488
  const side = this.settings.position === "start" ? "left" : "right";
112
489
  const sideRtlAware = dir === "rtl" ? side === "left" ? "right" : "left" : side;
490
+ const name = this.settings.displayName || "Assistant";
113
491
  this.host = document.createElement("div");
114
492
  this.host.setAttribute("data-brainerce-bot", this.connectionId);
115
493
  this.root = this.host.attachShadow({ mode: "open" });
116
494
  const style = document.createElement("style");
117
- style.textContent = `
118
- :host { all: initial; }
119
- * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
120
- .bb-root { position: fixed; bottom: 20px; ${sideRtlAware}: 20px; z-index: 2147483000; direction: ${dir}; }
121
- .bb-launcher {
122
- width: 56px; height: 56px; border: none; cursor: pointer; display: flex;
123
- align-items: center; justify-content: center; color: #fff; background: ${accent};
124
- border-radius: ${this.settings.bubbleShape === "square" ? "14px" : "9999px"};
125
- box-shadow: 0 8px 24px rgba(0,0,0,.22); transition: transform .15s ease;
126
- overflow: hidden; padding: 0;
127
- }
128
- .bb-launcher:hover { transform: scale(1.06); }
129
- .bb-launcher img { width: 100%; height: 100%; object-fit: cover; }
130
- .bb-window {
131
- position: absolute; bottom: 70px; ${sideRtlAware}: 0; width: 360px; max-width: calc(100vw - 32px);
132
- height: 540px; max-height: calc(100vh - 110px); display: none; flex-direction: column;
133
- background: #fff; border-radius: 16px; overflow: hidden;
134
- box-shadow: 0 16px 48px rgba(0,0,0,.24); border: 1px solid rgba(0,0,0,.06);
135
- }
136
- .bb-window.open { display: flex; }
137
- .bb-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: ${accent}; color: #fff; }
138
- .bb-avatar { width: 34px; height: 34px; border-radius: 9999px; background: rgba(255,255,255,.25);
139
- display: flex; align-items: center; justify-content: center; font-weight: 600; overflow: hidden; flex-shrink: 0; }
140
- .bb-avatar img { width: 100%; height: 100%; object-fit: cover; }
141
- .bb-head-main { flex: 1; min-width: 0; }
142
- .bb-name { font-size: 14px; font-weight: 600; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
143
- .bb-status { font-size: 11px; opacity: .9; display: flex; align-items: center; gap: 5px; }
144
- .bb-dot { width: 6px; height: 6px; border-radius: 9999px; background: #34d399; }
145
- .bb-iconbtn { background: none; border: none; color: #fff; cursor: pointer; opacity: .85; font-size: 16px; padding: 4px; }
146
- .bb-iconbtn:hover { opacity: 1; }
147
- .bb-messages { flex: 1; overflow-y: auto; padding: 14px; background: #f7f7f9; display: flex; flex-direction: column; gap: 8px; }
148
- .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; }
149
- .bb-msg.bot { align-self: flex-start; background: #fff; border: 1px solid rgba(0,0,0,.07); border-end-start-radius: 4px; }
150
- .bb-msg.user { align-self: flex-end; background: ${accent}; color: #fff; border-end-end-radius: 4px; }
151
- .bb-msg.err { align-self: flex-start; background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; }
152
- .bb-typing { align-self: flex-start; font-size: 11.5px; color: #6b7280; padding: 2px 4px; }
153
- .bb-card { align-self: flex-start; width: 230px; background: #fff; border: 1px solid rgba(0,0,0,.08);
154
- border-radius: 12px; overflow: hidden; text-decoration: none; color: inherit; display: block; }
155
- .bb-card img { width: 100%; height: 120px; object-fit: cover; display: block; background: #eee; }
156
- .bb-card-body { padding: 9px 11px; }
157
- .bb-card-title { font-size: 13px; font-weight: 600; margin: 0 0 3px; }
158
- .bb-card-price { font-size: 13px; color: ${accent}; font-weight: 600; }
159
- .bb-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px 10px; background: #f7f7f9; }
160
- .bb-chip { border: 1px solid ${accent}; color: ${accent}; background: #fff; border-radius: 9999px;
161
- font-size: 12px; padding: 5px 11px; cursor: pointer; }
162
- .bb-inputrow { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid rgba(0,0,0,.07); background: #fff; }
163
- .bb-input { flex: 1; border: none; outline: none; font-size: 13.5px; background: #f1f1f4; border-radius: 9999px; padding: 9px 14px; }
164
- .bb-send { border: none; background: ${accent}; color: #fff; width: 36px; height: 36px; border-radius: 9999px; cursor: pointer; font-size: 15px; flex-shrink: 0; }
165
- .bb-send:disabled { opacity: .5; cursor: default; }
166
- .bb-esc { padding: 12px 14px; background: #fff; border-top: 1px solid rgba(0,0,0,.07); display: none; flex-direction: column; gap: 8px; }
167
- .bb-esc.open { display: flex; }
168
- .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; }
169
- .bb-esc button { border: none; background: ${accent}; color: #fff; border-radius: 8px; padding: 8px; font-size: 13px; cursor: pointer; }
170
- .bb-esc-note { font-size: 12px; color: #047857; }
171
- `;
495
+ style.textContent = this.css(accent, dir, sideRtlAware);
172
496
  this.root.appendChild(style);
173
497
  const rootEl = document.createElement("div");
174
- rootEl.className = "bb-root";
498
+ rootEl.className = "bb";
175
499
  this.root.appendChild(rootEl);
500
+ const scrim = document.createElement("div");
501
+ scrim.className = "bb-scrim";
502
+ scrim.addEventListener("click", () => {
503
+ if (this.expanded) this.toggleExpand();
504
+ else this.close();
505
+ });
506
+ rootEl.appendChild(scrim);
176
507
  this.windowEl = document.createElement("div");
177
508
  this.windowEl.className = "bb-window";
178
509
  rootEl.appendChild(this.windowEl);
179
- const name = this.settings.displayName || "Assistant";
180
510
  const header = document.createElement("div");
181
511
  header.className = "bb-header";
182
512
  const avatar = document.createElement("span");
@@ -187,7 +517,7 @@ var BrainerceBot = class _BrainerceBot {
187
517
  img.alt = "";
188
518
  avatar.appendChild(img);
189
519
  } else {
190
- avatar.textContent = name.charAt(0).toUpperCase();
520
+ avatar.appendChild(document.createTextNode(name.charAt(0).toUpperCase()));
191
521
  }
192
522
  const headMain = document.createElement("span");
193
523
  headMain.className = "bb-head-main";
@@ -196,31 +526,24 @@ var BrainerceBot = class _BrainerceBot {
196
526
  nameEl.textContent = name;
197
527
  const statusEl = document.createElement("span");
198
528
  statusEl.className = "bb-status";
199
- const dot = document.createElement("span");
200
- dot.className = "bb-dot";
201
- statusEl.appendChild(dot);
202
- statusEl.appendChild(document.createTextNode(this.t("online")));
529
+ statusEl.textContent = this.t("online");
203
530
  headMain.appendChild(nameEl);
204
531
  headMain.appendChild(statusEl);
205
- const escBtn = document.createElement("button");
206
- escBtn.className = "bb-iconbtn";
207
- escBtn.dataset.act = "esc";
208
- escBtn.title = this.t("leaveMessage");
209
- escBtn.textContent = "\u2709";
210
- const closeBtn = document.createElement("button");
211
- closeBtn.className = "bb-iconbtn";
212
- closeBtn.dataset.act = "close";
213
- closeBtn.title = this.t("close");
214
- closeBtn.textContent = "\u2715";
532
+ const actions = document.createElement("span");
533
+ actions.className = "bb-actions";
534
+ const mode = this.settings.displayMode ?? "floating";
535
+ if (mode !== "full_screen" && this.settings.allowExpand !== false) {
536
+ this.expandBtn = this.iconButton("expand", this.t("expand"), () => this.toggleExpand());
537
+ actions.appendChild(this.expandBtn);
538
+ }
539
+ actions.appendChild(this.iconButton("mail", this.t("leaveMessage"), () => this.toggleEscalation()));
540
+ actions.appendChild(this.iconButton("close", this.t("close"), () => this.close()));
215
541
  header.appendChild(avatar);
216
542
  header.appendChild(headMain);
217
- header.appendChild(escBtn);
218
- header.appendChild(closeBtn);
543
+ header.appendChild(actions);
219
544
  this.windowEl.appendChild(header);
220
- header.querySelector('[data-act="close"]')?.addEventListener("click", () => this.close());
221
- header.querySelector('[data-act="esc"]')?.addEventListener("click", () => this.toggleEscalation());
222
545
  this.messagesEl = document.createElement("div");
223
- this.messagesEl.className = "bb-messages";
546
+ this.messagesEl.className = "bb-msgs";
224
547
  this.windowEl.appendChild(this.messagesEl);
225
548
  this.chipsEl = document.createElement("div");
226
549
  this.chipsEl.className = "bb-chips";
@@ -234,6 +557,9 @@ var BrainerceBot = class _BrainerceBot {
234
557
  this.windowEl.appendChild(this.chipsEl);
235
558
  const esc = document.createElement("div");
236
559
  esc.className = "bb-esc";
560
+ const escTitle = document.createElement("span");
561
+ escTitle.className = "bb-esc-title";
562
+ escTitle.textContent = this.t("leaveMessage");
237
563
  const escEmail = document.createElement("input");
238
564
  escEmail.type = "email";
239
565
  escEmail.name = "email";
@@ -244,46 +570,86 @@ var BrainerceBot = class _BrainerceBot {
244
570
  escMsg.placeholder = this.t("yourMessage");
245
571
  const escSend = document.createElement("button");
246
572
  escSend.type = "button";
573
+ escSend.className = "bb-esc-send";
247
574
  escSend.textContent = this.t("send");
575
+ escSend.addEventListener("click", () => this.submitEscalation(esc));
576
+ esc.appendChild(escTitle);
248
577
  esc.appendChild(escEmail);
249
578
  esc.appendChild(escMsg);
250
579
  esc.appendChild(escSend);
251
- esc.querySelector("button")?.addEventListener("click", () => this.submitEscalation(esc));
252
580
  this.windowEl.appendChild(esc);
253
- const inputRow = document.createElement("div");
254
- inputRow.className = "bb-inputrow";
255
- this.inputEl = document.createElement("input");
581
+ const composer = document.createElement("div");
582
+ composer.className = "bb-composer";
583
+ this.inputEl = document.createElement("textarea");
256
584
  this.inputEl.className = "bb-input";
585
+ this.inputEl.rows = 1;
257
586
  this.inputEl.placeholder = this.t("placeholder");
258
587
  this.inputEl.addEventListener("keydown", (e) => {
259
- if (e.key === "Enter") this.send(this.inputEl?.value ?? "");
588
+ if (e.key === "Enter" && !e.shiftKey) {
589
+ e.preventDefault();
590
+ this.send(this.inputEl?.value ?? "");
591
+ }
260
592
  });
261
- const sendBtn = document.createElement("button");
262
- sendBtn.className = "bb-send";
263
- sendBtn.textContent = "\u27A4";
264
- sendBtn.addEventListener("click", () => this.send(this.inputEl?.value ?? ""));
265
- inputRow.appendChild(this.inputEl);
266
- inputRow.appendChild(sendBtn);
267
- this.windowEl.appendChild(inputRow);
268
- const launcher = document.createElement("button");
269
- launcher.className = "bb-launcher";
270
- launcher.setAttribute("aria-label", name);
593
+ this.inputEl.addEventListener("input", () => this.syncSendState());
594
+ this.sendBtn = document.createElement("button");
595
+ this.sendBtn.className = "bb-send";
596
+ this.sendBtn.disabled = true;
597
+ this.sendBtn.setAttribute("aria-label", this.t("send"));
598
+ this.sendBtn.appendChild(icon("send"));
599
+ this.sendBtn.addEventListener("click", () => this.send(this.inputEl?.value ?? ""));
600
+ composer.appendChild(this.inputEl);
601
+ composer.appendChild(this.sendBtn);
602
+ this.windowEl.appendChild(composer);
603
+ const foot = document.createElement("div");
604
+ foot.className = "bb-foot";
605
+ const credit = document.createElement("a");
606
+ credit.href = "https://brainerce.com";
607
+ credit.target = "_blank";
608
+ credit.rel = "noopener noreferrer";
609
+ credit.appendChild(icon("brand"));
610
+ credit.appendChild(document.createTextNode(this.t("poweredBy")));
611
+ foot.appendChild(credit);
612
+ this.windowEl.appendChild(foot);
613
+ this.launcherEl = document.createElement("button");
614
+ this.launcherEl.className = "bb-launcher";
615
+ this.launcherEl.setAttribute("aria-label", name);
271
616
  if (this.settings.avatarUrl && isSafeUrl(this.settings.avatarUrl)) {
272
617
  const img = document.createElement("img");
273
618
  img.src = this.settings.avatarUrl;
274
619
  img.alt = "";
275
- launcher.appendChild(img);
620
+ this.launcherEl.appendChild(img);
276
621
  } else {
277
- launcher.textContent = "\u{1F4AC}";
622
+ const chatIc = icon("chat");
623
+ chatIc.classList.add("bb-l-chat");
624
+ this.launcherEl.appendChild(chatIc);
278
625
  }
279
- launcher.addEventListener("click", () => this.opened ? this.close() : this.open());
280
- rootEl.appendChild(launcher);
626
+ const closeIc = icon("close");
627
+ closeIc.classList.add("bb-l-close");
628
+ this.launcherEl.appendChild(closeIc);
629
+ this.launcherEl.addEventListener("click", () => this.opened ? this.close() : this.open());
630
+ rootEl.appendChild(this.launcherEl);
281
631
  target.appendChild(this.host);
282
632
  }
633
+ iconButton(name, label, onClick) {
634
+ const btn = document.createElement("button");
635
+ btn.className = "bb-iconbtn";
636
+ btn.title = label;
637
+ btn.setAttribute("aria-label", label);
638
+ btn.appendChild(icon(name));
639
+ btn.addEventListener("click", onClick);
640
+ return btn;
641
+ }
642
+ syncSendState() {
643
+ if (this.sendBtn) this.sendBtn.disabled = !(this.inputEl?.value ?? "").trim() || this.busy;
644
+ }
283
645
  open() {
284
646
  if (!this.windowEl || this.opened) return;
285
647
  this.opened = true;
286
- this.windowEl.classList.add("open");
648
+ const el = this.root?.querySelector(".bb");
649
+ el?.classList.add("open");
650
+ const takeover = (this.settings.displayMode ?? "floating") === "full_screen" || window.innerWidth <= 520;
651
+ el?.classList.toggle("big", takeover || this.expanded);
652
+ this.syncBodyScroll();
287
653
  if (this.messagesEl && this.messagesEl.childElementCount === 0) {
288
654
  void this.primeThread();
289
655
  }
@@ -291,7 +657,32 @@ var BrainerceBot = class _BrainerceBot {
291
657
  }
292
658
  close() {
293
659
  this.opened = false;
294
- this.windowEl?.classList.remove("open");
660
+ const el = this.root?.querySelector(".bb");
661
+ el?.classList.remove("open");
662
+ if (!this.expanded) el?.classList.remove("big");
663
+ this.syncBodyScroll();
664
+ }
665
+ syncBodyScroll() {
666
+ const big = this.opened && !!this.root?.querySelector(".bb")?.classList.contains("big");
667
+ if (big && this.prevBodyOverflow === null) {
668
+ this.prevBodyOverflow = document.body.style.overflow || "";
669
+ document.body.style.overflow = "hidden";
670
+ } else if (!big && this.prevBodyOverflow !== null) {
671
+ document.body.style.overflow = this.prevBodyOverflow;
672
+ this.prevBodyOverflow = null;
673
+ }
674
+ }
675
+ toggleExpand() {
676
+ this.expanded = !this.expanded;
677
+ this.root?.querySelector(".bb")?.classList.toggle("expanded", this.expanded);
678
+ this.root?.querySelector(".bb")?.classList.toggle("big", this.expanded);
679
+ this.syncBodyScroll();
680
+ if (this.expandBtn) {
681
+ this.expandBtn.replaceChildren(icon(this.expanded ? "collapse" : "expand"));
682
+ const label = this.t(this.expanded ? "collapse" : "expand");
683
+ this.expandBtn.title = label;
684
+ this.expandBtn.setAttribute("aria-label", label);
685
+ }
295
686
  }
296
687
  /** First open: restore the server thread, or show the greeting. */
297
688
  async primeThread() {
@@ -304,7 +695,8 @@ var BrainerceBot = class _BrainerceBot {
304
695
  if (res.ok) {
305
696
  const data = await res.json();
306
697
  for (const m of data.data) {
307
- this.appendMessage(m.role === "assistant" ? "bot" : "user", m.content);
698
+ const el = this.appendMessage(m.role === "assistant" ? "bot" : "user", "");
699
+ renderRich(el, m.content);
308
700
  }
309
701
  if (data.data.length > 0) {
310
702
  this.chipsEl?.remove();
@@ -318,7 +710,10 @@ var BrainerceBot = class _BrainerceBot {
318
710
  } catch {
319
711
  }
320
712
  }
321
- if (this.settings.greeting) this.appendMessage("bot", this.settings.greeting);
713
+ if (this.settings.greeting) {
714
+ const el = this.appendMessage("bot", "");
715
+ renderRich(el, this.settings.greeting);
716
+ }
322
717
  }
323
718
  // --------------------------------------------------------------------------
324
719
  // The chat turn
@@ -328,9 +723,13 @@ var BrainerceBot = class _BrainerceBot {
328
723
  if (!message || this.busy) return;
329
724
  this.busy = true;
330
725
  if (this.inputEl) this.inputEl.value = "";
726
+ this.syncSendState();
331
727
  this.chipsEl?.remove();
332
728
  this.appendMessage("user", message);
333
729
  const typing = this.appendTyping();
730
+ this.pendingText = "";
731
+ this.cardsRow = null;
732
+ this.cardIds = /* @__PURE__ */ new Set();
334
733
  let botBubble = null;
335
734
  try {
336
735
  const res = await fetch(
@@ -371,11 +770,13 @@ var BrainerceBot = class _BrainerceBot {
371
770
  botBubble = this.handleFrame(frame, typing, botBubble);
372
771
  }
373
772
  }
773
+ if (botBubble && this.pendingText) renderRich(botBubble, this.pendingText);
374
774
  } catch {
375
775
  this.appendMessage("err", this.t("error"));
376
776
  } finally {
377
777
  typing.remove();
378
778
  this.busy = false;
779
+ this.syncSendState();
379
780
  }
380
781
  }
381
782
  handleFrame(frame, typing, botBubble) {
@@ -390,16 +791,20 @@ var BrainerceBot = class _BrainerceBot {
390
791
  typing.remove();
391
792
  botBubble = this.appendMessage("bot", "");
392
793
  }
393
- botBubble.textContent = (botBubble.textContent ?? "") + frame.text;
794
+ this.pendingText += frame.text;
795
+ botBubble.textContent = this.pendingText;
394
796
  this.scrollDown();
395
797
  return botBubble;
396
798
  }
397
799
  case "tool":
398
- typing.textContent = frame.status === "running" ? "\u22EF" : "";
800
+ typing.classList.toggle("searching", frame.status === "running");
399
801
  return botBubble;
400
802
  case "card":
401
803
  this.appendCard(frame.card);
402
804
  return botBubble;
805
+ case "action":
806
+ void this.handleAction(frame);
807
+ return botBubble;
403
808
  case "error":
404
809
  this.appendMessage("err", frame.message || this.t("error"));
405
810
  return botBubble;
@@ -409,7 +814,239 @@ var BrainerceBot = class _BrainerceBot {
409
814
  }
410
815
  }
411
816
  // --------------------------------------------------------------------------
412
- // Escalation + attribution
817
+ // Product cards
818
+ // --------------------------------------------------------------------------
819
+ appendCard(card) {
820
+ if (!this.messagesEl) return;
821
+ if (this.cardIds.has(card.productId)) return;
822
+ this.cardIds.add(card.productId);
823
+ if (!this.cardsRow) {
824
+ const shelf = document.createElement("div");
825
+ shelf.className = "bb-shelf";
826
+ const cap = document.createElement("div");
827
+ cap.className = "bb-shelf-cap";
828
+ cap.textContent = this.t("results");
829
+ this.cardsRow = document.createElement("div");
830
+ this.cardsRow.className = "bb-cards";
831
+ shelf.appendChild(cap);
832
+ shelf.appendChild(this.cardsRow);
833
+ this.messagesEl.appendChild(shelf);
834
+ }
835
+ const safeHref = isSafeUrl(card.url) ? card.url : null;
836
+ const el = document.createElement("div");
837
+ el.className = "bb-card";
838
+ const goToProduct = () => {
839
+ this.beacon(card.botRef);
840
+ if (safeHref) window.location.href = safeHref;
841
+ };
842
+ const imgWrap = document.createElement("span");
843
+ imgWrap.className = "bb-card-img";
844
+ if (card.imageUrl && isSafeUrl(card.imageUrl)) {
845
+ const img = document.createElement("img");
846
+ img.src = card.imageUrl;
847
+ img.alt = "";
848
+ img.loading = "lazy";
849
+ imgWrap.appendChild(img);
850
+ } else {
851
+ imgWrap.appendChild(icon("cart"));
852
+ }
853
+ imgWrap.addEventListener("click", goToProduct);
854
+ el.appendChild(imgWrap);
855
+ const body = document.createElement("span");
856
+ body.className = "bb-card-body";
857
+ const title = document.createElement("span");
858
+ title.className = "bb-card-title";
859
+ title.textContent = card.title;
860
+ title.addEventListener("click", goToProduct);
861
+ const priceEl = document.createElement("span");
862
+ priceEl.className = "bb-card-price";
863
+ priceEl.textContent = card.price.formatted;
864
+ body.appendChild(title);
865
+ body.appendChild(priceEl);
866
+ const cta = document.createElement("span");
867
+ cta.className = "bb-card-cta";
868
+ if (card.requiresOptions) {
869
+ const cartBtn = document.createElement("button");
870
+ cartBtn.className = "bb-btn bb-btn-add";
871
+ cartBtn.setAttribute("aria-label", this.t("addToCart"));
872
+ cartBtn.appendChild(icon("cart"));
873
+ if (card.variants?.length) {
874
+ cartBtn.addEventListener("click", () => this.togglePicker(body, card, imgWrap));
875
+ } else {
876
+ cartBtn.addEventListener("click", goToProduct);
877
+ }
878
+ cta.appendChild(cartBtn);
879
+ } else {
880
+ const addBtn = document.createElement("button");
881
+ addBtn.className = "bb-btn bb-btn-add";
882
+ addBtn.appendChild(icon("cart"));
883
+ addBtn.appendChild(document.createTextNode(this.t("addToCart")));
884
+ addBtn.addEventListener("click", () => void this.addToCart(card, addBtn));
885
+ const viewBtn = document.createElement("button");
886
+ viewBtn.className = "bb-btn bb-btn-ghost";
887
+ viewBtn.appendChild(document.createTextNode(this.t("view")));
888
+ viewBtn.addEventListener("click", goToProduct);
889
+ cta.appendChild(addBtn);
890
+ cta.appendChild(viewBtn);
891
+ }
892
+ body.appendChild(cta);
893
+ el.appendChild(body);
894
+ this.cardsRow.appendChild(el);
895
+ this.scrollDown();
896
+ }
897
+ /**
898
+ * In-card variant picker: one chip-row per attribute; a complete selection
899
+ * resolves to a variantId and becomes a real add-to-cart. Variant image and
900
+ * price update live. Built entirely with createElement (no innerHTML).
901
+ */
902
+ togglePicker(body, card, imgWrap) {
903
+ const cardEl = body.parentElement;
904
+ const existing = body.querySelector(".bb-pick");
905
+ if (existing) {
906
+ existing.remove();
907
+ cardEl?.classList.remove("picking");
908
+ return;
909
+ }
910
+ cardEl?.classList.add("picking");
911
+ const variants = card.variants ?? [];
912
+ const keys = [];
913
+ for (const v of variants) {
914
+ for (const k of Object.keys(v.attributes)) if (!keys.includes(k)) keys.push(k);
915
+ }
916
+ const sel = {};
917
+ const pick = document.createElement("span");
918
+ pick.className = "bb-pick";
919
+ const resolved = () => variants.find((v) => keys.every((k) => sel[k] && v.attributes[k] === sel[k])) ?? null;
920
+ const render = () => {
921
+ pick.replaceChildren();
922
+ for (const key of keys) {
923
+ const label = document.createElement("span");
924
+ label.className = "bb-pick-key";
925
+ label.textContent = key;
926
+ const vals = document.createElement("span");
927
+ vals.className = "bb-pick-vals";
928
+ const seen = /* @__PURE__ */ new Set();
929
+ for (const v of variants) {
930
+ const val = v.attributes[key];
931
+ if (!val || seen.has(val)) continue;
932
+ seen.add(val);
933
+ const possible = variants.some(
934
+ (x) => x.attributes[key] === val && keys.every((k) => k === key || !sel[k] || x.attributes[k] === sel[k])
935
+ );
936
+ const chip = document.createElement("button");
937
+ chip.className = `bb-chipv${sel[key] === val ? " sel" : ""}${possible ? "" : " off"}`;
938
+ chip.textContent = val;
939
+ chip.addEventListener("click", () => {
940
+ if (sel[key] === val) delete sel[key];
941
+ else sel[key] = val;
942
+ render();
943
+ });
944
+ vals.appendChild(chip);
945
+ }
946
+ pick.appendChild(label);
947
+ pick.appendChild(vals);
948
+ }
949
+ const closeBtn = document.createElement("button");
950
+ closeBtn.className = "bb-pick-close";
951
+ closeBtn.setAttribute("aria-label", this.t("close"));
952
+ closeBtn.appendChild(icon("close"));
953
+ closeBtn.addEventListener("click", () => {
954
+ pick.remove();
955
+ cardEl?.classList.remove("picking");
956
+ });
957
+ pick.appendChild(closeBtn);
958
+ const match = resolved();
959
+ const foot = document.createElement("span");
960
+ foot.className = "bb-pick-foot";
961
+ const priceEl = document.createElement("span");
962
+ priceEl.className = "bb-pick-price";
963
+ priceEl.textContent = match ? match.price.formatted : "";
964
+ const addBtn = document.createElement("button");
965
+ addBtn.className = "bb-btn bb-btn-add";
966
+ addBtn.appendChild(icon("cart"));
967
+ addBtn.appendChild(document.createTextNode(this.t("addToCart")));
968
+ if (!match) addBtn.disabled = true;
969
+ addBtn.addEventListener("click", () => {
970
+ const m = resolved();
971
+ if (m) void this.addToCart(card, addBtn, m.id);
972
+ });
973
+ foot.appendChild(priceEl);
974
+ foot.appendChild(addBtn);
975
+ pick.appendChild(foot);
976
+ if (match?.imageUrl && isSafeUrl(match.imageUrl)) {
977
+ const img = imgWrap.querySelector("img");
978
+ if (img) img.src = match.imageUrl;
979
+ }
980
+ this.scrollDown();
981
+ };
982
+ render();
983
+ body.appendChild(pick);
984
+ this.scrollDown();
985
+ }
986
+ /**
987
+ * Add-to-cart resolution chain — must never be a dead button:
988
+ * 1. host `onAddToCart` option (scaffolded stores: syncs their cart UI)
989
+ * 2. cancelable `brainerce:bot:add-to-cart` CustomEvent (custom embeds)
990
+ * 3. fallback: navigate to the product page
991
+ */
992
+ async addToCart(card, btn, variantId) {
993
+ this.beacon(card.botRef);
994
+ btn.dataset.state = "busy";
995
+ const handled = await this.dispatchAdd(card.productId, variantId ?? null);
996
+ if (handled) {
997
+ btn.dataset.state = "done";
998
+ btn.replaceChildren(icon("check"), document.createTextNode(this.t("added")));
999
+ setTimeout(() => {
1000
+ if (!this.destroyed && btn.isConnected) {
1001
+ delete btn.dataset.state;
1002
+ btn.replaceChildren(icon("cart"), document.createTextNode(this.t("addToCart")));
1003
+ }
1004
+ }, 2200);
1005
+ } else {
1006
+ delete btn.dataset.state;
1007
+ if (isSafeUrl(card.url)) window.location.href = card.url;
1008
+ }
1009
+ }
1010
+ /**
1011
+ * The host-cart chain shared by card buttons and bot-initiated actions:
1012
+ * onAddToCart option -> cancelable CustomEvent. Returns whether a host
1013
+ * took the add.
1014
+ */
1015
+ async dispatchAdd(productId, variantId) {
1016
+ try {
1017
+ if (this.onAddToCart) {
1018
+ return await this.onAddToCart({ productId, variantId, quantity: 1 }) !== false;
1019
+ }
1020
+ const ev = new CustomEvent("brainerce:bot:add-to-cart", {
1021
+ detail: { productId, variantId, quantity: 1, connectionId: this.connectionId },
1022
+ cancelable: true,
1023
+ bubbles: true,
1024
+ composed: true
1025
+ });
1026
+ return !window.dispatchEvent(ev);
1027
+ } catch {
1028
+ return false;
1029
+ }
1030
+ }
1031
+ /** Bot-initiated widget actions (the model called the addToCart tool). */
1032
+ async handleAction(frame) {
1033
+ if (frame.action !== "add_to_cart") return;
1034
+ if (frame.botRef) this.beacon(frame.botRef);
1035
+ const ok = await this.dispatchAdd(frame.productId, frame.variantId);
1036
+ if (!ok) this.appendMessage("err", this.t("addFailed"));
1037
+ }
1038
+ /** The durable conversion signal — fire-and-forget, never blocks. */
1039
+ beacon(botRef) {
1040
+ try {
1041
+ navigator.sendBeacon?.(
1042
+ `${this.baseUrl}/api/storefront-bot/attribution/click`,
1043
+ new Blob([JSON.stringify({ botRef })], { type: "application/json" })
1044
+ );
1045
+ } catch {
1046
+ }
1047
+ }
1048
+ // --------------------------------------------------------------------------
1049
+ // Escalation
413
1050
  // --------------------------------------------------------------------------
414
1051
  toggleEscalation() {
415
1052
  this.root?.querySelector(".bb-esc")?.classList.toggle("open");
@@ -436,50 +1073,18 @@ var BrainerceBot = class _BrainerceBot {
436
1073
  if (res.ok) {
437
1074
  const note = document.createElement("span");
438
1075
  note.className = "bb-esc-note";
439
- note.textContent = this.t("sent");
1076
+ note.appendChild(icon("check"));
1077
+ note.appendChild(document.createTextNode(this.t("sent")));
440
1078
  form.replaceChildren(note);
441
1079
  }
442
1080
  } catch {
443
1081
  }
444
1082
  }
445
- appendCard(card) {
446
- if (!this.messagesEl) return;
447
- const a = document.createElement("a");
448
- a.className = "bb-card";
449
- a.href = isSafeUrl(card.url) ? card.url : "#";
450
- if (card.imageUrl && isSafeUrl(card.imageUrl)) {
451
- const img = document.createElement("img");
452
- img.src = card.imageUrl;
453
- img.alt = "";
454
- a.appendChild(img);
455
- }
456
- const body = document.createElement("span");
457
- body.className = "bb-card-body";
458
- const title = document.createElement("p");
459
- title.className = "bb-card-title";
460
- title.textContent = card.title;
461
- const priceEl = document.createElement("span");
462
- priceEl.className = "bb-card-price";
463
- priceEl.textContent = card.price.formatted;
464
- body.appendChild(title);
465
- body.appendChild(priceEl);
466
- a.appendChild(body);
467
- a.addEventListener("click", () => {
468
- try {
469
- navigator.sendBeacon?.(
470
- `${this.baseUrl}/api/storefront-bot/attribution/click`,
471
- new Blob([JSON.stringify({ botRef: card.botRef })], { type: "application/json" })
472
- );
473
- } catch {
474
- }
475
- });
476
- this.messagesEl.appendChild(a);
477
- this.scrollDown();
478
- }
479
1083
  // --------------------------------------------------------------------------
480
1084
  appendMessage(kind, text) {
481
1085
  const el = document.createElement("div");
482
1086
  el.className = `bb-msg ${kind}`;
1087
+ el.setAttribute("dir", "auto");
483
1088
  el.textContent = text;
484
1089
  this.messagesEl?.appendChild(el);
485
1090
  this.scrollDown();
@@ -488,7 +1093,14 @@ var BrainerceBot = class _BrainerceBot {
488
1093
  appendTyping() {
489
1094
  const el = document.createElement("div");
490
1095
  el.className = "bb-typing";
491
- el.textContent = "\u22EF";
1096
+ const dots = document.createElement("span");
1097
+ dots.className = "bb-dots";
1098
+ for (let i = 0; i < 3; i++) dots.appendChild(document.createElement("i"));
1099
+ const tool = document.createElement("span");
1100
+ tool.className = "bb-tool";
1101
+ tool.textContent = this.t("searching");
1102
+ el.appendChild(dots);
1103
+ el.appendChild(tool);
492
1104
  this.messagesEl?.appendChild(el);
493
1105
  this.scrollDown();
494
1106
  return el;
@@ -497,9 +1109,6 @@ var BrainerceBot = class _BrainerceBot {
497
1109
  if (this.messagesEl) this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
498
1110
  }
499
1111
  };
500
- function isSafeUrl(url) {
501
- return /^\/(?!\/)/.test(url) || /^https?:\/\//i.test(url);
502
- }
503
1112
  export {
504
1113
  BrainerceBot
505
1114
  };