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