brainerce 1.46.0 → 1.47.1
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 +150 -6
- package/dist/bot/bootstrap.global.js +6 -6
- package/dist/bot/index.d.mts +26 -0
- package/dist/bot/index.d.ts +26 -0
- package/dist/bot/index.js +32 -1
- package/dist/bot/index.mjs +32 -1
- package/dist/index.d.mts +357 -4
- package/dist/index.d.ts +357 -4
- package/dist/index.js +399 -6
- package/dist/index.mjs +390 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,8 @@ Every Brainerce storefront must include **all mandatory features** below. Featur
|
|
|
46
46
|
| Forgot / reset password | `client.forgotPassword()`, `client.resetPassword()` | ✅ |
|
|
47
47
|
| OAuth sign-in buttons + callback handler | `client.getAvailableOAuthProviders()` | ✅ |
|
|
48
48
|
| Account area (profile + order history) | `client.getMyProfile()`, `client.getMyOrders()` | ✅ |
|
|
49
|
-
| Loyalty & rewards (points balance + tiers + redeem) | `client.getLoyaltyStatus()`, `client.getAvailableRewards()`, `client.redeemLoyaltyReward(id)`, `client.reportSocialShare()` | conditional |
|
|
49
|
+
| Loyalty & rewards (points balance + tiers + redeem) | `client.getLoyaltyStatus()`, `client.getAvailableRewards()`, `client.getRecommendedReward()`, `client.redeemLoyaltyReward(id)`, `client.reportSocialShare()` | conditional |
|
|
50
|
+
| Loyalty paid membership (premium subscription) | `client.getMembershipPlans()`, `client.getMySavedPaymentMethods()`, `client.subscribeToMembership(params)`, `client.cancelMembership()` | conditional |
|
|
50
51
|
| Global header: cart count + search autocomplete | `client.getCart()`, `client.getSearchSuggestions(query)` | ✅ |
|
|
51
52
|
| Discount banners + product badges | `client.getDiscountBanners()`, `client.getProductDiscountBadge(productId)` | ✅ |
|
|
52
53
|
| Product reviews on PDP + JSON-LD aggregateRating | `client.listProductReviews(id)`, `client.submitProductReview(id, …)` | ✅ |
|
|
@@ -266,6 +267,12 @@ The SDK exports these utility functions for common UI tasks:
|
|
|
266
267
|
| `isCouponApplicableToProduct(coupon, product)` | Check if coupon applies | `isCouponApplicableToProduct(coupon, product)` |
|
|
267
268
|
| `isAllowedPaymentUrl(url, options?)` | Validate a payment URL host | `isAllowedPaymentUrl(intent.clientSecret)` → `true` |
|
|
268
269
|
| `safePaymentRedirect(url, options?)` | Validate then `window.location.href` | `safePaymentRedirect(intent.clientSecret)` |
|
|
270
|
+
| `buildProductJsonLd(product, opts)` | schema.org Product JSON-LD (PDPs only) | See SEO section |
|
|
271
|
+
| `buildArticleJsonLd(post, opts)` | schema.org Article JSON-LD for blog posts | See SEO section |
|
|
272
|
+
| `buildOrganizationJsonLd(store, opts)` | schema.org Organization for the homepage | See SEO section |
|
|
273
|
+
| `buildBreadcrumbJsonLd(items)` | schema.org BreadcrumbList | See SEO section |
|
|
274
|
+
| `jsonLdScriptProps(data)` | XSS-safe `<script type="application/ld+json">` props | `<script {...jsonLdScriptProps(data)} />` |
|
|
275
|
+
| `getBlogSitemapEntries(client, opts)` | Paginate published posts into sitemap entries | See SEO section |
|
|
269
276
|
|
|
270
277
|
```typescript
|
|
271
278
|
import {
|
|
@@ -451,7 +458,55 @@ const safeHtml = DOMPurify.sanitize(post.content);
|
|
|
451
458
|
return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;
|
|
452
459
|
```
|
|
453
460
|
|
|
454
|
-
**Scheduling**: A post is visible once `status === 'PUBLISHED'` and `publishedAt <= now()`. Set a future `publishedAt` when publishing to schedule
|
|
461
|
+
**Scheduling**: A post is visible once `status === 'PUBLISHED'` and `publishedAt <= now()`. Set a future `publishedAt` when publishing to schedule.
|
|
462
|
+
|
|
463
|
+
**SEO Autopilot writes here too**: the platform's SEO Autopilot publishes AI-written articles into this same blog automatically — render whatever `getPosts()` returns, and see the SEO section below for the required discoverability pieces.
|
|
464
|
+
|
|
465
|
+
### SEO — JSON-LD builders, sitemap helper, IndexNow key, llms.txt
|
|
466
|
+
|
|
467
|
+
The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on `reviewCount > 0`, AggregateOffer for VARIABLE products, XSS-safe serialization). Prefer them over hand-rolled JSON-LD:
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
import {
|
|
471
|
+
buildProductJsonLd, // single-product pages ONLY (never listing pages)
|
|
472
|
+
buildArticleJsonLd, // blog post pages
|
|
473
|
+
buildOrganizationJsonLd, // homepage
|
|
474
|
+
buildBreadcrumbJsonLd,
|
|
475
|
+
jsonLdScriptProps, // XSS-safe <script> props
|
|
476
|
+
} from 'brainerce';
|
|
477
|
+
|
|
478
|
+
// Blog article page
|
|
479
|
+
<script {...jsonLdScriptProps(buildArticleJsonLd(post, {
|
|
480
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
|
481
|
+
path: `/blog/${post.slug}`,
|
|
482
|
+
organizationName: storeInfo.name,
|
|
483
|
+
}))} />
|
|
484
|
+
|
|
485
|
+
// Product page
|
|
486
|
+
<script {...jsonLdScriptProps(buildProductJsonLd(product, {
|
|
487
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
|
488
|
+
path: `/products/${product.slug}`,
|
|
489
|
+
currency: storeInfo.currency,
|
|
490
|
+
}))} />
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**Blog entries in sitemap.xml** (required — autopilot articles missing from the sitemap never get crawled):
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
// app/sitemap.ts
|
|
497
|
+
import { getBlogSitemapEntries } from 'brainerce';
|
|
498
|
+
|
|
499
|
+
const blogPages = await getBlogSitemapEntries(client, {
|
|
500
|
+
siteUrl: baseUrl,
|
|
501
|
+
locales: supportedLocales, // optional (multi-locale stores)
|
|
502
|
+
defaultLocale,
|
|
503
|
+
}).catch(() => []);
|
|
504
|
+
return [...staticPages, ...productPages, ...blogPages];
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**IndexNow key file** (required): the platform pings IndexNow when posts publish; search engines verify by fetching `GET /indexnow-key.txt`. Serve `getStoreInfo().seo.indexNowKey` as `text/plain`, 404 while `null`. The key is **not a secret** (public by protocol design).
|
|
508
|
+
|
|
509
|
+
**llms.txt** (recommended): a plain-text site summary (store name, key pages, recent article links) for AI answer engines at `GET /llms.txt`.
|
|
455
510
|
|
|
456
511
|
---
|
|
457
512
|
|
|
@@ -892,10 +947,19 @@ const suggestions = await client.getAddressSuggestions('Rothschild 1', sessionTo
|
|
|
892
947
|
const { address, inZone } = await client.getAddressDetails(suggestions[0].placeId, sessionToken);
|
|
893
948
|
// address: { line1, city, region, postalCode, country, lat, lng, formattedAddress }
|
|
894
949
|
|
|
950
|
+
// address.region is Google's own administrative-area code — usually (not
|
|
951
|
+
// guaranteed) the same ISO 3166-2 subdivision code this store's own region
|
|
952
|
+
// lists use. Validate against destinations.regions before trusting it;
|
|
953
|
+
// never assign a code the region <select> wouldn't recognize.
|
|
954
|
+
const destinations = await client.getShippingDestinations();
|
|
955
|
+
const validRegions = destinations.regions[address.country] ?? [];
|
|
956
|
+
const region = validRegions.some((r) => r.code === address.region) ? address.region : '';
|
|
957
|
+
|
|
895
958
|
await client.setShippingAddress(checkout.id, {
|
|
896
959
|
firstName: 'John',
|
|
897
960
|
lastName: 'Doe',
|
|
898
961
|
...address,
|
|
962
|
+
region,
|
|
899
963
|
});
|
|
900
964
|
|
|
901
965
|
if (!inZone) {
|
|
@@ -1138,11 +1202,45 @@ const { categories } = await client.getCategories();
|
|
|
1138
1202
|
interface CategoryNode {
|
|
1139
1203
|
id: string;
|
|
1140
1204
|
name: string;
|
|
1205
|
+
slug?: string | null; // link a category page as `/category/${slug}`
|
|
1141
1206
|
parentId?: string | null;
|
|
1207
|
+
image?: string | null;
|
|
1142
1208
|
children: CategoryNode[]; // Recursive — can nest to any depth
|
|
1143
1209
|
}
|
|
1144
1210
|
```
|
|
1145
1211
|
|
|
1212
|
+
#### Get Category by Slug (Category Page)
|
|
1213
|
+
|
|
1214
|
+
Category (collection) pages are the highest-leverage organic-SEO surface — they rank for broad "research intent" queries that individual product pages never do. `getCategoryBySlug` returns the landing-page payload; fetch the products themselves with `getProducts({ categories: [category.id] })`.
|
|
1215
|
+
|
|
1216
|
+
```typescript
|
|
1217
|
+
// app/category/[slug]/page.tsx
|
|
1218
|
+
const category = await client.getCategoryBySlug(params.slug).catch(() => null);
|
|
1219
|
+
if (!category) notFound();
|
|
1220
|
+
|
|
1221
|
+
const { data: products } = await client.getProducts({ categories: [category.id] });
|
|
1222
|
+
|
|
1223
|
+
// SEO: CollectionPage + BreadcrumbList JSON-LD (never Product markup on a listing)
|
|
1224
|
+
const jsonLd = buildCollectionPageJsonLd(category, { siteUrl, path: `/category/${category.slug}` });
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
**Type:**
|
|
1228
|
+
|
|
1229
|
+
```typescript
|
|
1230
|
+
interface CategoryDetail {
|
|
1231
|
+
id: string;
|
|
1232
|
+
name: string;
|
|
1233
|
+
slug: string | null;
|
|
1234
|
+
description: string | null; // long-form HTML — render below the product grid
|
|
1235
|
+
metaDescription: string | null; // <meta name="description">
|
|
1236
|
+
image: string | null;
|
|
1237
|
+
breadcrumb: Array<{ name: string; slug: string | null }>; // root → parent
|
|
1238
|
+
productCount: number;
|
|
1239
|
+
}
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
> Vibe-coded mode only (like `getCategories`). Category slugs come from the dashboard SEO hub, which also writes the description + meta.
|
|
1243
|
+
|
|
1146
1244
|
#### Filter Products by Category
|
|
1147
1245
|
|
|
1148
1246
|
Pass a category ID to `getProducts()`. **The backend automatically includes all subcategories.** So filtering by a parent category returns products from that category AND all its descendants.
|
|
@@ -1416,7 +1514,9 @@ function SearchInput() {
|
|
|
1416
1514
|
</a>
|
|
1417
1515
|
))}
|
|
1418
1516
|
{suggestions.categories.map((category) => (
|
|
1419
|
-
|
|
1517
|
+
// Suggestions carry no slug — link to the filtered listing (by id),
|
|
1518
|
+
// not the SEO category page (which is `/category/${slug}`).
|
|
1519
|
+
<a key={category.id} href={`/products?category=${category.id}`}>
|
|
1420
1520
|
{category.name} ({category.productCount} products)
|
|
1421
1521
|
</a>
|
|
1422
1522
|
))}
|
|
@@ -2975,15 +3075,17 @@ customers check their points balance, enroll, and redeem rewards for one-time
|
|
|
2975
3075
|
coupons that apply at checkout.
|
|
2976
3076
|
|
|
2977
3077
|
```typescript
|
|
2978
|
-
// Current status — points balance + program display config + tier progress
|
|
2979
|
-
// `
|
|
2980
|
-
//
|
|
3078
|
+
// Current status — points balance + program display config + tier progress +
|
|
3079
|
+
// earned milestone `badges` + `paidMembership` (paid subscription, null for
|
|
3080
|
+
// free members). `program`/`tier`/`nextTier` are null if the store has no
|
|
3081
|
+
// loyalty program (or no tiers configured / the customer hasn't reached one yet).
|
|
2981
3082
|
const status = await client.getLoyaltyStatus();
|
|
2982
3083
|
if (status.enrolled) {
|
|
2983
3084
|
console.log(`${status.pointsBalance} ${status.program?.pointsName}`);
|
|
2984
3085
|
if (status.nextTier) {
|
|
2985
3086
|
console.log(`${status.pointsToNextTier} to reach ${status.nextTier.name}`);
|
|
2986
3087
|
}
|
|
3088
|
+
status.badges?.forEach((b) => console.log(`🏅 ${b.name}`));
|
|
2987
3089
|
}
|
|
2988
3090
|
|
|
2989
3091
|
// Enroll (idempotent) — only if the program is ACTIVE. May grant a one-time
|
|
@@ -2994,6 +3096,11 @@ await client.enrollInLoyalty();
|
|
|
2994
3096
|
// their current tier qualifies for).
|
|
2995
3097
|
const rewards = await client.getAvailableRewards();
|
|
2996
3098
|
|
|
3099
|
+
// AI "recommended for you" — always one of the catalog's real rewards (the AI
|
|
3100
|
+
// only ranks; it can never invent a discount). `{ reward: null }` when the
|
|
3101
|
+
// catalog is empty. Rate-limited to 5/min per customer.
|
|
3102
|
+
const { reward, reason } = await client.getRecommendedReward();
|
|
3103
|
+
|
|
2997
3104
|
// Redeem → spends points, returns a one-time coupon code to apply to the cart.
|
|
2998
3105
|
// `discountType` tells you how to interpret `discountValue` (currency amount
|
|
2999
3106
|
// for FIXED_DISCOUNT, 0-100 percent for PERCENT_DISCOUNT).
|
|
@@ -3038,6 +3145,43 @@ Birthday gifts need no SDK calls beyond profile data: set the customer's
|
|
|
3038
3145
|
platform emails a one-time gift coupon ahead of their birthday automatically
|
|
3039
3146
|
(when the store has it enabled).
|
|
3040
3147
|
|
|
3148
|
+
#### Paid Loyalty Membership
|
|
3149
|
+
|
|
3150
|
+
Stores can offer a paid "premium membership" inside the loyalty program — a
|
|
3151
|
+
recurring charge (default every 30 days) that grants a points multiplier and
|
|
3152
|
+
other perks. Storefront-mode only, requires `customerToken`. The customer must
|
|
3153
|
+
first have a saved card (vault one by checking out with `saveCard: true`).
|
|
3154
|
+
|
|
3155
|
+
```typescript
|
|
3156
|
+
// 1. Plans the store offers (empty if none / program not active).
|
|
3157
|
+
const plans = await client.getMembershipPlans();
|
|
3158
|
+
|
|
3159
|
+
// 2. The customer's saved cards — display fields only (brand / last4 / expiry).
|
|
3160
|
+
const methods = await client.getMySavedPaymentMethods();
|
|
3161
|
+
|
|
3162
|
+
// 3. Subscribe — charges the chosen card IMMEDIATELY and schedules the
|
|
3163
|
+
// recurring cycle. Declines throw a 409 with a `code` ('card_declined',
|
|
3164
|
+
// 'requires_action' when the bank demands 3D-Secure — not supported
|
|
3165
|
+
// off-session, ask the customer to use a different card).
|
|
3166
|
+
const membership = await client.subscribeToMembership({
|
|
3167
|
+
planId: plans[0].id,
|
|
3168
|
+
savedPaymentTokenId: methods.find((m) => m.isDefault)!.id,
|
|
3169
|
+
});
|
|
3170
|
+
// membership.status === 'ACTIVE'; points now earn at plan.pointsMultiplier ×
|
|
3171
|
+
// the tier multiplier.
|
|
3172
|
+
|
|
3173
|
+
// 4. Cancel — end-of-period: perks continue until `nextBillingAt`, then the
|
|
3174
|
+
// subscription ends with no further charge. Re-subscribing to the same plan
|
|
3175
|
+
// before period end un-cancels.
|
|
3176
|
+
const cancelled = await client.cancelMembership();
|
|
3177
|
+
// cancelled.cancelAtPeriodEnd === true
|
|
3178
|
+
|
|
3179
|
+
// `getLoyaltyStatus().paidMembership` reflects the subscription everywhere:
|
|
3180
|
+
// { status: 'ACTIVE' | 'PAST_DUE' | 'CANCELLED', nextBillingAt, plan, cancelAtPeriodEnd }
|
|
3181
|
+
// PAST_DUE = the last recurring charge failed; the platform retries daily and
|
|
3182
|
+
// pauses the multiplier until the charge clears.
|
|
3183
|
+
```
|
|
3184
|
+
|
|
3041
3185
|
#### Auth Response Type
|
|
3042
3186
|
|
|
3043
3187
|
```typescript
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";(()=>{var U="https://api.brainerce.com",z=new Set(["he","ar"]),L={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",poweredBy:"Powered by Brainerce",addFailed:"I couldn\u2019t add that to the cart \u2014 try the button on the product card."},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",poweredBy:"\u05DE\u05D5\u05E4\u05E2\u05DC \u05E2\u05DC \u05D9\u05D3\u05D9 Brainerce",addFailed:"\u05DC\u05D0 \u05D4\u05E6\u05DC\u05D7\u05EA\u05D9 \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05DC\u05E1\u05DC \u2014 \u05E0\u05E1\u05D5 \u05D3\u05E8\u05DA \u05D4\u05DB\u05E4\u05EA\u05D5\u05E8 \u05D1\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D4\u05DE\u05D5\u05E6\u05E8."}},H={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>',sparkle:'<svg viewBox="0 0 24 24" fill="none"><path d="M12 3v2M12 19v2M3 12h2M19 12h2M6.34 6.34l1.42 1.42M16.24 16.24l1.42 1.42M6.34 17.66l1.42-1.42M16.24 7.76l1.42-1.42" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="3.5" fill="currentColor"/></svg>'},j="data:image/png;base64,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",P=typeof DOMParser<"u"?new DOMParser:null;function m(p){let e=(H[p]??H.chat).replace("<svg ",'<svg xmlns="http://www.w3.org/2000/svg" '),t=P?.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 F(p){let e=new Uint8Array(16);crypto.getRandomValues(e);let n=btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return`${p}${n}`}function B(p){return/^\/(?!\/)/.test(p)||/^https?:\/\//i.test(p)}var R=/\*\*([^*\n]+)\*\*|\[([^\]\n]+)\]\(([^)\s]+)\)/g;function D(p,e){let n=0;R.lastIndex=0;for(let t=R.exec(e);t;t=R.exec(e)){if(t.index>n&&p.appendChild(document.createTextNode(e.slice(n,t.index))),t[1]!==void 0){let i=document.createElement("strong");i.textContent=t[1],p.appendChild(i)}else if(B(t[3])){let i=document.createElement("a");i.href=t[3],i.target="_blank",i.rel="noopener noreferrer",i.textContent=t[2],p.appendChild(i)}else p.appendChild(document.createTextNode(t[2]));n=t.index+t[0].length}n<e.length&&p.appendChild(document.createTextNode(e.slice(n)))}function O(p,e){p.replaceChildren();let n=e.split(`
|
|
2
|
-
`),t=null,
|
|
1
|
+
"use strict";(()=>{var D="https://api.brainerce.com",U=new Set(["he","ar"]),L={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",poweredBy:"Powered by Brainerce",addFailed:"I couldn\u2019t add that to the cart \u2014 try the button on the product card."},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",poweredBy:"\u05DE\u05D5\u05E4\u05E2\u05DC \u05E2\u05DC \u05D9\u05D3\u05D9 Brainerce",addFailed:"\u05DC\u05D0 \u05D4\u05E6\u05DC\u05D7\u05EA\u05D9 \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05DC\u05E1\u05DC \u2014 \u05E0\u05E1\u05D5 \u05D3\u05E8\u05DA \u05D4\u05DB\u05E4\u05EA\u05D5\u05E8 \u05D1\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D4\u05DE\u05D5\u05E6\u05E8."}},H={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>',sparkle:'<svg viewBox="0 0 24 24" fill="none"><path d="M12 3v2M12 19v2M3 12h2M19 12h2M6.34 6.34l1.42 1.42M16.24 16.24l1.42 1.42M6.34 17.66l1.42-1.42M16.24 7.76l1.42-1.42" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="12" r="3.5" fill="currentColor"/></svg>'},j="data:image/png;base64,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",z=typeof DOMParser<"u"?new DOMParser:null;function m(p){let e=(H[p]??H.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 F(p){let e=new Uint8Array(16);crypto.getRandomValues(e);let n=btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return`${p}${n}`}function T(p){return/^\/(?!\/)/.test(p)||/^https?:\/\//i.test(p)}var R=/\*\*([^*\n]+)\*\*|\[([^\]\n]+)\]\(([^)\s]+)\)/g;function P(p,e){let n=0;R.lastIndex=0;for(let t=R.exec(e);t;t=R.exec(e)){if(t.index>n&&p.appendChild(document.createTextNode(e.slice(n,t.index))),t[1]!==void 0){let s=document.createElement("strong");s.textContent=t[1],p.appendChild(s)}else if(T(t[3])){let s=document.createElement("a");s.href=t[3],s.target="_blank",s.rel="noopener noreferrer",s.textContent=t[2],p.appendChild(s)}else p.appendChild(document.createTextNode(t[2]));n=t.index+t[0].length}n<e.length&&p.appendChild(document.createTextNode(e.slice(n)))}function O(p,e){p.replaceChildren();let n=e.split(`
|
|
2
|
+
`),t=null,s=null;for(let o of n){let r=/^\s*[-•*]\s+(.*)$/.exec(o);if(r){s=null,t||(t=document.createElement("ul"),p.appendChild(t));let d=document.createElement("li");P(d,r[1]),t.appendChild(d);continue}if(t=null,!o.trim()){s=null;continue}s?s.appendChild(document.createElement("br")):(s=document.createElement("p"),p.appendChild(s)),P(s,o)}}var A=class p{constructor(e){this.botSessionToken=null;this.botSessionFetchedAt=0;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.prevBodyOverflow=null;this.connectionId=e.connectionId,this.baseUrl=(e.baseUrl||D).replace(/\/$/,""),this.storageKey=`brainerce-bot:${this.connectionId}`,this.onAddToCart=e.onAddToCart,this.customerSessionProxyPath=e.customerSessionProxyPath?.replace(/\/$/,"")}static async mount(e){if(!e?.connectionId)return console.warn("[BrainerceBot] connectionId is required"),null;let n=new p(e);return await n.boot(e.target??document.body)?n:null}destroy(){this.destroyed=!0,this.prevBodyOverflow!==null&&(document.body.style.overflow=this.prevBodyOverflow,this.prevBodyOverflow=null),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(L[this.locale]??L.en)[e]??L.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 s=this.settings.bubbleShape==="square",o=s?"14px":"22px",r=s?"8px":"15px",d="5px",a=this.settings.displayMode??"floating",l=a==="side_rail",i=a==="full_screen";return`
|
|
3
3
|
:host { all: initial; }
|
|
4
4
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0;
|
|
5
5
|
font-family: -apple-system, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI", system-ui, "Helvetica Neue", sans-serif;
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
.bb-launcher {
|
|
34
34
|
width: 58px; height: 58px; position: relative; display: flex; align-items: center;
|
|
35
35
|
justify-content: center; color: #fff; background: ${e};
|
|
36
|
-
border-radius: ${
|
|
36
|
+
border-radius: ${s?"16px":"999px"}; overflow: hidden;
|
|
37
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
38
|
transition: transform .22s cubic-bezier(.34,1.56,.64,1), box-shadow .22s ease;
|
|
39
39
|
}
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
width: auto; max-width: none; height: auto; }
|
|
69
69
|
${l?`.bb-window { bottom: 0; top: auto; ${t}: 0; height: calc(100vh - 98px); border-end-start-radius: ${o}; }
|
|
70
70
|
.bb.expanded .bb-window { width: min(560px, calc(100vw - 40px)); }`:""}
|
|
71
|
-
${
|
|
71
|
+
${i?`.bb.open .bb-window { position: fixed;
|
|
72
72
|
inset: min(6vh, 60px) max(24px, calc((100vw - 1680px) / 2));
|
|
73
73
|
width: auto; max-width: none; height: auto; }
|
|
74
74
|
.bb.open .bb-launcher { opacity: 0; pointer-events: none; }`:""}
|
|
@@ -245,6 +245,6 @@
|
|
|
245
245
|
transition: color .12s ease; }
|
|
246
246
|
.bb-foot a:hover { color: #6b7280; }
|
|
247
247
|
.bb-foot-mark { width: 14px; height: 14px; border-radius: 3px; display: block; }
|
|
248
|
-
`}render(e){let n=this.settings.accentColor||"#6366F1",t=
|
|
248
|
+
`}render(e){let n=this.settings.accentColor||"#6366F1",t=U.has(this.locale)?"rtl":"ltr",s=this.settings.position==="start"?"left":"right",o=t==="rtl"?s==="left"?"right":"left":s,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 i=document.createElement("div");i.className="bb-header";let b=document.createElement("span");if(b.className="bb-avatar",this.settings.avatarUrl&&T(this.settings.avatarUrl)){let g=document.createElement("img");g.src=this.settings.avatarUrl,g.alt="",b.appendChild(g)}else b.appendChild(m("sparkle"));let u=document.createElement("span");u.className="bb-head-main";let h=document.createElement("span");h.className="bb-name",h.textContent=r;let w=document.createElement("span");w.className="bb-status",w.textContent=this.t("online"),u.appendChild(h),u.appendChild(w);let v=document.createElement("span");v.className="bb-actions",(this.settings.displayMode??"floating")!=="full_screen"&&this.settings.allowExpand!==!1&&(this.expandBtn=this.iconButton("expand",this.t("expand"),()=>this.toggleExpand()),v.appendChild(this.expandBtn)),v.appendChild(this.iconButton("mail",this.t("leaveMessage"),()=>this.toggleEscalation())),v.appendChild(this.iconButton("close",this.t("close"),()=>this.close())),i.appendChild(b),i.appendChild(u),i.appendChild(v),this.windowEl.appendChild(i),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 g of this.settings.starterQuestions??[]){let M=document.createElement("button");M.className="bb-chip",M.textContent=g,M.addEventListener("click",()=>this.send(g)),this.chipsEl.appendChild(M)}this.windowEl.appendChild(this.chipsEl);let c=document.createElement("div");c.className="bb-esc";let k=document.createElement("span");k.className="bb-esc-title",k.textContent=this.t("leaveMessage");let E=document.createElement("input");E.type="email",E.name="email",E.placeholder=this.t("yourEmail");let C=document.createElement("textarea");C.name="message",C.rows=2,C.placeholder=this.t("yourMessage");let I=document.createElement("button");I.type="button",I.className="bb-esc-send",I.textContent=this.t("send"),I.addEventListener("click",()=>this.submitEscalation(c)),c.appendChild(k),c.appendChild(E),c.appendChild(C),c.appendChild(I),this.windowEl.appendChild(c);let f=document.createElement("div");f.className="bb-composer",this.inputEl=document.createElement("textarea"),this.inputEl.className="bb-input",this.inputEl.rows=1,this.inputEl.maxLength=4e3,this.inputEl.placeholder=this.t("placeholder"),this.inputEl.addEventListener("keydown",g=>{g.key==="Enter"&&!g.shiftKey&&(g.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(m("send")),this.sendBtn.addEventListener("click",()=>this.send(this.inputEl?.value??"")),f.appendChild(this.inputEl),f.appendChild(this.sendBtn),this.windowEl.appendChild(f);let B=document.createElement("div");B.className="bb-foot";let x=document.createElement("a");x.href="https://brainerce.com",x.target="_blank",x.rel="noopener noreferrer";let N=document.createElement("img");if(N.src=j,N.alt="",N.className="bb-foot-mark",x.appendChild(N),x.appendChild(document.createTextNode(this.t("poweredBy"))),B.appendChild(x),this.windowEl.appendChild(B),this.launcherEl=document.createElement("button"),this.launcherEl.className="bb-launcher",this.launcherEl.setAttribute("aria-label",r),this.settings.avatarUrl&&T(this.settings.avatarUrl)){let g=document.createElement("img");g.src=this.settings.avatarUrl,g.alt="",this.launcherEl.appendChild(g)}else{let g=m("chat");g.classList.add("bb-l-chat"),this.launcherEl.appendChild(g)}let S=m("close");S.classList.add("bb-l-close"),this.launcherEl.appendChild(S),this.launcherEl.addEventListener("click",()=>this.opened?this.close():this.open()),a.appendChild(this.launcherEl),e.appendChild(this.host)}iconButton(e,n,t){let s=document.createElement("button");return s.className="bb-iconbtn",s.title=n,s.setAttribute("aria-label",n),s.appendChild(m(e)),s.addEventListener("click",t),s}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.syncBodyScroll(),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"),this.syncBodyScroll()}syncBodyScroll(){let e=this.opened&&!!this.root?.querySelector(".bb")?.classList.contains("big");e&&this.prevBodyOverflow===null?(this.prevBodyOverflow=document.body.style.overflow||"",document.body.style.overflow="hidden"):!e&&this.prevBodyOverflow!==null&&(document.body.style.overflow=this.prevBodyOverflow,this.prevBodyOverflow=null)}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.syncBodyScroll(),this.expandBtn){this.expandBtn.replaceChildren(m(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 s=this.appendMessage(t.role==="assistant"?"bot":"user","");O(s,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","");O(e,this.settings.greeting)}}async refreshCustomerSession(){if(!this.customerSessionProxyPath)return;let e=240*1e3;if(!(this.botSessionToken&&Date.now()-this.botSessionFetchedAt<e))try{let n=await fetch(`${this.customerSessionProxyPath}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/customer-session`,{credentials:"include"});if(!n.ok)return;let t=await n.json();this.botSessionToken=t.loggedIn&&t.botSessionToken?t.botSessionToken:null,this.botSessionFetchedAt=Date.now()}catch{}}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 s=null;try{await this.refreshCustomerSession();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:F("trn_"),...this.conversationId?{conversationId:this.conversationId}:{},...this.sessionId?{anonymousSessionId:this.sessionId}:{},locale:this.locale,...this.botSessionToken?{botSessionToken:this.botSessionToken}:{}})});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:i}=await r.read();if(i)break;a+=d.decode(l,{stream:!0});let b;for(;(b=a.indexOf(`
|
|
249
249
|
|
|
250
|
-
`))>=0;){let u=a.slice(0,b);if(a=a.slice(b+2),!u.startsWith("data: "))continue;let h;try{h=JSON.parse(u.slice(6))}catch{continue}
|
|
250
|
+
`))>=0;){let u=a.slice(0,b);if(a=a.slice(b+2),!u.startsWith("data: "))continue;let h;try{h=JSON.parse(u.slice(6))}catch{continue}s=this.handleFrame(h,t,s)}}s&&this.pendingText&&O(s,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"action":return this.handleAction(e),t;case"error":return this.appendMessage("err",e.message||this.t("error")),t;case"escalate_offer":return this.openEscalation(),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 i=document.createElement("div");i.className="bb-shelf";let b=document.createElement("div");b.className="bb-shelf-cap",b.textContent=this.t("results"),this.cardsRow=document.createElement("div"),this.cardsRow.className="bb-cards",i.appendChild(b),i.appendChild(this.cardsRow),this.messagesEl.appendChild(i)}let n=T(e.url)?e.url:null,t=document.createElement("div");t.className="bb-card";let s=()=>{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 i=document.createElement("img");i.src=e.imageUrl,i.alt="",i.loading="lazy",o.appendChild(i)}else o.appendChild(m("cart"));o.addEventListener("click",s),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",s);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 i=document.createElement("button");i.className="bb-btn bb-btn-add",i.setAttribute("aria-label",this.t("addToCart")),i.appendChild(m("cart")),e.variants?.length?i.addEventListener("click",()=>this.togglePicker(r,e,o)):i.addEventListener("click",s),l.appendChild(i)}else{let i=document.createElement("button");i.className="bb-btn bb-btn-add bb-sq",i.setAttribute("aria-label",this.t("addToCart")),i.appendChild(m("cart")),i.addEventListener("click",()=>void this.addToCart(e,i));let b=document.createElement("button");b.className="bb-btn bb-btn-ghost bb-wide",b.appendChild(document.createTextNode(this.t("view"))),b.addEventListener("click",s),l.appendChild(b),l.appendChild(i)}r.appendChild(l),t.appendChild(r),this.cardsRow.appendChild(t),this.scrollDown()}togglePicker(e,n,t){let s=e.parentElement,o=e.querySelector(".bb-pick");if(o){o.remove(),s?.classList.remove("picking");return}s?.classList.add("picking");let r=n.variants??[],d=[];for(let u of r)for(let h of Object.keys(u.attributes))d.includes(h)||d.push(h);let a={},l=document.createElement("span");l.className="bb-pick";let i=()=>r.find(u=>d.every(h=>a[h]&&u.attributes[h]===a[h]))??null,b=()=>{l.replaceChildren();for(let c of d){let k=document.createElement("span");k.className="bb-pick-key",k.textContent=c;let E=document.createElement("span");E.className="bb-pick-vals";let C=new Set;for(let I of r){let f=I.attributes[c];if(!f||C.has(f))continue;C.add(f);let B=r.some(N=>N.attributes[c]===f&&d.every(S=>S===c||!a[S]||N.attributes[S]===a[S])),x=document.createElement("button");x.className=`bb-chipv${a[c]===f?" sel":""}${B?"":" off"}`,x.textContent=f,x.addEventListener("click",()=>{a[c]===f?delete a[c]:a[c]=f,b()}),E.appendChild(x)}l.appendChild(k),l.appendChild(E)}let u=document.createElement("button");u.className="bb-pick-close",u.setAttribute("aria-label",this.t("close")),u.appendChild(m("close")),u.addEventListener("click",()=>{l.remove(),s?.classList.remove("picking")}),l.appendChild(u);let h=i(),w=document.createElement("span");w.className="bb-pick-foot";let v=document.createElement("span");v.className="bb-pick-price",v.textContent=h?h.price.formatted:"";let y=document.createElement("button");if(y.className="bb-btn bb-btn-add bb-sq",y.setAttribute("aria-label",this.t("addToCart")),y.appendChild(m("cart")),h||(y.disabled=!0),y.addEventListener("click",()=>{let c=i();c&&this.addToCart(n,y,c.id)}),w.appendChild(v),w.appendChild(y),l.appendChild(w),h?.imageUrl&&T(h.imageUrl)){let c=t.querySelector("img");c&&(c.src=h.imageUrl)}this.scrollDown()};b(),e.appendChild(l),this.scrollDown()}async addToCart(e,n,t){this.beacon(e.botRef),n.dataset.state="busy",await this.dispatchAdd(e.productId,t??null)?(n.dataset.state="done",n.replaceChildren(m("check"),document.createTextNode(this.t("added"))),setTimeout(()=>{!this.destroyed&&n.isConnected&&(delete n.dataset.state,n.replaceChildren(m("cart"),document.createTextNode(this.t("addToCart"))))},2200)):(delete n.dataset.state,T(e.url)&&(window.location.href=e.url))}async dispatchAdd(e,n){try{if(this.onAddToCart)return await this.onAddToCart({productId:e,variantId:n,quantity:1})!==!1;let t=new CustomEvent("brainerce:bot:add-to-cart",{detail:{productId:e,variantId:n,quantity:1,connectionId:this.connectionId},cancelable:!0,bubbles:!0,composed:!0});return!window.dispatchEvent(t)}catch{return!1}}async handleAction(e){if(e.action!=="add_to_cart")return;e.botRef&&this.beacon(e.botRef),await this.dispatchAdd(e.productId,e.variantId)||this.appendMessage("err",this.t("addFailed"))}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")}openEscalation(){this.root?.querySelector(".bb-esc")?.classList.add("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(m("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 s=0;s<3;s++)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 p=document.currentScript,e=p?.dataset.connectionId;if(!e){console.warn("[BrainerceBot] missing data-connection-id on the bot.js script tag");return}let n=p?.dataset.apiBase||void 0,t=()=>void A.mount({connectionId:e,baseUrl:n});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()})();})();
|
package/dist/bot/index.d.mts
CHANGED
|
@@ -33,12 +33,29 @@ interface BrainerceBotOptions {
|
|
|
33
33
|
variantId?: string | null;
|
|
34
34
|
quantity: number;
|
|
35
35
|
}) => boolean | Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Part 2 (order-lookup login gating) — a SAME-ORIGIN path (e.g. `/api/store`, the
|
|
38
|
+
* Next.js template's own BFF proxy) that forwards to the backend and attaches the
|
|
39
|
+
* shopper's customer-session cookie as a Bearer header. When set, the widget calls
|
|
40
|
+
* `${customerSessionProxyPath}/api/storefront-bot/:connectionId/customer-session`
|
|
41
|
+
* (through THIS path, not `baseUrl` — a cross-origin request to `baseUrl` would
|
|
42
|
+
* never carry the httpOnly session cookie) to learn login state and obtain a
|
|
43
|
+
* short-lived token it attaches to its normal direct-to-backend /chat call, so
|
|
44
|
+
* streaming is completely unaffected. Omit entirely on any site without such a
|
|
45
|
+
* proxy — checkOrderStatus then simply always answers as a guest, no error.
|
|
46
|
+
*/
|
|
47
|
+
customerSessionProxyPath?: string;
|
|
36
48
|
}
|
|
37
49
|
declare class BrainerceBot {
|
|
38
50
|
private readonly connectionId;
|
|
39
51
|
private readonly baseUrl;
|
|
40
52
|
private readonly storageKey;
|
|
41
53
|
private readonly onAddToCart?;
|
|
54
|
+
private readonly customerSessionProxyPath?;
|
|
55
|
+
/** Part 2 (order-lookup login gating) — short-lived, in-memory only (never
|
|
56
|
+
* persisted): refreshed opportunistically before a chat send when stale. */
|
|
57
|
+
private botSessionToken;
|
|
58
|
+
private botSessionFetchedAt;
|
|
42
59
|
private host?;
|
|
43
60
|
private root?;
|
|
44
61
|
private windowEl?;
|
|
@@ -83,6 +100,15 @@ declare class BrainerceBot {
|
|
|
83
100
|
private toggleExpand;
|
|
84
101
|
/** First open: restore the server thread, or show the greeting. */
|
|
85
102
|
private primeThread;
|
|
103
|
+
/**
|
|
104
|
+
* Part 2 (order-lookup login gating) — refresh the short-lived bot-session token
|
|
105
|
+
* if this site opted in (`customerSessionProxyPath` set) and the current one is
|
|
106
|
+
* stale or absent. Comfortably under the backend's 5-minute expiry so a token
|
|
107
|
+
* fetched here is never expired by the time /chat reads it. Fails silently on any
|
|
108
|
+
* error (offline, no proxy configured server-side, etc.) — a guest must always be
|
|
109
|
+
* able to chat regardless of this call's outcome.
|
|
110
|
+
*/
|
|
111
|
+
private refreshCustomerSession;
|
|
86
112
|
private send;
|
|
87
113
|
private handleFrame;
|
|
88
114
|
private appendCard;
|
package/dist/bot/index.d.ts
CHANGED
|
@@ -33,12 +33,29 @@ interface BrainerceBotOptions {
|
|
|
33
33
|
variantId?: string | null;
|
|
34
34
|
quantity: number;
|
|
35
35
|
}) => boolean | Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Part 2 (order-lookup login gating) — a SAME-ORIGIN path (e.g. `/api/store`, the
|
|
38
|
+
* Next.js template's own BFF proxy) that forwards to the backend and attaches the
|
|
39
|
+
* shopper's customer-session cookie as a Bearer header. When set, the widget calls
|
|
40
|
+
* `${customerSessionProxyPath}/api/storefront-bot/:connectionId/customer-session`
|
|
41
|
+
* (through THIS path, not `baseUrl` — a cross-origin request to `baseUrl` would
|
|
42
|
+
* never carry the httpOnly session cookie) to learn login state and obtain a
|
|
43
|
+
* short-lived token it attaches to its normal direct-to-backend /chat call, so
|
|
44
|
+
* streaming is completely unaffected. Omit entirely on any site without such a
|
|
45
|
+
* proxy — checkOrderStatus then simply always answers as a guest, no error.
|
|
46
|
+
*/
|
|
47
|
+
customerSessionProxyPath?: string;
|
|
36
48
|
}
|
|
37
49
|
declare class BrainerceBot {
|
|
38
50
|
private readonly connectionId;
|
|
39
51
|
private readonly baseUrl;
|
|
40
52
|
private readonly storageKey;
|
|
41
53
|
private readonly onAddToCart?;
|
|
54
|
+
private readonly customerSessionProxyPath?;
|
|
55
|
+
/** Part 2 (order-lookup login gating) — short-lived, in-memory only (never
|
|
56
|
+
* persisted): refreshed opportunistically before a chat send when stale. */
|
|
57
|
+
private botSessionToken;
|
|
58
|
+
private botSessionFetchedAt;
|
|
42
59
|
private host?;
|
|
43
60
|
private root?;
|
|
44
61
|
private windowEl?;
|
|
@@ -83,6 +100,15 @@ declare class BrainerceBot {
|
|
|
83
100
|
private toggleExpand;
|
|
84
101
|
/** First open: restore the server thread, or show the greeting. */
|
|
85
102
|
private primeThread;
|
|
103
|
+
/**
|
|
104
|
+
* Part 2 (order-lookup login gating) — refresh the short-lived bot-session token
|
|
105
|
+
* if this site opted in (`customerSessionProxyPath` set) and the current one is
|
|
106
|
+
* stale or absent. Comfortably under the backend's 5-minute expiry so a token
|
|
107
|
+
* fetched here is never expired by the time /chat reads it. Fails silently on any
|
|
108
|
+
* error (offline, no proxy configured server-side, etc.) — a guest must always be
|
|
109
|
+
* able to chat regardless of this call's outcome.
|
|
110
|
+
*/
|
|
111
|
+
private refreshCustomerSession;
|
|
86
112
|
private send;
|
|
87
113
|
private handleFrame;
|
|
88
114
|
private appendCard;
|
package/dist/bot/index.js
CHANGED
|
@@ -164,6 +164,10 @@ function renderRich(el, text) {
|
|
|
164
164
|
}
|
|
165
165
|
var BrainerceBot = class _BrainerceBot {
|
|
166
166
|
constructor(options) {
|
|
167
|
+
/** Part 2 (order-lookup login gating) — short-lived, in-memory only (never
|
|
168
|
+
* persisted): refreshed opportunistically before a chat send when stale. */
|
|
169
|
+
this.botSessionToken = null;
|
|
170
|
+
this.botSessionFetchedAt = 0;
|
|
167
171
|
this.settings = { enabled: false };
|
|
168
172
|
this.locale = "en";
|
|
169
173
|
this.sessionId = null;
|
|
@@ -185,6 +189,7 @@ var BrainerceBot = class _BrainerceBot {
|
|
|
185
189
|
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
186
190
|
this.storageKey = `brainerce-bot:${this.connectionId}`;
|
|
187
191
|
this.onAddToCart = options.onAddToCart;
|
|
192
|
+
this.customerSessionProxyPath = options.customerSessionProxyPath?.replace(/\/$/, "");
|
|
188
193
|
}
|
|
189
194
|
/** Boot the widget. Resolves to null when the bot is disabled server-side. */
|
|
190
195
|
static async mount(options) {
|
|
@@ -749,6 +754,30 @@ var BrainerceBot = class _BrainerceBot {
|
|
|
749
754
|
// --------------------------------------------------------------------------
|
|
750
755
|
// The chat turn
|
|
751
756
|
// --------------------------------------------------------------------------
|
|
757
|
+
/**
|
|
758
|
+
* Part 2 (order-lookup login gating) — refresh the short-lived bot-session token
|
|
759
|
+
* if this site opted in (`customerSessionProxyPath` set) and the current one is
|
|
760
|
+
* stale or absent. Comfortably under the backend's 5-minute expiry so a token
|
|
761
|
+
* fetched here is never expired by the time /chat reads it. Fails silently on any
|
|
762
|
+
* error (offline, no proxy configured server-side, etc.) — a guest must always be
|
|
763
|
+
* able to chat regardless of this call's outcome.
|
|
764
|
+
*/
|
|
765
|
+
async refreshCustomerSession() {
|
|
766
|
+
if (!this.customerSessionProxyPath) return;
|
|
767
|
+
const FOUR_MINUTES_MS = 4 * 60 * 1e3;
|
|
768
|
+
if (this.botSessionToken && Date.now() - this.botSessionFetchedAt < FOUR_MINUTES_MS) return;
|
|
769
|
+
try {
|
|
770
|
+
const res = await fetch(
|
|
771
|
+
`${this.customerSessionProxyPath}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/customer-session`,
|
|
772
|
+
{ credentials: "include" }
|
|
773
|
+
);
|
|
774
|
+
if (!res.ok) return;
|
|
775
|
+
const data = await res.json();
|
|
776
|
+
this.botSessionToken = data.loggedIn && data.botSessionToken ? data.botSessionToken : null;
|
|
777
|
+
this.botSessionFetchedAt = Date.now();
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
}
|
|
752
781
|
async send(text) {
|
|
753
782
|
const message = text.trim();
|
|
754
783
|
if (!message || this.busy) return;
|
|
@@ -763,6 +792,7 @@ var BrainerceBot = class _BrainerceBot {
|
|
|
763
792
|
this.cardIds = /* @__PURE__ */ new Set();
|
|
764
793
|
let botBubble = null;
|
|
765
794
|
try {
|
|
795
|
+
await this.refreshCustomerSession();
|
|
766
796
|
const res = await fetch(
|
|
767
797
|
`${this.baseUrl}/api/storefront-bot/${encodeURIComponent(this.connectionId)}/chat`,
|
|
768
798
|
{
|
|
@@ -773,7 +803,8 @@ var BrainerceBot = class _BrainerceBot {
|
|
|
773
803
|
turnId: randomId("trn_"),
|
|
774
804
|
...this.conversationId ? { conversationId: this.conversationId } : {},
|
|
775
805
|
...this.sessionId ? { anonymousSessionId: this.sessionId } : {},
|
|
776
|
-
locale: this.locale
|
|
806
|
+
locale: this.locale,
|
|
807
|
+
...this.botSessionToken ? { botSessionToken: this.botSessionToken } : {}
|
|
777
808
|
})
|
|
778
809
|
}
|
|
779
810
|
);
|