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