brainerce 1.28.1 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/bot/bootstrap.global.js +57 -0
- package/dist/bot/index.d.mts +63 -0
- package/dist/bot/index.d.ts +63 -0
- package/dist/bot/index.js +532 -0
- package/dist/bot/index.mjs +505 -0
- package/dist/index.d.mts +56 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +38 -0
- package/dist/index.mjs +38 -0
- package/package.json +7 -2
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// src/bot/widget.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
3
|
+
var RTL_LOCALES = /* @__PURE__ */ new Set(["he", "ar"]);
|
|
4
|
+
var CHROME = {
|
|
5
|
+
en: {
|
|
6
|
+
online: "Online",
|
|
7
|
+
placeholder: "Ask anything\u2026",
|
|
8
|
+
error: "Something went wrong \u2014 please try again.",
|
|
9
|
+
leaveMessage: "Leave a message for the team",
|
|
10
|
+
yourEmail: "Your email",
|
|
11
|
+
yourMessage: "Your message",
|
|
12
|
+
send: "Send",
|
|
13
|
+
sent: "Thanks! The team will get back to you by email.",
|
|
14
|
+
close: "Close"
|
|
15
|
+
},
|
|
16
|
+
he: {
|
|
17
|
+
online: "\u05DE\u05D7\u05D5\u05D1\u05E8",
|
|
18
|
+
placeholder: "\u05E9\u05D0\u05DC\u05D5 \u05D0\u05D5\u05EA\u05D9 \u05D4\u05DB\u05DC\u2026",
|
|
19
|
+
error: "\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9 \u2014 \u05E0\u05E1\u05D5 \u05E9\u05D5\u05D1.",
|
|
20
|
+
leaveMessage: "\u05D4\u05E9\u05D0\u05D9\u05E8\u05D5 \u05D4\u05D5\u05D3\u05E2\u05D4 \u05DC\u05E6\u05D5\u05D5\u05EA",
|
|
21
|
+
yourEmail: "\u05D4\u05D0\u05D9\u05DE\u05D9\u05D9\u05DC \u05E9\u05DC\u05DB\u05DD",
|
|
22
|
+
yourMessage: "\u05D4\u05D4\u05D5\u05D3\u05E2\u05D4 \u05E9\u05DC\u05DB\u05DD",
|
|
23
|
+
send: "\u05E9\u05DC\u05D9\u05D7\u05D4",
|
|
24
|
+
sent: "\u05EA\u05D5\u05D3\u05D4! \u05D4\u05E6\u05D5\u05D5\u05EA \u05D9\u05D7\u05D6\u05D5\u05E8 \u05D0\u05DC\u05D9\u05DB\u05DD \u05D1\u05DE\u05D9\u05D9\u05DC.",
|
|
25
|
+
close: "\u05E1\u05D2\u05D9\u05E8\u05D4"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function randomId(prefix) {
|
|
29
|
+
const bytes = new Uint8Array(16);
|
|
30
|
+
crypto.getRandomValues(bytes);
|
|
31
|
+
const b64 = btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
32
|
+
return `${prefix}${b64}`;
|
|
33
|
+
}
|
|
34
|
+
var BrainerceBot = class _BrainerceBot {
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.settings = { enabled: false };
|
|
37
|
+
this.locale = "en";
|
|
38
|
+
this.sessionId = null;
|
|
39
|
+
this.conversationId = null;
|
|
40
|
+
this.busy = false;
|
|
41
|
+
this.opened = false;
|
|
42
|
+
this.destroyed = false;
|
|
43
|
+
this.connectionId = options.connectionId;
|
|
44
|
+
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
45
|
+
this.storageKey = `brainerce-bot:${this.connectionId}`;
|
|
46
|
+
}
|
|
47
|
+
/** Boot the widget. Resolves to null when the bot is disabled server-side. */
|
|
48
|
+
static async mount(options) {
|
|
49
|
+
if (!options?.connectionId) {
|
|
50
|
+
console.warn("[BrainerceBot] connectionId is required");
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const bot = new _BrainerceBot(options);
|
|
54
|
+
const ok = await bot.boot(options.target ?? document.body);
|
|
55
|
+
return ok ? bot : null;
|
|
56
|
+
}
|
|
57
|
+
destroy() {
|
|
58
|
+
this.destroyed = true;
|
|
59
|
+
this.host?.remove();
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
async boot(target) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(
|
|
65
|
+
`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/settings`
|
|
66
|
+
);
|
|
67
|
+
if (!res.ok) return false;
|
|
68
|
+
this.settings = await res.json();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (!this.settings.enabled) return false;
|
|
73
|
+
this.locale = this.settings.languages?.[0] ?? "en";
|
|
74
|
+
this.restoreIds();
|
|
75
|
+
this.render(target);
|
|
76
|
+
if (this.settings.displayMode === "auto_open") {
|
|
77
|
+
setTimeout(() => !this.destroyed && this.open(), 3e3);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
t(key) {
|
|
82
|
+
return (CHROME[this.locale] ?? CHROME.en)[key] ?? CHROME.en[key] ?? key;
|
|
83
|
+
}
|
|
84
|
+
restoreIds() {
|
|
85
|
+
try {
|
|
86
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
87
|
+
if (raw) {
|
|
88
|
+
const parsed = JSON.parse(raw);
|
|
89
|
+
this.sessionId = parsed.sessionId ?? null;
|
|
90
|
+
this.conversationId = parsed.conversationId ?? null;
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
persistIds() {
|
|
96
|
+
try {
|
|
97
|
+
localStorage.setItem(
|
|
98
|
+
this.storageKey,
|
|
99
|
+
JSON.stringify({ sessionId: this.sessionId, conversationId: this.conversationId })
|
|
100
|
+
);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
105
|
+
// Rendering
|
|
106
|
+
// --------------------------------------------------------------------------
|
|
107
|
+
render(target) {
|
|
108
|
+
const accent = this.settings.accentColor || "#6366F1";
|
|
109
|
+
const dir = RTL_LOCALES.has(this.locale) ? "rtl" : "ltr";
|
|
110
|
+
const radius = this.settings.bubbleShape === "square" ? "8px" : "16px";
|
|
111
|
+
const side = this.settings.position === "start" ? "left" : "right";
|
|
112
|
+
const sideRtlAware = dir === "rtl" ? side === "left" ? "right" : "left" : side;
|
|
113
|
+
this.host = document.createElement("div");
|
|
114
|
+
this.host.setAttribute("data-brainerce-bot", this.connectionId);
|
|
115
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
116
|
+
const style = document.createElement("style");
|
|
117
|
+
style.textContent = `
|
|
118
|
+
:host { all: initial; }
|
|
119
|
+
* { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
120
|
+
.bb-root { position: fixed; bottom: 20px; ${sideRtlAware}: 20px; z-index: 2147483000; direction: ${dir}; }
|
|
121
|
+
.bb-launcher {
|
|
122
|
+
width: 56px; height: 56px; border: none; cursor: pointer; display: flex;
|
|
123
|
+
align-items: center; justify-content: center; color: #fff; background: ${accent};
|
|
124
|
+
border-radius: ${this.settings.bubbleShape === "square" ? "14px" : "9999px"};
|
|
125
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.22); transition: transform .15s ease;
|
|
126
|
+
overflow: hidden; padding: 0;
|
|
127
|
+
}
|
|
128
|
+
.bb-launcher:hover { transform: scale(1.06); }
|
|
129
|
+
.bb-launcher img { width: 100%; height: 100%; object-fit: cover; }
|
|
130
|
+
.bb-window {
|
|
131
|
+
position: absolute; bottom: 70px; ${sideRtlAware}: 0; width: 360px; max-width: calc(100vw - 32px);
|
|
132
|
+
height: 540px; max-height: calc(100vh - 110px); display: none; flex-direction: column;
|
|
133
|
+
background: #fff; border-radius: 16px; overflow: hidden;
|
|
134
|
+
box-shadow: 0 16px 48px rgba(0,0,0,.24); border: 1px solid rgba(0,0,0,.06);
|
|
135
|
+
}
|
|
136
|
+
.bb-window.open { display: flex; }
|
|
137
|
+
.bb-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; background: ${accent}; color: #fff; }
|
|
138
|
+
.bb-avatar { width: 34px; height: 34px; border-radius: 9999px; background: rgba(255,255,255,.25);
|
|
139
|
+
display: flex; align-items: center; justify-content: center; font-weight: 600; overflow: hidden; flex-shrink: 0; }
|
|
140
|
+
.bb-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
141
|
+
.bb-head-main { flex: 1; min-width: 0; }
|
|
142
|
+
.bb-name { font-size: 14px; font-weight: 600; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
143
|
+
.bb-status { font-size: 11px; opacity: .9; display: flex; align-items: center; gap: 5px; }
|
|
144
|
+
.bb-dot { width: 6px; height: 6px; border-radius: 9999px; background: #34d399; }
|
|
145
|
+
.bb-iconbtn { background: none; border: none; color: #fff; cursor: pointer; opacity: .85; font-size: 16px; padding: 4px; }
|
|
146
|
+
.bb-iconbtn:hover { opacity: 1; }
|
|
147
|
+
.bb-messages { flex: 1; overflow-y: auto; padding: 14px; background: #f7f7f9; display: flex; flex-direction: column; gap: 8px; }
|
|
148
|
+
.bb-msg { max-width: 80%; padding: 9px 12px; border-radius: ${radius}; font-size: 13.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
|
|
149
|
+
.bb-msg.bot { align-self: flex-start; background: #fff; border: 1px solid rgba(0,0,0,.07); border-end-start-radius: 4px; }
|
|
150
|
+
.bb-msg.user { align-self: flex-end; background: ${accent}; color: #fff; border-end-end-radius: 4px; }
|
|
151
|
+
.bb-msg.err { align-self: flex-start; background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; }
|
|
152
|
+
.bb-typing { align-self: flex-start; font-size: 11.5px; color: #6b7280; padding: 2px 4px; }
|
|
153
|
+
.bb-card { align-self: flex-start; width: 230px; background: #fff; border: 1px solid rgba(0,0,0,.08);
|
|
154
|
+
border-radius: 12px; overflow: hidden; text-decoration: none; color: inherit; display: block; }
|
|
155
|
+
.bb-card img { width: 100%; height: 120px; object-fit: cover; display: block; background: #eee; }
|
|
156
|
+
.bb-card-body { padding: 9px 11px; }
|
|
157
|
+
.bb-card-title { font-size: 13px; font-weight: 600; margin: 0 0 3px; }
|
|
158
|
+
.bb-card-price { font-size: 13px; color: ${accent}; font-weight: 600; }
|
|
159
|
+
.bb-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px 10px; background: #f7f7f9; }
|
|
160
|
+
.bb-chip { border: 1px solid ${accent}; color: ${accent}; background: #fff; border-radius: 9999px;
|
|
161
|
+
font-size: 12px; padding: 5px 11px; cursor: pointer; }
|
|
162
|
+
.bb-inputrow { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid rgba(0,0,0,.07); background: #fff; }
|
|
163
|
+
.bb-input { flex: 1; border: none; outline: none; font-size: 13.5px; background: #f1f1f4; border-radius: 9999px; padding: 9px 14px; }
|
|
164
|
+
.bb-send { border: none; background: ${accent}; color: #fff; width: 36px; height: 36px; border-radius: 9999px; cursor: pointer; font-size: 15px; flex-shrink: 0; }
|
|
165
|
+
.bb-send:disabled { opacity: .5; cursor: default; }
|
|
166
|
+
.bb-esc { padding: 12px 14px; background: #fff; border-top: 1px solid rgba(0,0,0,.07); display: none; flex-direction: column; gap: 8px; }
|
|
167
|
+
.bb-esc.open { display: flex; }
|
|
168
|
+
.bb-esc input, .bb-esc textarea { border: 1px solid rgba(0,0,0,.12); border-radius: 8px; padding: 8px 10px; font-size: 13px; outline: none; resize: none; }
|
|
169
|
+
.bb-esc button { border: none; background: ${accent}; color: #fff; border-radius: 8px; padding: 8px; font-size: 13px; cursor: pointer; }
|
|
170
|
+
.bb-esc-note { font-size: 12px; color: #047857; }
|
|
171
|
+
`;
|
|
172
|
+
this.root.appendChild(style);
|
|
173
|
+
const rootEl = document.createElement("div");
|
|
174
|
+
rootEl.className = "bb-root";
|
|
175
|
+
this.root.appendChild(rootEl);
|
|
176
|
+
this.windowEl = document.createElement("div");
|
|
177
|
+
this.windowEl.className = "bb-window";
|
|
178
|
+
rootEl.appendChild(this.windowEl);
|
|
179
|
+
const name = this.settings.displayName || "Assistant";
|
|
180
|
+
const header = document.createElement("div");
|
|
181
|
+
header.className = "bb-header";
|
|
182
|
+
const avatar = document.createElement("span");
|
|
183
|
+
avatar.className = "bb-avatar";
|
|
184
|
+
if (this.settings.avatarUrl && isSafeUrl(this.settings.avatarUrl)) {
|
|
185
|
+
const img = document.createElement("img");
|
|
186
|
+
img.src = this.settings.avatarUrl;
|
|
187
|
+
img.alt = "";
|
|
188
|
+
avatar.appendChild(img);
|
|
189
|
+
} else {
|
|
190
|
+
avatar.textContent = name.charAt(0).toUpperCase();
|
|
191
|
+
}
|
|
192
|
+
const headMain = document.createElement("span");
|
|
193
|
+
headMain.className = "bb-head-main";
|
|
194
|
+
const nameEl = document.createElement("span");
|
|
195
|
+
nameEl.className = "bb-name";
|
|
196
|
+
nameEl.textContent = name;
|
|
197
|
+
const statusEl = document.createElement("span");
|
|
198
|
+
statusEl.className = "bb-status";
|
|
199
|
+
const dot = document.createElement("span");
|
|
200
|
+
dot.className = "bb-dot";
|
|
201
|
+
statusEl.appendChild(dot);
|
|
202
|
+
statusEl.appendChild(document.createTextNode(this.t("online")));
|
|
203
|
+
headMain.appendChild(nameEl);
|
|
204
|
+
headMain.appendChild(statusEl);
|
|
205
|
+
const escBtn = document.createElement("button");
|
|
206
|
+
escBtn.className = "bb-iconbtn";
|
|
207
|
+
escBtn.dataset.act = "esc";
|
|
208
|
+
escBtn.title = this.t("leaveMessage");
|
|
209
|
+
escBtn.textContent = "\u2709";
|
|
210
|
+
const closeBtn = document.createElement("button");
|
|
211
|
+
closeBtn.className = "bb-iconbtn";
|
|
212
|
+
closeBtn.dataset.act = "close";
|
|
213
|
+
closeBtn.title = this.t("close");
|
|
214
|
+
closeBtn.textContent = "\u2715";
|
|
215
|
+
header.appendChild(avatar);
|
|
216
|
+
header.appendChild(headMain);
|
|
217
|
+
header.appendChild(escBtn);
|
|
218
|
+
header.appendChild(closeBtn);
|
|
219
|
+
this.windowEl.appendChild(header);
|
|
220
|
+
header.querySelector('[data-act="close"]')?.addEventListener("click", () => this.close());
|
|
221
|
+
header.querySelector('[data-act="esc"]')?.addEventListener("click", () => this.toggleEscalation());
|
|
222
|
+
this.messagesEl = document.createElement("div");
|
|
223
|
+
this.messagesEl.className = "bb-messages";
|
|
224
|
+
this.windowEl.appendChild(this.messagesEl);
|
|
225
|
+
this.chipsEl = document.createElement("div");
|
|
226
|
+
this.chipsEl.className = "bb-chips";
|
|
227
|
+
for (const q of this.settings.starterQuestions ?? []) {
|
|
228
|
+
const chip = document.createElement("button");
|
|
229
|
+
chip.className = "bb-chip";
|
|
230
|
+
chip.textContent = q;
|
|
231
|
+
chip.addEventListener("click", () => this.send(q));
|
|
232
|
+
this.chipsEl.appendChild(chip);
|
|
233
|
+
}
|
|
234
|
+
this.windowEl.appendChild(this.chipsEl);
|
|
235
|
+
const esc = document.createElement("div");
|
|
236
|
+
esc.className = "bb-esc";
|
|
237
|
+
const escEmail = document.createElement("input");
|
|
238
|
+
escEmail.type = "email";
|
|
239
|
+
escEmail.name = "email";
|
|
240
|
+
escEmail.placeholder = this.t("yourEmail");
|
|
241
|
+
const escMsg = document.createElement("textarea");
|
|
242
|
+
escMsg.name = "message";
|
|
243
|
+
escMsg.rows = 2;
|
|
244
|
+
escMsg.placeholder = this.t("yourMessage");
|
|
245
|
+
const escSend = document.createElement("button");
|
|
246
|
+
escSend.type = "button";
|
|
247
|
+
escSend.textContent = this.t("send");
|
|
248
|
+
esc.appendChild(escEmail);
|
|
249
|
+
esc.appendChild(escMsg);
|
|
250
|
+
esc.appendChild(escSend);
|
|
251
|
+
esc.querySelector("button")?.addEventListener("click", () => this.submitEscalation(esc));
|
|
252
|
+
this.windowEl.appendChild(esc);
|
|
253
|
+
const inputRow = document.createElement("div");
|
|
254
|
+
inputRow.className = "bb-inputrow";
|
|
255
|
+
this.inputEl = document.createElement("input");
|
|
256
|
+
this.inputEl.className = "bb-input";
|
|
257
|
+
this.inputEl.placeholder = this.t("placeholder");
|
|
258
|
+
this.inputEl.addEventListener("keydown", (e) => {
|
|
259
|
+
if (e.key === "Enter") this.send(this.inputEl?.value ?? "");
|
|
260
|
+
});
|
|
261
|
+
const sendBtn = document.createElement("button");
|
|
262
|
+
sendBtn.className = "bb-send";
|
|
263
|
+
sendBtn.textContent = "\u27A4";
|
|
264
|
+
sendBtn.addEventListener("click", () => this.send(this.inputEl?.value ?? ""));
|
|
265
|
+
inputRow.appendChild(this.inputEl);
|
|
266
|
+
inputRow.appendChild(sendBtn);
|
|
267
|
+
this.windowEl.appendChild(inputRow);
|
|
268
|
+
const launcher = document.createElement("button");
|
|
269
|
+
launcher.className = "bb-launcher";
|
|
270
|
+
launcher.setAttribute("aria-label", name);
|
|
271
|
+
if (this.settings.avatarUrl && isSafeUrl(this.settings.avatarUrl)) {
|
|
272
|
+
const img = document.createElement("img");
|
|
273
|
+
img.src = this.settings.avatarUrl;
|
|
274
|
+
img.alt = "";
|
|
275
|
+
launcher.appendChild(img);
|
|
276
|
+
} else {
|
|
277
|
+
launcher.textContent = "\u{1F4AC}";
|
|
278
|
+
}
|
|
279
|
+
launcher.addEventListener("click", () => this.opened ? this.close() : this.open());
|
|
280
|
+
rootEl.appendChild(launcher);
|
|
281
|
+
target.appendChild(this.host);
|
|
282
|
+
}
|
|
283
|
+
open() {
|
|
284
|
+
if (!this.windowEl || this.opened) return;
|
|
285
|
+
this.opened = true;
|
|
286
|
+
this.windowEl.classList.add("open");
|
|
287
|
+
if (this.messagesEl && this.messagesEl.childElementCount === 0) {
|
|
288
|
+
void this.primeThread();
|
|
289
|
+
}
|
|
290
|
+
this.inputEl?.focus();
|
|
291
|
+
}
|
|
292
|
+
close() {
|
|
293
|
+
this.opened = false;
|
|
294
|
+
this.windowEl?.classList.remove("open");
|
|
295
|
+
}
|
|
296
|
+
/** First open: restore the server thread, or show the greeting. */
|
|
297
|
+
async primeThread() {
|
|
298
|
+
if (this.conversationId && this.sessionId) {
|
|
299
|
+
try {
|
|
300
|
+
const res = await fetch(
|
|
301
|
+
`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/conversations/${encodeURIComponent(this.conversationId)}?limit=50`,
|
|
302
|
+
{ headers: { "X-Bot-Session": this.sessionId } }
|
|
303
|
+
);
|
|
304
|
+
if (res.ok) {
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
for (const m of data.data) {
|
|
307
|
+
this.appendMessage(m.role === "assistant" ? "bot" : "user", m.content);
|
|
308
|
+
}
|
|
309
|
+
if (data.data.length > 0) {
|
|
310
|
+
this.chipsEl?.remove();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
this.conversationId = null;
|
|
315
|
+
this.sessionId = null;
|
|
316
|
+
this.persistIds();
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (this.settings.greeting) this.appendMessage("bot", this.settings.greeting);
|
|
322
|
+
}
|
|
323
|
+
// --------------------------------------------------------------------------
|
|
324
|
+
// The chat turn
|
|
325
|
+
// --------------------------------------------------------------------------
|
|
326
|
+
async send(text) {
|
|
327
|
+
const message = text.trim();
|
|
328
|
+
if (!message || this.busy) return;
|
|
329
|
+
this.busy = true;
|
|
330
|
+
if (this.inputEl) this.inputEl.value = "";
|
|
331
|
+
this.chipsEl?.remove();
|
|
332
|
+
this.appendMessage("user", message);
|
|
333
|
+
const typing = this.appendTyping();
|
|
334
|
+
let botBubble = null;
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch(
|
|
337
|
+
`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,
|
|
338
|
+
{
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { "Content-Type": "application/json" },
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
message,
|
|
343
|
+
turnId: randomId("trn_"),
|
|
344
|
+
...this.conversationId ? { conversationId: this.conversationId } : {},
|
|
345
|
+
...this.sessionId ? { anonymousSessionId: this.sessionId } : {},
|
|
346
|
+
locale: this.locale
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
if (!res.ok || !res.body) {
|
|
351
|
+
throw new Error(`chat failed (${res.status})`);
|
|
352
|
+
}
|
|
353
|
+
const reader = res.body.getReader();
|
|
354
|
+
const decoder = new TextDecoder();
|
|
355
|
+
let buffer = "";
|
|
356
|
+
for (; ; ) {
|
|
357
|
+
const { value, done } = await reader.read();
|
|
358
|
+
if (done) break;
|
|
359
|
+
buffer += decoder.decode(value, { stream: true });
|
|
360
|
+
let idx;
|
|
361
|
+
while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
362
|
+
const raw = buffer.slice(0, idx);
|
|
363
|
+
buffer = buffer.slice(idx + 2);
|
|
364
|
+
if (!raw.startsWith("data: ")) continue;
|
|
365
|
+
let frame;
|
|
366
|
+
try {
|
|
367
|
+
frame = JSON.parse(raw.slice(6));
|
|
368
|
+
} catch {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
botBubble = this.handleFrame(frame, typing, botBubble);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
this.appendMessage("err", this.t("error"));
|
|
376
|
+
} finally {
|
|
377
|
+
typing.remove();
|
|
378
|
+
this.busy = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
handleFrame(frame, typing, botBubble) {
|
|
382
|
+
switch (frame.type) {
|
|
383
|
+
case "connected":
|
|
384
|
+
this.conversationId = frame.conversationId || this.conversationId;
|
|
385
|
+
this.sessionId = frame.anonymousSessionId || this.sessionId;
|
|
386
|
+
this.persistIds();
|
|
387
|
+
return botBubble;
|
|
388
|
+
case "token": {
|
|
389
|
+
if (!botBubble) {
|
|
390
|
+
typing.remove();
|
|
391
|
+
botBubble = this.appendMessage("bot", "");
|
|
392
|
+
}
|
|
393
|
+
botBubble.textContent = (botBubble.textContent ?? "") + frame.text;
|
|
394
|
+
this.scrollDown();
|
|
395
|
+
return botBubble;
|
|
396
|
+
}
|
|
397
|
+
case "tool":
|
|
398
|
+
typing.textContent = frame.status === "running" ? "\u22EF" : "";
|
|
399
|
+
return botBubble;
|
|
400
|
+
case "card":
|
|
401
|
+
this.appendCard(frame.card);
|
|
402
|
+
return botBubble;
|
|
403
|
+
case "error":
|
|
404
|
+
this.appendMessage("err", frame.message || this.t("error"));
|
|
405
|
+
return botBubble;
|
|
406
|
+
case "done":
|
|
407
|
+
default:
|
|
408
|
+
return botBubble;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// --------------------------------------------------------------------------
|
|
412
|
+
// Escalation + attribution
|
|
413
|
+
// --------------------------------------------------------------------------
|
|
414
|
+
toggleEscalation() {
|
|
415
|
+
this.root?.querySelector(".bb-esc")?.classList.toggle("open");
|
|
416
|
+
}
|
|
417
|
+
async submitEscalation(form) {
|
|
418
|
+
const email = form.querySelector('input[name="email"]')?.value.trim();
|
|
419
|
+
const message = form.querySelector('textarea[name="message"]')?.value.trim();
|
|
420
|
+
if (!email || !message || !this.conversationId || !this.sessionId) return;
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch(
|
|
423
|
+
`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/escalate`,
|
|
424
|
+
{
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers: { "Content-Type": "application/json" },
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
email,
|
|
429
|
+
message,
|
|
430
|
+
conversationId: this.conversationId,
|
|
431
|
+
anonymousSessionId: this.sessionId,
|
|
432
|
+
locale: this.locale
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
if (res.ok) {
|
|
437
|
+
const note = document.createElement("span");
|
|
438
|
+
note.className = "bb-esc-note";
|
|
439
|
+
note.textContent = this.t("sent");
|
|
440
|
+
form.replaceChildren(note);
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
appendCard(card) {
|
|
446
|
+
if (!this.messagesEl) return;
|
|
447
|
+
const a = document.createElement("a");
|
|
448
|
+
a.className = "bb-card";
|
|
449
|
+
a.href = isSafeUrl(card.url) ? card.url : "#";
|
|
450
|
+
if (card.imageUrl && isSafeUrl(card.imageUrl)) {
|
|
451
|
+
const img = document.createElement("img");
|
|
452
|
+
img.src = card.imageUrl;
|
|
453
|
+
img.alt = "";
|
|
454
|
+
a.appendChild(img);
|
|
455
|
+
}
|
|
456
|
+
const body = document.createElement("span");
|
|
457
|
+
body.className = "bb-card-body";
|
|
458
|
+
const title = document.createElement("p");
|
|
459
|
+
title.className = "bb-card-title";
|
|
460
|
+
title.textContent = card.title;
|
|
461
|
+
const priceEl = document.createElement("span");
|
|
462
|
+
priceEl.className = "bb-card-price";
|
|
463
|
+
priceEl.textContent = card.price.formatted;
|
|
464
|
+
body.appendChild(title);
|
|
465
|
+
body.appendChild(priceEl);
|
|
466
|
+
a.appendChild(body);
|
|
467
|
+
a.addEventListener("click", () => {
|
|
468
|
+
try {
|
|
469
|
+
navigator.sendBeacon?.(
|
|
470
|
+
`${this.baseUrl}/api/storefront-bot/attribution/click`,
|
|
471
|
+
new Blob([JSON.stringify({ botRef: card.botRef })], { type: "application/json" })
|
|
472
|
+
);
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
this.messagesEl.appendChild(a);
|
|
477
|
+
this.scrollDown();
|
|
478
|
+
}
|
|
479
|
+
// --------------------------------------------------------------------------
|
|
480
|
+
appendMessage(kind, text) {
|
|
481
|
+
const el = document.createElement("div");
|
|
482
|
+
el.className = `bb-msg ${kind}`;
|
|
483
|
+
el.textContent = text;
|
|
484
|
+
this.messagesEl?.appendChild(el);
|
|
485
|
+
this.scrollDown();
|
|
486
|
+
return el;
|
|
487
|
+
}
|
|
488
|
+
appendTyping() {
|
|
489
|
+
const el = document.createElement("div");
|
|
490
|
+
el.className = "bb-typing";
|
|
491
|
+
el.textContent = "\u22EF";
|
|
492
|
+
this.messagesEl?.appendChild(el);
|
|
493
|
+
this.scrollDown();
|
|
494
|
+
return el;
|
|
495
|
+
}
|
|
496
|
+
scrollDown() {
|
|
497
|
+
if (this.messagesEl) this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
function isSafeUrl(url) {
|
|
501
|
+
return /^\/(?!\/)/.test(url) || /^https?:\/\//i.test(url);
|
|
502
|
+
}
|
|
503
|
+
export {
|
|
504
|
+
BrainerceBot
|
|
505
|
+
};
|
package/dist/index.d.mts
CHANGED
|
@@ -3807,6 +3807,35 @@ interface PublicRegionPaymentProvider {
|
|
|
3807
3807
|
interface PublicRegionDetail extends PublicRegion {
|
|
3808
3808
|
paymentProviders: PublicRegionPaymentProvider[];
|
|
3809
3809
|
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Server-side region resolution for a buyer country (storefront seam).
|
|
3812
|
+
* `matched=true` → the country was explicitly listed by a region. `false` →
|
|
3813
|
+
* fell back to the default region (or `region=null` when no default exists).
|
|
3814
|
+
*/
|
|
3815
|
+
interface AutoRegionResponse {
|
|
3816
|
+
region: PublicRegion | null;
|
|
3817
|
+
matched: boolean;
|
|
3818
|
+
/** Upper-cased ISO-3166-1 alpha-2. Empty string when caller omitted `country`. */
|
|
3819
|
+
country: string;
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* Non-binding tax preview for PDP / PLP / cart. The authoritative calculation
|
|
3823
|
+
* still runs at checkout against the full shipping address (state / postal /
|
|
3824
|
+
* per-class). Render with an "Estimate" affordance.
|
|
3825
|
+
*/
|
|
3826
|
+
interface TaxEstimateResponse {
|
|
3827
|
+
appliesTax: boolean;
|
|
3828
|
+
/** Percent — e.g. 18 for 18%. `null` when no matching rule. */
|
|
3829
|
+
rate: number | null;
|
|
3830
|
+
rateName: string | null;
|
|
3831
|
+
/** Tax portion of `subtotal` at the store's `pricesIncludeTax` mode. */
|
|
3832
|
+
estimatedTax: number;
|
|
3833
|
+
pricesIncludeTax: boolean;
|
|
3834
|
+
/** Store currency — the cart / order currency, unchanged by FX display overlay. */
|
|
3835
|
+
currency: string;
|
|
3836
|
+
/** Disclaimer copy — always present to surface the "preview, not binding" nature. */
|
|
3837
|
+
note: string;
|
|
3838
|
+
}
|
|
3810
3839
|
interface CreateRegionDto {
|
|
3811
3840
|
name: string;
|
|
3812
3841
|
/** ISO 4217 */
|
|
@@ -8616,6 +8645,33 @@ declare class BrainerceClient {
|
|
|
8616
8645
|
* `countries` includes `country`, else the default region, else null.
|
|
8617
8646
|
*/
|
|
8618
8647
|
detectRegion(country: string, regions: Array<Region | PublicRegion>): Region | PublicRegion | null;
|
|
8648
|
+
/**
|
|
8649
|
+
* Server-side region resolution in one round trip (storefront seam). Pair
|
|
8650
|
+
* with the country your edge runtime extracts — Cloudflare `CF-IPCountry`,
|
|
8651
|
+
* Vercel `request.geo?.country`, Fastly `client-geo-country`, etc.
|
|
8652
|
+
*
|
|
8653
|
+
* Brainerce does NOT derive the country from the request IP server-side
|
|
8654
|
+
* because the storefront server is what reaches the backend (not the end-
|
|
8655
|
+
* customer). The storefront must extract + forward the country.
|
|
8656
|
+
*
|
|
8657
|
+
* Returns `{ region, matched, country }` — `matched=true` means the country
|
|
8658
|
+
* was explicitly listed by a region; `false` means we fell back to the
|
|
8659
|
+
* default region (or `region=null` if no default exists either).
|
|
8660
|
+
*/
|
|
8661
|
+
getAutoRegion(country?: string): Promise<AutoRegionResponse>;
|
|
8662
|
+
/**
|
|
8663
|
+
* Non-binding tax preview for PDP / PLP / cart. Pass the buyer's country
|
|
8664
|
+
* (from your edge runtime — same source as `getAutoRegion`) and the current
|
|
8665
|
+
* items subtotal in the store currency. The authoritative tax still runs
|
|
8666
|
+
* at checkout — render with an "Estimate" affordance.
|
|
8667
|
+
*
|
|
8668
|
+
* Returns `appliesTax=false` when tax is disabled, the country is missing,
|
|
8669
|
+
* or no active rate covers it.
|
|
8670
|
+
*/
|
|
8671
|
+
estimateTax(params: {
|
|
8672
|
+
country?: string;
|
|
8673
|
+
subtotal: number;
|
|
8674
|
+
}): Promise<TaxEstimateResponse>;
|
|
8619
8675
|
/**
|
|
8620
8676
|
* List the store's tax classes (public, no apiKey — storeId mode). Storefront-
|
|
8621
8677
|
* safe fields only (id/name/slug/description/isDefault) for transparency UIs
|
package/dist/index.d.ts
CHANGED
|
@@ -3807,6 +3807,35 @@ interface PublicRegionPaymentProvider {
|
|
|
3807
3807
|
interface PublicRegionDetail extends PublicRegion {
|
|
3808
3808
|
paymentProviders: PublicRegionPaymentProvider[];
|
|
3809
3809
|
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Server-side region resolution for a buyer country (storefront seam).
|
|
3812
|
+
* `matched=true` → the country was explicitly listed by a region. `false` →
|
|
3813
|
+
* fell back to the default region (or `region=null` when no default exists).
|
|
3814
|
+
*/
|
|
3815
|
+
interface AutoRegionResponse {
|
|
3816
|
+
region: PublicRegion | null;
|
|
3817
|
+
matched: boolean;
|
|
3818
|
+
/** Upper-cased ISO-3166-1 alpha-2. Empty string when caller omitted `country`. */
|
|
3819
|
+
country: string;
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* Non-binding tax preview for PDP / PLP / cart. The authoritative calculation
|
|
3823
|
+
* still runs at checkout against the full shipping address (state / postal /
|
|
3824
|
+
* per-class). Render with an "Estimate" affordance.
|
|
3825
|
+
*/
|
|
3826
|
+
interface TaxEstimateResponse {
|
|
3827
|
+
appliesTax: boolean;
|
|
3828
|
+
/** Percent — e.g. 18 for 18%. `null` when no matching rule. */
|
|
3829
|
+
rate: number | null;
|
|
3830
|
+
rateName: string | null;
|
|
3831
|
+
/** Tax portion of `subtotal` at the store's `pricesIncludeTax` mode. */
|
|
3832
|
+
estimatedTax: number;
|
|
3833
|
+
pricesIncludeTax: boolean;
|
|
3834
|
+
/** Store currency — the cart / order currency, unchanged by FX display overlay. */
|
|
3835
|
+
currency: string;
|
|
3836
|
+
/** Disclaimer copy — always present to surface the "preview, not binding" nature. */
|
|
3837
|
+
note: string;
|
|
3838
|
+
}
|
|
3810
3839
|
interface CreateRegionDto {
|
|
3811
3840
|
name: string;
|
|
3812
3841
|
/** ISO 4217 */
|
|
@@ -8616,6 +8645,33 @@ declare class BrainerceClient {
|
|
|
8616
8645
|
* `countries` includes `country`, else the default region, else null.
|
|
8617
8646
|
*/
|
|
8618
8647
|
detectRegion(country: string, regions: Array<Region | PublicRegion>): Region | PublicRegion | null;
|
|
8648
|
+
/**
|
|
8649
|
+
* Server-side region resolution in one round trip (storefront seam). Pair
|
|
8650
|
+
* with the country your edge runtime extracts — Cloudflare `CF-IPCountry`,
|
|
8651
|
+
* Vercel `request.geo?.country`, Fastly `client-geo-country`, etc.
|
|
8652
|
+
*
|
|
8653
|
+
* Brainerce does NOT derive the country from the request IP server-side
|
|
8654
|
+
* because the storefront server is what reaches the backend (not the end-
|
|
8655
|
+
* customer). The storefront must extract + forward the country.
|
|
8656
|
+
*
|
|
8657
|
+
* Returns `{ region, matched, country }` — `matched=true` means the country
|
|
8658
|
+
* was explicitly listed by a region; `false` means we fell back to the
|
|
8659
|
+
* default region (or `region=null` if no default exists either).
|
|
8660
|
+
*/
|
|
8661
|
+
getAutoRegion(country?: string): Promise<AutoRegionResponse>;
|
|
8662
|
+
/**
|
|
8663
|
+
* Non-binding tax preview for PDP / PLP / cart. Pass the buyer's country
|
|
8664
|
+
* (from your edge runtime — same source as `getAutoRegion`) and the current
|
|
8665
|
+
* items subtotal in the store currency. The authoritative tax still runs
|
|
8666
|
+
* at checkout — render with an "Estimate" affordance.
|
|
8667
|
+
*
|
|
8668
|
+
* Returns `appliesTax=false` when tax is disabled, the country is missing,
|
|
8669
|
+
* or no active rate covers it.
|
|
8670
|
+
*/
|
|
8671
|
+
estimateTax(params: {
|
|
8672
|
+
country?: string;
|
|
8673
|
+
subtotal: number;
|
|
8674
|
+
}): Promise<TaxEstimateResponse>;
|
|
8619
8675
|
/**
|
|
8620
8676
|
* List the store's tax classes (public, no apiKey — storeId mode). Storefront-
|
|
8621
8677
|
* safe fields only (id/name/slug/description/isDefault) for transparency UIs
|
package/dist/index.js
CHANGED
|
@@ -7054,6 +7054,44 @@ var BrainerceClient = class {
|
|
|
7054
7054
|
const code = country.toUpperCase();
|
|
7055
7055
|
return regions.find((r) => r.countries.includes(code)) ?? regions.find((r) => r.isDefault) ?? null;
|
|
7056
7056
|
}
|
|
7057
|
+
/**
|
|
7058
|
+
* Server-side region resolution in one round trip (storefront seam). Pair
|
|
7059
|
+
* with the country your edge runtime extracts — Cloudflare `CF-IPCountry`,
|
|
7060
|
+
* Vercel `request.geo?.country`, Fastly `client-geo-country`, etc.
|
|
7061
|
+
*
|
|
7062
|
+
* Brainerce does NOT derive the country from the request IP server-side
|
|
7063
|
+
* because the storefront server is what reaches the backend (not the end-
|
|
7064
|
+
* customer). The storefront must extract + forward the country.
|
|
7065
|
+
*
|
|
7066
|
+
* Returns `{ region, matched, country }` — `matched=true` means the country
|
|
7067
|
+
* was explicitly listed by a region; `false` means we fell back to the
|
|
7068
|
+
* default region (or `region=null` if no default exists either).
|
|
7069
|
+
*/
|
|
7070
|
+
async getAutoRegion(country) {
|
|
7071
|
+
return this.storefrontRequest(
|
|
7072
|
+
"GET",
|
|
7073
|
+
"/regions/auto",
|
|
7074
|
+
void 0,
|
|
7075
|
+
country ? { country } : void 0
|
|
7076
|
+
);
|
|
7077
|
+
}
|
|
7078
|
+
/**
|
|
7079
|
+
* Non-binding tax preview for PDP / PLP / cart. Pass the buyer's country
|
|
7080
|
+
* (from your edge runtime — same source as `getAutoRegion`) and the current
|
|
7081
|
+
* items subtotal in the store currency. The authoritative tax still runs
|
|
7082
|
+
* at checkout — render with an "Estimate" affordance.
|
|
7083
|
+
*
|
|
7084
|
+
* Returns `appliesTax=false` when tax is disabled, the country is missing,
|
|
7085
|
+
* or no active rate covers it.
|
|
7086
|
+
*/
|
|
7087
|
+
async estimateTax(params) {
|
|
7088
|
+
return this.storefrontRequest(
|
|
7089
|
+
"GET",
|
|
7090
|
+
"/tax/estimate",
|
|
7091
|
+
void 0,
|
|
7092
|
+
params.country ? { country: params.country, subtotal: params.subtotal } : { subtotal: params.subtotal }
|
|
7093
|
+
);
|
|
7094
|
+
}
|
|
7057
7095
|
// -------------------- Tax Classes (Storefront mode, public — no apiKey) --------------------
|
|
7058
7096
|
/**
|
|
7059
7097
|
* List the store's tax classes (public, no apiKey — storeId mode). Storefront-
|