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/README.md CHANGED
@@ -4865,12 +4865,23 @@ Add the store's AI shopping assistant with one line. All configuration (name, av
4865
4865
  // or mount from the SDK (client-side only)
4866
4866
  import { BrainerceBot } from 'brainerce/bot';
4867
4867
 
4868
- const bot = await BrainerceBot.mount({ connectionId: 'vc_abc123' });
4868
+ const bot = await BrainerceBot.mount({
4869
+ connectionId: 'vc_abc123',
4870
+ // Optional: make the cards' "Add to cart" button use YOUR cart so your
4871
+ // header count stays in sync. Return false to make the widget fall back
4872
+ // to navigating to the product page.
4873
+ onAddToCart: async ({ productId, quantity }) => {
4874
+ await client.smartAddToCart({ productId, quantity });
4875
+ return true;
4876
+ },
4877
+ });
4869
4878
  // resolves to null when the bot is disabled — safe to call unconditionally
4870
4879
  bot?.destroy(); // optional teardown
4871
4880
  ```
4872
4881
 
4873
- The widget persists an anonymous session in `localStorage`, restores conversations on revisit, streams answers, shows product recommendation cards (linking to `/products/<slug>`), and includes a leave-a-message form that lands in the merchant's Inquiries inbox. It is read-only by design — shoppers can never mutate the store through it.
4882
+ The widget persists an anonymous session in `localStorage`, restores conversations on revisit, streams answers, and shows product recommendation cards (image, price, add-to-cart / view buttons). It also includes a leave-a-message form that lands in the merchant's Inquiries inbox.
4883
+
4884
+ **Add to cart resolution** (never a dead button): the widget first calls your `onAddToCart` option; without one it dispatches a cancelable `brainerce:bot:add-to-cart` `CustomEvent` on `window` (`detail: { productId, quantity, connectionId }` — call `preventDefault()` after handling it); if nothing handles either, it navigates to the product page. Products that need option selection always navigate. Aside from your own cart handler, the widget is read-only by design — shoppers can never mutate the store through it.
4874
4885
 
4875
4886
  ## Webhooks
4876
4887
 
@@ -1,57 +1,242 @@
1
- "use strict";(()=>{var N="https://api.brainerce.com",U=new Set(["he","ar"]),M={en:{online:"Online",placeholder:"Ask anything\u2026",error:"Something went wrong \u2014 please try again.",leaveMessage:"Leave a message for the team",yourEmail:"Your email",yourMessage:"Your message",send:"Send",sent:"Thanks! The team will get back to you by email.",close:"Close"},he:{online:"\u05DE\u05D7\u05D5\u05D1\u05E8",placeholder:"\u05E9\u05D0\u05DC\u05D5 \u05D0\u05D5\u05EA\u05D9 \u05D4\u05DB\u05DC\u2026",error:"\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9 \u2014 \u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.",leaveMessage:"\u05D4\u05E9\u05D0\u05D9\u05E8\u05D5 \u05D4\u05D5\u05D3\u05E2\u05D4 \u05DC\u05E6\u05D5\u05D5\u05EA",yourEmail:"\u05D4\u05D0\u05D9\u05DE\u05D9\u05D9\u05DC \u05E9\u05DC\u05DB\u05DD",yourMessage:"\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",send:"\u05E9\u05DC\u05D9\u05D7\u05D4",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.",close:"\u05E1\u05D2\u05D9\u05E8\u05D4"}};function $(r){let e=new Uint8Array(16);crypto.getRandomValues(e);let t=btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return`${r}${t}`}var S=class r{constructor(e){this.settings={enabled:!1};this.locale="en";this.sessionId=null;this.conversationId=null;this.busy=!1;this.opened=!1;this.destroyed=!1;this.connectionId=e.connectionId,this.baseUrl=(e.baseUrl||N).replace(/\/$/,""),this.storageKey=`brainerce-bot:${this.connectionId}`}static async mount(e){if(!e?.connectionId)return console.warn("[BrainerceBot] connectionId is required"),null;let t=new r(e);return await t.boot(e.target??document.body)?t:null}destroy(){this.destroyed=!0,this.host?.remove()}async boot(e){try{let t=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/settings`);if(!t.ok)return!1;this.settings=await t.json()}catch{return!1}return this.settings.enabled?(this.locale=this.settings.languages?.[0]??"en",this.restoreIds(),this.render(e),this.settings.displayMode==="auto_open"&&setTimeout(()=>!this.destroyed&&this.open(),3e3),!0):!1}t(e){return(M[this.locale]??M.en)[e]??M.en[e]??e}restoreIds(){try{let e=localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);this.sessionId=t.sessionId??null,this.conversationId=t.conversationId??null}}catch{}}persistIds(){try{localStorage.setItem(this.storageKey,JSON.stringify({sessionId:this.sessionId,conversationId:this.conversationId}))}catch{}}render(e){let t=this.settings.accentColor||"#6366F1",n=U.has(this.locale)?"rtl":"ltr",d=this.settings.bubbleShape==="square"?"8px":"16px",s=this.settings.position==="start"?"left":"right",l=n==="rtl"?s==="left"?"right":"left":s;this.host=document.createElement("div"),this.host.setAttribute("data-brainerce-bot",this.connectionId),this.root=this.host.attachShadow({mode:"open"});let x=document.createElement("style");x.textContent=`
1
+ "use strict";(()=>{var A="https://api.brainerce.com",R=new Set(["he","ar"]),$={en:{online:"Online",placeholder:"Ask anything\u2026",error:"Something went wrong \u2014 please try again.",leaveMessage:"Leave a message for the team",yourEmail:"Your email",yourMessage:"Your message",send:"Send",sent:"Thanks! The team will get back to you by email.",close:"Close",expand:"Expand",collapse:"Collapse",searching:"Searching the store\u2026",addToCart:"Add to cart",added:"Added",view:"View",chooseOptions:"View product",results:"From the store"},he:{online:"\u05DE\u05D7\u05D5\u05D1\u05E8",placeholder:"\u05E9\u05D0\u05DC\u05D5 \u05D0\u05D5\u05EA\u05D9 \u05D4\u05DB\u05DC\u2026",error:"\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9 \u2014 \u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.",leaveMessage:"\u05D4\u05E9\u05D0\u05D9\u05E8\u05D5 \u05D4\u05D5\u05D3\u05E2\u05D4 \u05DC\u05E6\u05D5\u05D5\u05EA",yourEmail:"\u05D4\u05D0\u05D9\u05DE\u05D9\u05D9\u05DC \u05E9\u05DC\u05DB\u05DD",yourMessage:"\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",send:"\u05E9\u05DC\u05D9\u05D7\u05D4",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.",close:"\u05E1\u05D2\u05D9\u05E8\u05D4",expand:"\u05D4\u05E8\u05D7\u05D1\u05D4",collapse:"\u05DB\u05D9\u05D5\u05D5\u05E5",searching:"\u05DE\u05D7\u05E4\u05E9 \u05D1\u05D7\u05E0\u05D5\u05EA\u2026",addToCart:"\u05D4\u05D5\u05E1\u05E4\u05D4 \u05DC\u05E1\u05DC",added:"\u05E0\u05D5\u05E1\u05E3",view:"\u05E6\u05E4\u05D9\u05D9\u05D4",chooseOptions:"\u05DC\u05E6\u05E4\u05D5\u05EA \u05D1\u05DE\u05D5\u05E6\u05E8",results:"\u05DE\u05EA\u05D5\u05DA \u05D4\u05D7\u05E0\u05D5\u05EA"}},U={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>',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>',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>',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>',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>',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>',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>',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>',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>'},z=typeof DOMParser<"u"?new DOMParser:null;function f(b){let e=(U[b]??U.chat).replace("<svg ",'<svg xmlns="http://www.w3.org/2000/svg" '),t=z?.parseFromString(e,"image/svg+xml")?.documentElement;return!t||t.nodeName==="parsererror"?document.createTextNode(""):(t.setAttribute("aria-hidden","true"),t.setAttribute("class","bb-ic"),t)}function O(b){let e=new Uint8Array(16);crypto.getRandomValues(e);let n=btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return`${b}${n}`}function T(b){return/^\/(?!\/)/.test(b)||/^https?:\/\//i.test(b)}var S=/\*\*([^*\n]+)\*\*|\[([^\]\n]+)\]\(([^)\s]+)\)/g;function H(b,e){let n=0;S.lastIndex=0;for(let t=S.exec(e);t;t=S.exec(e)){if(t.index>n&&b.appendChild(document.createTextNode(e.slice(n,t.index))),t[1]!==void 0){let i=document.createElement("strong");i.textContent=t[1],b.appendChild(i)}else if(T(t[3])){let i=document.createElement("a");i.href=t[3],i.target="_blank",i.rel="noopener noreferrer",i.textContent=t[2],b.appendChild(i)}else b.appendChild(document.createTextNode(t[2]));n=t.index+t[0].length}n<e.length&&b.appendChild(document.createTextNode(e.slice(n)))}function B(b,e){b.replaceChildren();let n=e.split(`
2
+ `),t=null,i=null;for(let o of n){let r=/^\s*[-•*]\s+(.*)$/.exec(o);if(r){i=null,t||(t=document.createElement("ul"),b.appendChild(t));let d=document.createElement("li");H(d,r[1]),t.appendChild(d);continue}if(t=null,!o.trim()){i=null;continue}i?i.appendChild(document.createElement("br")):(i=document.createElement("p"),b.appendChild(i)),H(i,o)}}var N=class b{constructor(e){this.settings={enabled:!1};this.locale="en";this.sessionId=null;this.conversationId=null;this.busy=!1;this.opened=!1;this.expanded=!1;this.destroyed=!1;this.pendingText="";this.cardsRow=null;this.cardIds=new Set;this.connectionId=e.connectionId,this.baseUrl=(e.baseUrl||A).replace(/\/$/,""),this.storageKey=`brainerce-bot:${this.connectionId}`,this.onAddToCart=e.onAddToCart}static async mount(e){if(!e?.connectionId)return console.warn("[BrainerceBot] connectionId is required"),null;let n=new b(e);return await n.boot(e.target??document.body)?n:null}destroy(){this.destroyed=!0,this.host?.remove()}async boot(e){try{let n=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/settings`);if(!n.ok)return!1;this.settings=await n.json()}catch{return!1}return this.settings.enabled?(this.locale=this.settings.languages?.[0]??"en",this.restoreIds(),this.render(e),this.settings.displayMode==="auto_open"&&setTimeout(()=>!this.destroyed&&this.open(),3e3),!0):!1}t(e){return($[this.locale]??$.en)[e]??$.en[e]??e}restoreIds(){try{let e=localStorage.getItem(this.storageKey);if(e){let n=JSON.parse(e);this.sessionId=n.sessionId??null,this.conversationId=n.conversationId??null}}catch{}}persistIds(){try{localStorage.setItem(this.storageKey,JSON.stringify({sessionId:this.sessionId,conversationId:this.conversationId}))}catch{}}css(e,n,t){let i=this.settings.bubbleShape==="square",o=i?"14px":"22px",r=i?"8px":"15px",d="5px",a=this.settings.displayMode??"floating",l=a==="side_rail",s=a==="full_screen";return`
2
3
  :host { all: initial; }
3
- * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
4
- .bb-root { position: fixed; bottom: 20px; ${l}: 20px; z-index: 2147483000; direction: ${n}; }
4
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0;
5
+ font-family: -apple-system, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI", system-ui, "Helvetica Neue", sans-serif;
6
+ -webkit-font-smoothing: antialiased; }
7
+ button { font: inherit; cursor: pointer; background: none; border: none; color: inherit; }
8
+ .bb { position: fixed; bottom: 24px; ${t}: 24px; z-index: 2147483000; direction: ${n};
9
+ /* product-card density scales with the window size */
10
+ --bb-card-w: 148px; --bb-img-h: 84px; --bb-title-fs: 11.5px; --bb-price-fs: 12.5px; --bb-btn-h: 27px; }
11
+ /* .big = any large surface (expanded, full-screen mode, mobile takeover) */
12
+ .bb.big { --bb-card-w: 216px; --bb-img-h: 132px; --bb-title-fs: 13px; --bb-price-fs: 14px; --bb-btn-h: 33px; }
13
+ .bb.big .bb-msgs, .bb.big .bb-chips, .bb.big .bb-esc {
14
+ padding-inline: max(20px, calc((100% - 1340px) / 2)); }
15
+ .bb.big .bb-msgs { gap: 12px; padding-top: 22px; }
16
+ .bb.big .bb-msg { font-size: 14px; padding: 11px 15px; }
17
+ .bb.big .bb-header { padding: 15px 22px; }
18
+ /* Big-surface composer: a full-width message box with the send inside */
19
+ .bb.big .bb-composer { position: relative; padding: 16px 20px 20px; }
20
+ .bb.big .bb-input { min-height: 116px; border-radius: 18px; background: #fff;
21
+ border: 1px solid #e3e5ec; padding: 16px 20px 52px; font-size: 15px;
22
+ overflow-y: auto; box-shadow: 0 2px 8px -2px rgba(15,18,34,.06); }
23
+ .bb.big .bb-send { position: absolute; bottom: 34px; inset-inline-end: 36px;
24
+ width: 42px; height: 42px; }
25
+ .bb.big .bb-send .bb-ic { width: 19px; height: 19px; }
26
+ /* Backdrop for the large dialog: dim + blur the storefront behind it */
27
+ .bb-scrim { position: fixed; inset: 0; background: rgba(15,18,34,.35);
28
+ -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
29
+ opacity: 0; pointer-events: none; transition: opacity .25s ease; }
30
+ .bb.open.big .bb-scrim { opacity: 1; pointer-events: auto; }
31
+
32
+ /* ---- launcher ------------------------------------------------------ */
5
33
  .bb-launcher {
6
- width: 56px; height: 56px; border: none; cursor: pointer; display: flex;
7
- align-items: center; justify-content: center; color: #fff; background: ${t};
8
- border-radius: ${this.settings.bubbleShape==="square"?"14px":"9999px"};
9
- box-shadow: 0 8px 24px rgba(0,0,0,.22); transition: transform .15s ease;
10
- overflow: hidden; padding: 0;
34
+ width: 58px; height: 58px; position: relative; display: flex; align-items: center;
35
+ justify-content: center; color: #fff; background: ${e};
36
+ border-radius: ${i?"16px":"999px"}; overflow: hidden;
37
+ box-shadow: 0 6px 16px -4px color-mix(in srgb, ${e} 55%, rgba(10,12,30,.4)), 0 2px 6px rgba(10,12,30,.18);
38
+ transition: transform .22s cubic-bezier(.34,1.56,.64,1), box-shadow .22s ease;
11
39
  }
12
- .bb-launcher:hover { transform: scale(1.06); }
13
- .bb-launcher img { width: 100%; height: 100%; object-fit: cover; }
40
+ .bb-launcher:hover { transform: scale(1.07) translateY(-1px); }
41
+ .bb-launcher:active { transform: scale(.96); }
42
+ .bb-launcher img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
43
+ transition: opacity .18s ease, transform .25s ease; }
44
+ .bb-launcher .bb-ic { width: 26px; height: 26px; position: absolute;
45
+ transition: opacity .18s ease, transform .25s ease; }
46
+ .bb-launcher .bb-l-close { opacity: 0; transform: rotate(-90deg) scale(.6); }
47
+ .bb.open .bb-launcher .bb-l-chat, .bb.open .bb-launcher img { opacity: 0; transform: rotate(90deg) scale(.6); }
48
+ .bb.open .bb-launcher img { transform: scale(1.15); }
49
+ .bb.open .bb-launcher .bb-l-close { opacity: 1; transform: rotate(0) scale(1); }
50
+
51
+ /* ---- window -------------------------------------------------------- */
14
52
  .bb-window {
15
- position: absolute; bottom: 70px; ${l}: 0; width: 360px; max-width: calc(100vw - 32px);
16
- height: 540px; max-height: calc(100vh - 110px); display: none; flex-direction: column;
17
- background: #fff; border-radius: 16px; overflow: hidden;
18
- box-shadow: 0 16px 48px rgba(0,0,0,.24); border: 1px solid rgba(0,0,0,.06);
53
+ position: absolute; bottom: 74px; ${t}: 0;
54
+ width: 384px; max-width: calc(100vw - 32px);
55
+ height: min(620px, calc(100vh - 122px));
56
+ display: flex; flex-direction: column; background: #fff;
57
+ border-radius: ${o}; overflow: hidden;
58
+ 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);
59
+ opacity: 0; transform: translateY(10px) scale(.97);
60
+ transform-origin: bottom ${t==="left"?"left":"right"};
61
+ pointer-events: none;
62
+ transition: opacity .2s ease, transform .24s cubic-bezier(.22,1.2,.36,1);
63
+ }
64
+ .bb.open .bb-window { opacity: 1; transform: none; pointer-events: auto; }
65
+ /* Shopper-expanded = the large centered dialog (same as full-screen mode) */
66
+ .bb.expanded .bb-window { position: fixed;
67
+ inset: min(6vh, 60px) max(24px, calc((100vw - 1680px) / 2));
68
+ width: auto; max-width: none; height: auto; }
69
+ ${l?`.bb-window { bottom: 0; top: auto; ${t}: 0; height: calc(100vh - 98px); border-end-start-radius: ${o}; }
70
+ .bb.expanded .bb-window { width: min(560px, calc(100vw - 40px)); }`:""}
71
+ ${s?`.bb.open .bb-window { position: fixed;
72
+ inset: min(6vh, 60px) max(24px, calc((100vw - 1680px) / 2));
73
+ width: auto; max-width: none; height: auto; }
74
+ .bb.open .bb-launcher { opacity: 0; pointer-events: none; }`:""}
75
+ @media (max-width: 520px) {
76
+ .bb { bottom: 16px; ${t}: 16px; }
77
+ .bb.open .bb-window { position: fixed; inset: 0; width: 100%; max-width: none; height: 100%; max-height: none; border-radius: 0; }
78
+ .bb.open .bb-launcher { opacity: 0; pointer-events: none; }
19
79
  }
20
- .bb-window.open { display: flex; }
21
- .bb-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: ${t}; color: #fff; }
22
- .bb-avatar { width: 34px; height: 34px; border-radius: 9999px; background: rgba(255,255,255,.25);
23
- display: flex; align-items: center; justify-content: center; font-weight: 600; overflow: hidden; flex-shrink: 0; }
24
- .bb-avatar img { width: 100%; height: 100%; object-fit: cover; }
80
+
81
+ /* ---- header -------------------------------------------------------- */
82
+ .bb-header { display: flex; align-items: center; gap: 11px; padding: 13px 16px;
83
+ background: #fff; border-bottom: 1px solid #eef0f4; flex-shrink: 0; }
84
+ .bb-avatar { width: 38px; height: 38px; position: relative; flex-shrink: 0;
85
+ border-radius: 999px; background: color-mix(in srgb, ${e} 14%, #fff);
86
+ color: ${e}; font-size: 15px; font-weight: 700;
87
+ display: flex; align-items: center; justify-content: center;
88
+ box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px color-mix(in srgb, ${e} 35%, #fff); }
89
+ .bb-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
90
+ .bb-avatar::after { content: ''; position: absolute; bottom: -1px; inset-inline-end: -1px;
91
+ width: 10px; height: 10px; border-radius: 999px; background: #22c55e; border: 2px solid #fff; }
25
92
  .bb-head-main { flex: 1; min-width: 0; }
26
- .bb-name { font-size: 14px; font-weight: 600; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
27
- .bb-status { font-size: 11px; opacity: .9; display: flex; align-items: center; gap: 5px; }
28
- .bb-dot { width: 6px; height: 6px; border-radius: 9999px; background: #34d399; }
29
- .bb-iconbtn { background: none; border: none; color: #fff; cursor: pointer; opacity: .85; font-size: 16px; padding: 4px; }
30
- .bb-iconbtn:hover { opacity: 1; }
31
- .bb-messages { flex: 1; overflow-y: auto; padding: 14px; background: #f7f7f9; display: flex; flex-direction: column; gap: 8px; }
32
- .bb-msg { max-width: 80%; padding: 9px 12px; border-radius: ${d}; font-size: 13.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
33
- .bb-msg.bot { align-self: flex-start; background: #fff; border: 1px solid rgba(0,0,0,.07); border-end-start-radius: 4px; }
34
- .bb-msg.user { align-self: flex-end; background: ${t}; color: #fff; border-end-end-radius: 4px; }
35
- .bb-msg.err { align-self: flex-start; background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; }
36
- .bb-typing { align-self: flex-start; font-size: 11.5px; color: #6b7280; padding: 2px 4px; }
37
- .bb-card { align-self: flex-start; width: 230px; background: #fff; border: 1px solid rgba(0,0,0,.08);
38
- border-radius: 12px; overflow: hidden; text-decoration: none; color: inherit; display: block; }
39
- .bb-card img { width: 100%; height: 120px; object-fit: cover; display: block; background: #eee; }
40
- .bb-card-body { padding: 9px 11px; }
41
- .bb-card-title { font-size: 13px; font-weight: 600; margin: 0 0 3px; }
42
- .bb-card-price { font-size: 13px; color: ${t}; font-weight: 600; }
43
- .bb-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px 10px; background: #f7f7f9; }
44
- .bb-chip { border: 1px solid ${t}; color: ${t}; background: #fff; border-radius: 9999px;
45
- font-size: 12px; padding: 5px 11px; cursor: pointer; }
46
- .bb-inputrow { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid rgba(0,0,0,.07); background: #fff; }
47
- .bb-input { flex: 1; border: none; outline: none; font-size: 13.5px; background: #f1f1f4; border-radius: 9999px; padding: 9px 14px; }
48
- .bb-send { border: none; background: ${t}; color: #fff; width: 36px; height: 36px; border-radius: 9999px; cursor: pointer; font-size: 15px; flex-shrink: 0; }
49
- .bb-send:disabled { opacity: .5; cursor: default; }
50
- .bb-esc { padding: 12px 14px; background: #fff; border-top: 1px solid rgba(0,0,0,.07); display: none; flex-direction: column; gap: 8px; }
51
- .bb-esc.open { display: flex; }
52
- .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; }
53
- .bb-esc button { border: none; background: ${t}; color: #fff; border-radius: 8px; padding: 8px; font-size: 13px; cursor: pointer; }
54
- .bb-esc-note { font-size: 12px; color: #047857; }
55
- `,this.root.appendChild(x);let o=document.createElement("div");o.className="bb-root",this.root.appendChild(o),this.windowEl=document.createElement("div"),this.windowEl.className="bb-window",o.appendChild(this.windowEl);let m=this.settings.displayName||"Assistant",a=document.createElement("div");a.className="bb-header";let c=document.createElement("span");if(c.className="bb-avatar",this.settings.avatarUrl&&k(this.settings.avatarUrl)){let i=document.createElement("img");i.src=this.settings.avatarUrl,i.alt="",c.appendChild(i)}else c.textContent=m.charAt(0).toUpperCase();let p=document.createElement("span");p.className="bb-head-main";let b=document.createElement("span");b.className="bb-name",b.textContent=m;let v=document.createElement("span");v.className="bb-status";let T=document.createElement("span");T.className="bb-dot",v.appendChild(T),v.appendChild(document.createTextNode(this.t("online"))),p.appendChild(b),p.appendChild(v);let g=document.createElement("button");g.className="bb-iconbtn",g.dataset.act="esc",g.title=this.t("leaveMessage"),g.textContent="\u2709";let f=document.createElement("button");f.className="bb-iconbtn",f.dataset.act="close",f.title=this.t("close"),f.textContent="\u2715",a.appendChild(c),a.appendChild(p),a.appendChild(g),a.appendChild(f),this.windowEl.appendChild(a),a.querySelector('[data-act="close"]')?.addEventListener("click",()=>this.close()),a.querySelector('[data-act="esc"]')?.addEventListener("click",()=>this.toggleEscalation()),this.messagesEl=document.createElement("div"),this.messagesEl.className="bb-messages",this.windowEl.appendChild(this.messagesEl),this.chipsEl=document.createElement("div"),this.chipsEl.className="bb-chips";for(let i of this.settings.starterQuestions??[]){let I=document.createElement("button");I.className="bb-chip",I.textContent=i,I.addEventListener("click",()=>this.send(i)),this.chipsEl.appendChild(I)}this.windowEl.appendChild(this.chipsEl);let h=document.createElement("div");h.className="bb-esc";let E=document.createElement("input");E.type="email",E.name="email",E.placeholder=this.t("yourEmail");let y=document.createElement("textarea");y.name="message",y.rows=2,y.placeholder=this.t("yourMessage");let L=document.createElement("button");L.type="button",L.textContent=this.t("send"),h.appendChild(E),h.appendChild(y),h.appendChild(L),h.querySelector("button")?.addEventListener("click",()=>this.submitEscalation(h)),this.windowEl.appendChild(h);let w=document.createElement("div");w.className="bb-inputrow",this.inputEl=document.createElement("input"),this.inputEl.className="bb-input",this.inputEl.placeholder=this.t("placeholder"),this.inputEl.addEventListener("keydown",i=>{i.key==="Enter"&&this.send(this.inputEl?.value??"")});let C=document.createElement("button");C.className="bb-send",C.textContent="\u27A4",C.addEventListener("click",()=>this.send(this.inputEl?.value??"")),w.appendChild(this.inputEl),w.appendChild(C),this.windowEl.appendChild(w);let u=document.createElement("button");if(u.className="bb-launcher",u.setAttribute("aria-label",m),this.settings.avatarUrl&&k(this.settings.avatarUrl)){let i=document.createElement("img");i.src=this.settings.avatarUrl,i.alt="",u.appendChild(i)}else u.textContent="\u{1F4AC}";u.addEventListener("click",()=>this.opened?this.close():this.open()),o.appendChild(u),e.appendChild(this.host)}open(){!this.windowEl||this.opened||(this.opened=!0,this.windowEl.classList.add("open"),this.messagesEl&&this.messagesEl.childElementCount===0&&this.primeThread(),this.inputEl?.focus())}close(){this.opened=!1,this.windowEl?.classList.remove("open")}async primeThread(){if(this.conversationId&&this.sessionId)try{let e=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/conversations/${encodeURIComponent(this.conversationId)}?limit=50`,{headers:{"X-Bot-Session":this.sessionId}});if(e.ok){let t=await e.json();for(let n of t.data)this.appendMessage(n.role==="assistant"?"bot":"user",n.content);if(t.data.length>0){this.chipsEl?.remove();return}}else this.conversationId=null,this.sessionId=null,this.persistIds()}catch{}this.settings.greeting&&this.appendMessage("bot",this.settings.greeting)}async send(e){let t=e.trim();if(!t||this.busy)return;this.busy=!0,this.inputEl&&(this.inputEl.value=""),this.chipsEl?.remove(),this.appendMessage("user",t);let n=this.appendTyping(),d=null;try{let s=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:t,turnId:$("trn_"),...this.conversationId?{conversationId:this.conversationId}:{},...this.sessionId?{anonymousSessionId:this.sessionId}:{},locale:this.locale})});if(!s.ok||!s.body)throw new Error(`chat failed (${s.status})`);let l=s.body.getReader(),x=new TextDecoder,o="";for(;;){let{value:m,done:a}=await l.read();if(a)break;o+=x.decode(m,{stream:!0});let c;for(;(c=o.indexOf(`
56
-
57
- `))>=0;){let p=o.slice(0,c);if(o=o.slice(c+2),!p.startsWith("data: "))continue;let b;try{b=JSON.parse(p.slice(6))}catch{continue}d=this.handleFrame(b,n,d)}}}catch{this.appendMessage("err",this.t("error"))}finally{n.remove(),this.busy=!1}}handleFrame(e,t,n){switch(e.type){case"connected":return this.conversationId=e.conversationId||this.conversationId,this.sessionId=e.anonymousSessionId||this.sessionId,this.persistIds(),n;case"token":return n||(t.remove(),n=this.appendMessage("bot","")),n.textContent=(n.textContent??"")+e.text,this.scrollDown(),n;case"tool":return t.textContent=e.status==="running"?"\u22EF":"",n;case"card":return this.appendCard(e.card),n;case"error":return this.appendMessage("err",e.message||this.t("error")),n;case"done":default:return n}}toggleEscalation(){this.root?.querySelector(".bb-esc")?.classList.toggle("open")}async submitEscalation(e){let t=e.querySelector('input[name="email"]')?.value.trim(),n=e.querySelector('textarea[name="message"]')?.value.trim();if(!(!t||!n||!this.conversationId||!this.sessionId))try{if((await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/escalate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:t,message:n,conversationId:this.conversationId,anonymousSessionId:this.sessionId,locale:this.locale})})).ok){let s=document.createElement("span");s.className="bb-esc-note",s.textContent=this.t("sent"),e.replaceChildren(s)}}catch{}}appendCard(e){if(!this.messagesEl)return;let t=document.createElement("a");if(t.className="bb-card",t.href=k(e.url)?e.url:"#",e.imageUrl&&k(e.imageUrl)){let l=document.createElement("img");l.src=e.imageUrl,l.alt="",t.appendChild(l)}let n=document.createElement("span");n.className="bb-card-body";let d=document.createElement("p");d.className="bb-card-title",d.textContent=e.title;let s=document.createElement("span");s.className="bb-card-price",s.textContent=e.price.formatted,n.appendChild(d),n.appendChild(s),t.appendChild(n),t.addEventListener("click",()=>{try{navigator.sendBeacon?.(`${this.baseUrl}/api/storefront-bot/attribution/click`,new Blob([JSON.stringify({botRef:e.botRef})],{type:"application/json"}))}catch{}}),this.messagesEl.appendChild(t),this.scrollDown()}appendMessage(e,t){let n=document.createElement("div");return n.className=`bb-msg ${e}`,n.textContent=t,this.messagesEl?.appendChild(n),this.scrollDown(),n}appendTyping(){let e=document.createElement("div");return e.className="bb-typing",e.textContent="\u22EF",this.messagesEl?.appendChild(e),this.scrollDown(),e}scrollDown(){this.messagesEl&&(this.messagesEl.scrollTop=this.messagesEl.scrollHeight)}};function k(r){return/^\/(?!\/)/.test(r)||/^https?:\/\//i.test(r)}(()=>{let r=document.currentScript,e=r?.dataset.connectionId;if(!e){console.warn("[BrainerceBot] missing data-connection-id on the bot.js script tag");return}let t=r?.dataset.apiBase||void 0,n=()=>void S.mount({connectionId:e,baseUrl:t});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",n,{once:!0}):n()})();})();
93
+ .bb-name { display: block; font-size: 13.5px; font-weight: 650; color: #14161f;
94
+ letter-spacing: -.01em; line-height: 1.25;
95
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
96
+ .bb-status { display: block; font-size: 11px; color: #8a8f9e; line-height: 1.3; }
97
+ .bb-actions { display: flex; align-items: center; gap: 2px; }
98
+ .bb-iconbtn { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
99
+ border-radius: 8px; color: #9aa0ae; transition: background .12s ease, color .12s ease; }
100
+ .bb-iconbtn:hover { background: #f3f4f7; color: #4d5260; }
101
+ .bb-iconbtn .bb-ic { width: 17px; height: 17px; }
102
+
103
+ /* ---- messages ------------------------------------------------------ */
104
+ .bb-msgs { flex: 1; overflow-y: auto; overscroll-behavior: contain;
105
+ padding: 16px 14px 10px; display: flex; flex-direction: column; gap: 10px;
106
+ background: linear-gradient(color-mix(in srgb, ${e} 4%, #f7f8fa), #f7f8fa 140px); }
107
+ .bb-msgs::-webkit-scrollbar { width: 5px; }
108
+ .bb-msgs::-webkit-scrollbar-thumb { background: rgba(20,22,31,.12); border-radius: 99px; }
109
+ .bb-msg { max-width: 84%; padding: 9px 13px; font-size: 13px; line-height: 1.55;
110
+ word-break: break-word; animation: bb-in .2s ease both; unicode-bidi: plaintext; }
111
+ .bb-msg p + p, .bb-msg p + ul, .bb-msg ul + p { margin-top: 6px; }
112
+ .bb-msg ul { padding-inline-start: 18px; }
113
+ .bb-msg li { margin: 2px 0; }
114
+ .bb-msg a { color: ${e}; font-weight: 550; text-decoration: underline; text-underline-offset: 2px; }
115
+ .bb-msg.bot { align-self: flex-start; background: #fff; color: #232633;
116
+ border: 1px solid #eceef3; border-radius: ${r}; border-end-start-radius: ${d};
117
+ box-shadow: 0 1px 2px rgba(15,18,34,.04); }
118
+ .bb-msg.user { align-self: flex-end; background: ${e}; color: #fff;
119
+ border-radius: ${r}; border-end-end-radius: ${d};
120
+ box-shadow: 0 2px 6px -2px color-mix(in srgb, ${e} 50%, rgba(10,12,30,.3)); }
121
+ .bb-msg.err { align-self: flex-start; background: #fef2f2; color: #b91c1c;
122
+ border: 1px solid #fecaca; border-radius: ${r}; }
123
+ @keyframes bb-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
124
+
125
+ /* typing / tool status */
126
+ .bb-typing { align-self: flex-start; display: flex; align-items: center; gap: 8px;
127
+ background: #fff; border: 1px solid #eceef3; border-radius: ${r};
128
+ border-end-start-radius: ${d}; padding: 10px 13px; animation: bb-in .2s ease both; }
129
+ .bb-typing .bb-dots { display: flex; gap: 4px; }
130
+ .bb-typing .bb-dots i { width: 6px; height: 6px; border-radius: 99px;
131
+ background: color-mix(in srgb, ${e} 65%, #aab); animation: bb-bounce 1.2s ease-in-out infinite; }
132
+ .bb-typing .bb-dots i:nth-child(2) { animation-delay: .15s; }
133
+ .bb-typing .bb-dots i:nth-child(3) { animation-delay: .3s; }
134
+ .bb-typing .bb-tool { font-size: 11.5px; color: #8a8f9e; display: none; }
135
+ .bb-typing.searching .bb-tool { display: block; }
136
+ @keyframes bb-bounce { 0%, 60%, 100% { transform: none; opacity: .55; } 30% { transform: translateY(-4px); opacity: 1; } }
137
+
138
+ /* ---- product cards ------------------------------------------------- */
139
+ .bb-shelf { align-self: stretch; animation: bb-in .25s ease both; }
140
+ .bb-shelf-cap { font-size: 10.5px; font-weight: 650; letter-spacing: .07em; text-transform: uppercase;
141
+ color: #9aa0ae; padding: 2px 4px 6px; }
142
+ .bb-cards { display: flex; gap: 10px; overflow-x: auto; padding: 2px 2px 8px;
143
+ scroll-snap-type: x proximity; scrollbar-width: none; }
144
+ .bb-cards::-webkit-scrollbar { display: none; }
145
+ .bb-card { flex: 0 0 var(--bb-card-w); scroll-snap-align: start; background: #fff;
146
+ border: 1px solid #eceef3; border-radius: 12px; overflow: hidden;
147
+ display: flex; flex-direction: column;
148
+ box-shadow: 0 1px 2px rgba(15,18,34,.04);
149
+ transition: transform .16s ease, box-shadow .16s ease; }
150
+ .bb-card:hover { transform: translateY(-2px); box-shadow: 0 8px 20px -8px rgba(15,18,34,.18); }
151
+ .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; }
152
+ .bb-card-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
153
+ .bb-card-img .bb-ic { width: 22px; height: 22px; color: #c6c9d4; margin: auto; }
154
+ .bb-card-body { padding: 8px 10px 10px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
155
+ .bb-card-title { font-size: var(--bb-title-fs); font-weight: 600; color: #1c1e29; line-height: 1.35;
156
+ letter-spacing: -.005em; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
157
+ overflow: hidden; min-height: 2.7em; cursor: pointer; unicode-bidi: plaintext; }
158
+ .bb-card-title:hover { color: ${e}; }
159
+ .bb-card-price { font-size: var(--bb-price-fs); font-weight: 700; color: #14161f;
160
+ font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
161
+ .bb-card-cta { display: flex; gap: 5px; margin-top: auto; }
162
+ .bb-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 5px;
163
+ height: var(--bb-btn-h); border-radius: 8px; font-size: 11px; font-weight: 650;
164
+ white-space: nowrap; padding: 0 6px;
165
+ transition: filter .12s ease, background .12s ease, color .12s ease; }
166
+ .bb-btn .bb-ic { width: 13px; height: 13px; }
167
+ .bb-btn-add { background: ${e}; color: #fff; }
168
+ .bb-btn-add:hover { filter: brightness(1.08); }
169
+ .bb-btn-add[data-state="busy"] { opacity: .65; pointer-events: none; }
170
+ .bb-btn-add[data-state="done"] { background: #059669; pointer-events: none; }
171
+ .bb-btn-ghost { background: #fff; border: 1px solid #e3e5ec; color: #3c4150; flex: 0 0 auto; padding: 0 11px; }
172
+ .bb-btn-ghost:hover { background: #f6f7f9; border-color: #d5d8e1; }
173
+ .bb-btn-ghost.bb-wide { flex: 1; color: ${e}; border-color: color-mix(in srgb, ${e} 35%, #e3e5ec); }
174
+ .bb-btn-ghost.bb-wide:hover { background: color-mix(in srgb, ${e} 6%, #fff); }
175
+ .bb-btn:disabled { opacity: .45; pointer-events: none; }
176
+
177
+ /* in-card variant picker */
178
+ .bb-card.picking .bb-card-cta { display: none; }
179
+ .bb-pick { display: flex; flex-direction: column; gap: 6px; margin-top: 4px;
180
+ padding-top: 8px; border-top: 1px dashed #e9ebf0; position: relative; }
181
+ .bb-pick-close { position: absolute; top: 6px; inset-inline-end: 0;
182
+ width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
183
+ border-radius: 6px; color: #9aa0ae; }
184
+ .bb-pick-close:hover { background: #f3f4f7; color: #4d5260; }
185
+ .bb-pick-close .bb-ic { width: 11px; height: 11px; }
186
+ .bb-pick-key { font-size: 9.5px; font-weight: 700; text-transform: uppercase;
187
+ letter-spacing: .06em; color: #9aa0ae; unicode-bidi: plaintext; }
188
+ .bb-pick-vals { display: flex; flex-wrap: wrap; gap: 4px; }
189
+ .bb-chipv { border: 1px solid #e3e5ec; border-radius: 7px; padding: 3px 9px;
190
+ font-size: 11px; font-weight: 550; color: #3c4150; background: #fff;
191
+ transition: border-color .12s ease, background .12s ease, color .12s ease; }
192
+ .bb-chipv:hover { border-color: color-mix(in srgb, ${e} 50%, #e3e5ec); }
193
+ .bb-chipv.sel { background: ${e}; border-color: ${e}; color: #fff; }
194
+ .bb-chipv.off { opacity: .35; pointer-events: none; }
195
+ .bb-pick-foot { display: flex; align-items: center; justify-content: space-between;
196
+ gap: 6px; margin-top: 2px; }
197
+ .bb-pick-foot .bb-btn { flex: 0 0 auto; padding: 0 11px; }
198
+ .bb-pick-price { font-size: 12.5px; font-weight: 700; color: #14161f;
199
+ font-variant-numeric: tabular-nums; }
200
+
201
+ /* ---- starter chips -------------------------------------------------- */
202
+ .bb-chips { display: flex; flex-wrap: wrap; gap: 7px; padding: 4px 14px 12px; background: #f7f8fa; }
203
+ .bb-chip { border: 1px solid color-mix(in srgb, ${e} 30%, #e3e5ec);
204
+ color: color-mix(in srgb, ${e} 85%, #000); background: #fff; border-radius: 999px;
205
+ font-size: 12px; font-weight: 550; padding: 6px 13px;
206
+ transition: background .12s ease, transform .12s ease; }
207
+ .bb-chip:hover { background: color-mix(in srgb, ${e} 7%, #fff); transform: translateY(-1px); }
208
+
209
+ /* ---- escalation sheet ----------------------------------------------- */
210
+ .bb-esc { display: none; flex-direction: column; gap: 8px; padding: 12px 14px;
211
+ background: #fff; border-top: 1px solid #eef0f4; flex-shrink: 0; }
212
+ .bb-esc.open { display: flex; animation: bb-in .18s ease both; }
213
+ .bb-esc-title { font-size: 12px; font-weight: 650; color: #3c4150; }
214
+ .bb-esc input, .bb-esc textarea { border: 1px solid #e3e5ec; border-radius: 10px;
215
+ padding: 8px 11px; font-size: 12.5px; color: #1c1e29; outline: none; resize: none; background: #fbfbfd; }
216
+ .bb-esc input:focus, .bb-esc textarea:focus { border-color: color-mix(in srgb, ${e} 55%, #e3e5ec);
217
+ box-shadow: 0 0 0 3px color-mix(in srgb, ${e} 12%, transparent); background: #fff; }
218
+ .bb-esc-send { height: 33px; border-radius: 10px; background: ${e}; color: #fff;
219
+ font-size: 12.5px; font-weight: 650; }
220
+ .bb-esc-send:hover { filter: brightness(1.08); }
221
+ .bb-esc-note { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: #047857; font-weight: 550; }
222
+ .bb-esc-note .bb-ic { width: 15px; height: 15px; }
223
+
224
+ /* ---- composer ------------------------------------------------------- */
225
+ .bb-composer { display: flex; align-items: center; gap: 8px; padding: 11px 12px;
226
+ background: #fff; border-top: 1px solid #eef0f4; flex-shrink: 0; }
227
+ .bb-input { flex: 1; border: 1px solid transparent; outline: none; font-size: 13px; color: #1c1e29;
228
+ background: #f1f2f5; border-radius: 999px; padding: 9px 15px; min-width: 0;
229
+ resize: none; overflow-y: hidden; height: 38px; line-height: 1.45;
230
+ transition: border-color .12s ease, box-shadow .12s ease, background .12s ease; }
231
+ .bb-input::placeholder { color: #9aa0ae; }
232
+ .bb-input:focus { background: #fff; border-color: color-mix(in srgb, ${e} 55%, #e3e5ec);
233
+ box-shadow: 0 0 0 3px color-mix(in srgb, ${e} 12%, transparent); }
234
+ .bb-send { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center;
235
+ justify-content: center; border-radius: 999px; background: ${e}; color: #fff;
236
+ transition: transform .15s cubic-bezier(.34,1.56,.64,1), opacity .12s ease, filter .12s ease; }
237
+ .bb-send .bb-ic { width: 17px; height: 17px; ${n==="rtl"?"transform: scaleX(-1);":""} }
238
+ .bb-send:hover { filter: brightness(1.08); transform: scale(1.06); }
239
+ .bb-send:disabled { opacity: .4; pointer-events: none; }
240
+ `}render(e){let n=this.settings.accentColor||"#6366F1",t=R.has(this.locale)?"rtl":"ltr",i=this.settings.position==="start"?"left":"right",o=t==="rtl"?i==="left"?"right":"left":i,r=this.settings.displayName||"Assistant";this.host=document.createElement("div"),this.host.setAttribute("data-brainerce-bot",this.connectionId),this.root=this.host.attachShadow({mode:"open"});let d=document.createElement("style");d.textContent=this.css(n,t,o),this.root.appendChild(d);let a=document.createElement("div");a.className="bb",this.root.appendChild(a);let l=document.createElement("div");l.className="bb-scrim",l.addEventListener("click",()=>{this.expanded?this.toggleExpand():this.close()}),a.appendChild(l),this.windowEl=document.createElement("div"),this.windowEl.className="bb-window",a.appendChild(this.windowEl);let s=document.createElement("div");s.className="bb-header";let h=document.createElement("span");if(h.className="bb-avatar",this.settings.avatarUrl&&T(this.settings.avatarUrl)){let p=document.createElement("img");p.src=this.settings.avatarUrl,p.alt="",h.appendChild(p)}else h.appendChild(document.createTextNode(r.charAt(0).toUpperCase()));let u=document.createElement("span");u.className="bb-head-main";let g=document.createElement("span");g.className="bb-name",g.textContent=r;let v=document.createElement("span");v.className="bb-status",v.textContent=this.t("online"),u.appendChild(g),u.appendChild(v);let x=document.createElement("span");x.className="bb-actions",(this.settings.displayMode??"floating")!=="full_screen"&&this.settings.allowExpand!==!1&&(this.expandBtn=this.iconButton("expand",this.t("expand"),()=>this.toggleExpand()),x.appendChild(this.expandBtn)),x.appendChild(this.iconButton("mail",this.t("leaveMessage"),()=>this.toggleEscalation())),x.appendChild(this.iconButton("close",this.t("close"),()=>this.close())),s.appendChild(h),s.appendChild(u),s.appendChild(x),this.windowEl.appendChild(s),this.messagesEl=document.createElement("div"),this.messagesEl.className="bb-msgs",this.windowEl.appendChild(this.messagesEl),this.chipsEl=document.createElement("div"),this.chipsEl.className="bb-chips";for(let p of this.settings.starterQuestions??[]){let I=document.createElement("button");I.className="bb-chip",I.textContent=p,I.addEventListener("click",()=>this.send(p)),this.chipsEl.appendChild(I)}this.windowEl.appendChild(this.chipsEl);let c=document.createElement("div");c.className="bb-esc";let E=document.createElement("span");E.className="bb-esc-title",E.textContent=this.t("leaveMessage");let y=document.createElement("input");y.type="email",y.name="email",y.placeholder=this.t("yourEmail");let C=document.createElement("textarea");C.name="message",C.rows=2,C.placeholder=this.t("yourMessage");let k=document.createElement("button");k.type="button",k.className="bb-esc-send",k.textContent=this.t("send"),k.addEventListener("click",()=>this.submitEscalation(c)),c.appendChild(E),c.appendChild(y),c.appendChild(C),c.appendChild(k),this.windowEl.appendChild(c);let m=document.createElement("div");if(m.className="bb-composer",this.inputEl=document.createElement("textarea"),this.inputEl.className="bb-input",this.inputEl.rows=1,this.inputEl.placeholder=this.t("placeholder"),this.inputEl.addEventListener("keydown",p=>{p.key==="Enter"&&!p.shiftKey&&(p.preventDefault(),this.send(this.inputEl?.value??""))}),this.inputEl.addEventListener("input",()=>this.syncSendState()),this.sendBtn=document.createElement("button"),this.sendBtn.className="bb-send",this.sendBtn.disabled=!0,this.sendBtn.setAttribute("aria-label",this.t("send")),this.sendBtn.appendChild(f("send")),this.sendBtn.addEventListener("click",()=>this.send(this.inputEl?.value??"")),m.appendChild(this.inputEl),m.appendChild(this.sendBtn),this.windowEl.appendChild(m),this.launcherEl=document.createElement("button"),this.launcherEl.className="bb-launcher",this.launcherEl.setAttribute("aria-label",r),this.settings.avatarUrl&&T(this.settings.avatarUrl)){let p=document.createElement("img");p.src=this.settings.avatarUrl,p.alt="",this.launcherEl.appendChild(p)}else{let p=f("chat");p.classList.add("bb-l-chat"),this.launcherEl.appendChild(p)}let L=f("close");L.classList.add("bb-l-close"),this.launcherEl.appendChild(L),this.launcherEl.addEventListener("click",()=>this.opened?this.close():this.open()),a.appendChild(this.launcherEl),e.appendChild(this.host)}iconButton(e,n,t){let i=document.createElement("button");return i.className="bb-iconbtn",i.title=n,i.setAttribute("aria-label",n),i.appendChild(f(e)),i.addEventListener("click",t),i}syncSendState(){this.sendBtn&&(this.sendBtn.disabled=!(this.inputEl?.value??"").trim()||this.busy)}open(){if(!this.windowEl||this.opened)return;this.opened=!0;let e=this.root?.querySelector(".bb");e?.classList.add("open");let n=(this.settings.displayMode??"floating")==="full_screen"||window.innerWidth<=520;e?.classList.toggle("big",n||this.expanded),this.messagesEl&&this.messagesEl.childElementCount===0&&this.primeThread(),this.inputEl?.focus()}close(){this.opened=!1;let e=this.root?.querySelector(".bb");e?.classList.remove("open"),this.expanded||e?.classList.remove("big")}toggleExpand(){if(this.expanded=!this.expanded,this.root?.querySelector(".bb")?.classList.toggle("expanded",this.expanded),this.root?.querySelector(".bb")?.classList.toggle("big",this.expanded),this.expandBtn){this.expandBtn.replaceChildren(f(this.expanded?"collapse":"expand"));let e=this.t(this.expanded?"collapse":"expand");this.expandBtn.title=e,this.expandBtn.setAttribute("aria-label",e)}}async primeThread(){if(this.conversationId&&this.sessionId)try{let e=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/conversations/${encodeURIComponent(this.conversationId)}?limit=50`,{headers:{"X-Bot-Session":this.sessionId}});if(e.ok){let n=await e.json();for(let t of n.data){let i=this.appendMessage(t.role==="assistant"?"bot":"user","");B(i,t.content)}if(n.data.length>0){this.chipsEl?.remove();return}}else this.conversationId=null,this.sessionId=null,this.persistIds()}catch{}if(this.settings.greeting){let e=this.appendMessage("bot","");B(e,this.settings.greeting)}}async send(e){let n=e.trim();if(!n||this.busy)return;this.busy=!0,this.inputEl&&(this.inputEl.value=""),this.syncSendState(),this.chipsEl?.remove(),this.appendMessage("user",n);let t=this.appendTyping();this.pendingText="",this.cardsRow=null,this.cardIds=new Set;let i=null;try{let o=await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:n,turnId:O("trn_"),...this.conversationId?{conversationId:this.conversationId}:{},...this.sessionId?{anonymousSessionId:this.sessionId}:{},locale:this.locale})});if(!o.ok||!o.body)throw new Error(`chat failed (${o.status})`);let r=o.body.getReader(),d=new TextDecoder,a="";for(;;){let{value:l,done:s}=await r.read();if(s)break;a+=d.decode(l,{stream:!0});let h;for(;(h=a.indexOf(`
241
+
242
+ `))>=0;){let u=a.slice(0,h);if(a=a.slice(h+2),!u.startsWith("data: "))continue;let g;try{g=JSON.parse(u.slice(6))}catch{continue}i=this.handleFrame(g,t,i)}}i&&this.pendingText&&B(i,this.pendingText)}catch{this.appendMessage("err",this.t("error"))}finally{t.remove(),this.busy=!1,this.syncSendState()}}handleFrame(e,n,t){switch(e.type){case"connected":return this.conversationId=e.conversationId||this.conversationId,this.sessionId=e.anonymousSessionId||this.sessionId,this.persistIds(),t;case"token":return t||(n.remove(),t=this.appendMessage("bot","")),this.pendingText+=e.text,t.textContent=this.pendingText,this.scrollDown(),t;case"tool":return n.classList.toggle("searching",e.status==="running"),t;case"card":return this.appendCard(e.card),t;case"error":return this.appendMessage("err",e.message||this.t("error")),t;case"done":default:return t}}appendCard(e){if(!this.messagesEl||this.cardIds.has(e.productId))return;if(this.cardIds.add(e.productId),!this.cardsRow){let s=document.createElement("div");s.className="bb-shelf";let h=document.createElement("div");h.className="bb-shelf-cap",h.textContent=this.t("results"),this.cardsRow=document.createElement("div"),this.cardsRow.className="bb-cards",s.appendChild(h),s.appendChild(this.cardsRow),this.messagesEl.appendChild(s)}let n=T(e.url)?e.url:null,t=document.createElement("div");t.className="bb-card";let i=()=>{this.beacon(e.botRef),n&&(window.location.href=n)},o=document.createElement("span");if(o.className="bb-card-img",e.imageUrl&&T(e.imageUrl)){let s=document.createElement("img");s.src=e.imageUrl,s.alt="",s.loading="lazy",o.appendChild(s)}else o.appendChild(f("cart"));o.addEventListener("click",i),t.appendChild(o);let r=document.createElement("span");r.className="bb-card-body";let d=document.createElement("span");d.className="bb-card-title",d.textContent=e.title,d.addEventListener("click",i);let a=document.createElement("span");a.className="bb-card-price",a.textContent=e.price.formatted,r.appendChild(d),r.appendChild(a);let l=document.createElement("span");if(l.className="bb-card-cta",e.requiresOptions){let s=document.createElement("button");s.className="bb-btn bb-btn-add",s.setAttribute("aria-label",this.t("addToCart")),s.appendChild(f("cart")),e.variants?.length?s.addEventListener("click",()=>this.togglePicker(r,e,o)):s.addEventListener("click",i),l.appendChild(s)}else{let s=document.createElement("button");s.className="bb-btn bb-btn-add",s.appendChild(f("cart")),s.appendChild(document.createTextNode(this.t("addToCart"))),s.addEventListener("click",()=>void this.addToCart(e,s));let h=document.createElement("button");h.className="bb-btn bb-btn-ghost",h.appendChild(document.createTextNode(this.t("view"))),h.addEventListener("click",i),l.appendChild(s),l.appendChild(h)}r.appendChild(l),t.appendChild(r),this.cardsRow.appendChild(t),this.scrollDown()}togglePicker(e,n,t){let i=e.parentElement,o=e.querySelector(".bb-pick");if(o){o.remove(),i?.classList.remove("picking");return}i?.classList.add("picking");let r=n.variants??[],d=[];for(let u of r)for(let g of Object.keys(u.attributes))d.includes(g)||d.push(g);let a={},l=document.createElement("span");l.className="bb-pick";let s=()=>r.find(u=>d.every(g=>a[g]&&u.attributes[g]===a[g]))??null,h=()=>{l.replaceChildren();for(let c of d){let E=document.createElement("span");E.className="bb-pick-key",E.textContent=c;let y=document.createElement("span");y.className="bb-pick-vals";let C=new Set;for(let k of r){let m=k.attributes[c];if(!m||C.has(m))continue;C.add(m);let L=r.some(I=>I.attributes[c]===m&&d.every(M=>M===c||!a[M]||I.attributes[M]===a[M])),p=document.createElement("button");p.className=`bb-chipv${a[c]===m?" sel":""}${L?"":" off"}`,p.textContent=m,p.addEventListener("click",()=>{a[c]===m?delete a[c]:a[c]=m,h()}),y.appendChild(p)}l.appendChild(E),l.appendChild(y)}let u=document.createElement("button");u.className="bb-pick-close",u.setAttribute("aria-label",this.t("close")),u.appendChild(f("close")),u.addEventListener("click",()=>{l.remove(),i?.classList.remove("picking")}),l.appendChild(u);let g=s(),v=document.createElement("span");v.className="bb-pick-foot";let x=document.createElement("span");x.className="bb-pick-price",x.textContent=g?g.price.formatted:"";let w=document.createElement("button");if(w.className="bb-btn bb-btn-add",w.appendChild(f("cart")),w.appendChild(document.createTextNode(this.t("addToCart"))),g||(w.disabled=!0),w.addEventListener("click",()=>{let c=s();c&&this.addToCart(n,w,c.id)}),v.appendChild(x),v.appendChild(w),l.appendChild(v),g?.imageUrl&&T(g.imageUrl)){let c=t.querySelector("img");c&&(c.src=g.imageUrl)}this.scrollDown()};h(),e.appendChild(l),this.scrollDown()}async addToCart(e,n,t){this.beacon(e.botRef),n.dataset.state="busy";let i=!1;try{if(this.onAddToCart)i=await this.onAddToCart({productId:e.productId,variantId:t??null,quantity:1})!==!1;else{let o=new CustomEvent("brainerce:bot:add-to-cart",{detail:{productId:e.productId,variantId:t??null,quantity:1,connectionId:this.connectionId},cancelable:!0,bubbles:!0,composed:!0});i=!window.dispatchEvent(o)}}catch{i=!1}i?(n.dataset.state="done",n.replaceChildren(f("check"),document.createTextNode(this.t("added"))),setTimeout(()=>{!this.destroyed&&n.isConnected&&(delete n.dataset.state,n.replaceChildren(f("cart"),document.createTextNode(this.t("addToCart"))))},2200)):(delete n.dataset.state,T(e.url)&&(window.location.href=e.url))}beacon(e){try{navigator.sendBeacon?.(`${this.baseUrl}/api/storefront-bot/attribution/click`,new Blob([JSON.stringify({botRef:e})],{type:"application/json"}))}catch{}}toggleEscalation(){this.root?.querySelector(".bb-esc")?.classList.toggle("open")}async submitEscalation(e){let n=e.querySelector('input[name="email"]')?.value.trim(),t=e.querySelector('textarea[name="message"]')?.value.trim();if(!(!n||!t||!this.conversationId||!this.sessionId))try{if((await fetch(`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/escalate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n,message:t,conversationId:this.conversationId,anonymousSessionId:this.sessionId,locale:this.locale})})).ok){let o=document.createElement("span");o.className="bb-esc-note",o.appendChild(f("check")),o.appendChild(document.createTextNode(this.t("sent"))),e.replaceChildren(o)}}catch{}}appendMessage(e,n){let t=document.createElement("div");return t.className=`bb-msg ${e}`,t.setAttribute("dir","auto"),t.textContent=n,this.messagesEl?.appendChild(t),this.scrollDown(),t}appendTyping(){let e=document.createElement("div");e.className="bb-typing";let n=document.createElement("span");n.className="bb-dots";for(let i=0;i<3;i++)n.appendChild(document.createElement("i"));let t=document.createElement("span");return t.className="bb-tool",t.textContent=this.t("searching"),e.appendChild(n),e.appendChild(t),this.messagesEl?.appendChild(e),this.scrollDown(),e}scrollDown(){this.messagesEl&&(this.messagesEl.scrollTop=this.messagesEl.scrollHeight)}};(()=>{let b=document.currentScript,e=b?.dataset.connectionId;if(!e){console.warn("[BrainerceBot] missing data-connection-id on the bot.js script tag");return}let n=b?.dataset.apiBase||void 0,t=()=>void N.mount({connectionId:e,baseUrl:n});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()})();})();
@@ -11,6 +11,10 @@
11
11
  * anonymousSessionId (localStorage). The server re-validates it on every call
12
12
  * and silently replaces it when stale — the widget always adopts whatever ids
13
13
  * arrive on the `connected` frame.
14
+ *
15
+ * Rendering safety: server/model strings NEVER pass through innerHTML. Text
16
+ * renders via textContent; the markdown-lite renderer builds DOM with
17
+ * createElement only; URLs pass the isSafeUrl gate before reaching href/src.
14
18
  */
15
19
  interface BrainerceBotOptions {
16
20
  /** Public vc_* connection id of the storefront. */
@@ -19,24 +23,43 @@ interface BrainerceBotOptions {
19
23
  baseUrl?: string;
20
24
  /** Mount target. Defaults to document.body. */
21
25
  target?: HTMLElement;
26
+ /**
27
+ * Host-provided add-to-cart. When present the widget calls it instead of
28
+ * navigating, so the host's own cart state/UI stays in sync. Return (or
29
+ * resolve) false to signal failure — the widget then falls back to the PDP.
30
+ */
31
+ onAddToCart?: (item: {
32
+ productId: string;
33
+ variantId?: string | null;
34
+ quantity: number;
35
+ }) => boolean | Promise<boolean>;
22
36
  }
23
37
  declare class BrainerceBot {
24
38
  private readonly connectionId;
25
39
  private readonly baseUrl;
26
40
  private readonly storageKey;
41
+ private readonly onAddToCart?;
27
42
  private host?;
28
43
  private root?;
29
44
  private windowEl?;
30
45
  private messagesEl?;
31
46
  private inputEl?;
47
+ private sendBtn?;
32
48
  private chipsEl?;
49
+ private launcherEl?;
50
+ private expandBtn?;
33
51
  private settings;
34
52
  private locale;
35
53
  private sessionId;
36
54
  private conversationId;
37
55
  private busy;
38
56
  private opened;
57
+ private expanded;
39
58
  private destroyed;
59
+ /** Per-turn streaming state. */
60
+ private pendingText;
61
+ private cardsRow;
62
+ private cardIds;
40
63
  private constructor();
41
64
  /** Boot the widget. Resolves to null when the bot is disabled server-side. */
42
65
  static mount(options: BrainerceBotOptions): Promise<BrainerceBot | null>;
@@ -45,16 +68,35 @@ declare class BrainerceBot {
45
68
  private t;
46
69
  private restoreIds;
47
70
  private persistIds;
71
+ private css;
48
72
  private render;
73
+ private iconButton;
74
+ private syncSendState;
49
75
  private open;
50
76
  private close;
77
+ private toggleExpand;
51
78
  /** First open: restore the server thread, or show the greeting. */
52
79
  private primeThread;
53
80
  private send;
54
81
  private handleFrame;
82
+ private appendCard;
83
+ /**
84
+ * In-card variant picker: one chip-row per attribute; a complete selection
85
+ * resolves to a variantId and becomes a real add-to-cart. Variant image and
86
+ * price update live. Built entirely with createElement (no innerHTML).
87
+ */
88
+ private togglePicker;
89
+ /**
90
+ * Add-to-cart resolution chain — must never be a dead button:
91
+ * 1. host `onAddToCart` option (scaffolded stores: syncs their cart UI)
92
+ * 2. cancelable `brainerce:bot:add-to-cart` CustomEvent (custom embeds)
93
+ * 3. fallback: navigate to the product page
94
+ */
95
+ private addToCart;
96
+ /** The durable conversion signal — fire-and-forget, never blocks. */
97
+ private beacon;
55
98
  private toggleEscalation;
56
99
  private submitEscalation;
57
- private appendCard;
58
100
  private appendMessage;
59
101
  private appendTyping;
60
102
  private scrollDown;
@@ -11,6 +11,10 @@
11
11
  * anonymousSessionId (localStorage). The server re-validates it on every call
12
12
  * and silently replaces it when stale — the widget always adopts whatever ids
13
13
  * arrive on the `connected` frame.
14
+ *
15
+ * Rendering safety: server/model strings NEVER pass through innerHTML. Text
16
+ * renders via textContent; the markdown-lite renderer builds DOM with
17
+ * createElement only; URLs pass the isSafeUrl gate before reaching href/src.
14
18
  */
15
19
  interface BrainerceBotOptions {
16
20
  /** Public vc_* connection id of the storefront. */
@@ -19,24 +23,43 @@ interface BrainerceBotOptions {
19
23
  baseUrl?: string;
20
24
  /** Mount target. Defaults to document.body. */
21
25
  target?: HTMLElement;
26
+ /**
27
+ * Host-provided add-to-cart. When present the widget calls it instead of
28
+ * navigating, so the host's own cart state/UI stays in sync. Return (or
29
+ * resolve) false to signal failure — the widget then falls back to the PDP.
30
+ */
31
+ onAddToCart?: (item: {
32
+ productId: string;
33
+ variantId?: string | null;
34
+ quantity: number;
35
+ }) => boolean | Promise<boolean>;
22
36
  }
23
37
  declare class BrainerceBot {
24
38
  private readonly connectionId;
25
39
  private readonly baseUrl;
26
40
  private readonly storageKey;
41
+ private readonly onAddToCart?;
27
42
  private host?;
28
43
  private root?;
29
44
  private windowEl?;
30
45
  private messagesEl?;
31
46
  private inputEl?;
47
+ private sendBtn?;
32
48
  private chipsEl?;
49
+ private launcherEl?;
50
+ private expandBtn?;
33
51
  private settings;
34
52
  private locale;
35
53
  private sessionId;
36
54
  private conversationId;
37
55
  private busy;
38
56
  private opened;
57
+ private expanded;
39
58
  private destroyed;
59
+ /** Per-turn streaming state. */
60
+ private pendingText;
61
+ private cardsRow;
62
+ private cardIds;
40
63
  private constructor();
41
64
  /** Boot the widget. Resolves to null when the bot is disabled server-side. */
42
65
  static mount(options: BrainerceBotOptions): Promise<BrainerceBot | null>;
@@ -45,16 +68,35 @@ declare class BrainerceBot {
45
68
  private t;
46
69
  private restoreIds;
47
70
  private persistIds;
71
+ private css;
48
72
  private render;
73
+ private iconButton;
74
+ private syncSendState;
49
75
  private open;
50
76
  private close;
77
+ private toggleExpand;
51
78
  /** First open: restore the server thread, or show the greeting. */
52
79
  private primeThread;
53
80
  private send;
54
81
  private handleFrame;
82
+ private appendCard;
83
+ /**
84
+ * In-card variant picker: one chip-row per attribute; a complete selection
85
+ * resolves to a variantId and becomes a real add-to-cart. Variant image and
86
+ * price update live. Built entirely with createElement (no innerHTML).
87
+ */
88
+ private togglePicker;
89
+ /**
90
+ * Add-to-cart resolution chain — must never be a dead button:
91
+ * 1. host `onAddToCart` option (scaffolded stores: syncs their cart UI)
92
+ * 2. cancelable `brainerce:bot:add-to-cart` CustomEvent (custom embeds)
93
+ * 3. fallback: navigate to the product page
94
+ */
95
+ private addToCart;
96
+ /** The durable conversion signal — fire-and-forget, never blocks. */
97
+ private beacon;
55
98
  private toggleEscalation;
56
99
  private submitEscalation;
57
- private appendCard;
58
100
  private appendMessage;
59
101
  private appendTyping;
60
102
  private scrollDown;