@blotoutio/providers-evo-search-sdk 1.70.0 → 1.70.2
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 +128 -0
- package/core.cjs.js +444 -14
- package/core.js +444 -14
- package/core.mjs +444 -14
- package/hooks.cjs.js +2901 -12
- package/hooks.d.ts +583 -1
- package/hooks.mjs +2898 -15
- package/index.cjs.js +8581 -215
- package/index.js +8581 -215
- package/index.mjs +8581 -215
- package/package.json +1 -1
package/core.cjs.js
CHANGED
|
@@ -1,5 +1,269 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Per-tenant filter vocabulary discovered from a catalog's structured product
|
|
4
|
+
// attributes (see buildProductAttributes in catalog.ts). The slot-fill LLM is
|
|
5
|
+
// constrained to this vocabulary and its output is validated against it, so
|
|
6
|
+
// filters reflect what the brand actually has and can't be hallucinated. Built
|
|
7
|
+
// at sync time, persisted to KV, read at query time. Deterministic — per-brand
|
|
8
|
+
// relevance emerges because the vocabulary is derived from that tenant's data.
|
|
9
|
+
// What a store that has never opened the Facets page gets. Availability is the
|
|
10
|
+
// only one: it reads a structural field every catalog has, so it needs no sync
|
|
11
|
+
// work and no merchant mapping, and it is the filter shoppers reach for most.
|
|
12
|
+
// Everything else stays opt-in — inventing Brand/Category groups for a catalog
|
|
13
|
+
// we haven't been told about is what the discovered-facet design got wrong.
|
|
14
|
+
//
|
|
15
|
+
// This is a fallback for an ABSENT config, never an injection into a present
|
|
16
|
+
// one: a merchant who saves a list without Availability has removed it, and a
|
|
17
|
+
// merchant who clears every facet gets `{ facets: [] }` and no groups at all.
|
|
18
|
+
// `resolveFacetsConfig` is the single place that distinction is made.
|
|
19
|
+
const DEFAULT_FACETS_CONFIG = {
|
|
20
|
+
facets: [
|
|
21
|
+
{
|
|
22
|
+
id: 'availability',
|
|
23
|
+
label: 'Availability',
|
|
24
|
+
source: { kind: 'availability' },
|
|
25
|
+
display: 'list',
|
|
26
|
+
enabled: true,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
/** Compatibility order consumed by renderer versions that predate embedded facets. */
|
|
31
|
+
const deriveFacetIds = (config) => [
|
|
32
|
+
'price',
|
|
33
|
+
...config.facets
|
|
34
|
+
.filter((facet) => facet.enabled && facet.id !== 'price')
|
|
35
|
+
.map((facet) => facet.id),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const SEARCH_PAGE_DESIGN_VERSION = 1;
|
|
39
|
+
/**
|
|
40
|
+
* Sentinel accepted by color tokens, `controlRadius` and `containerWidth`:
|
|
41
|
+
* resolve the value from the merchant's live theme at mount (storefront) or
|
|
42
|
+
* fall back to the preset value below (editor preview, headless, resolution
|
|
43
|
+
* failure).
|
|
44
|
+
*/
|
|
45
|
+
const AUTO_DESIGN_TOKEN = 'auto';
|
|
46
|
+
const SEARCH_PAGE_PRODUCT_CARD_DEFAULTS = {
|
|
47
|
+
textAlignment: 'start',
|
|
48
|
+
imageFit: 'cover',
|
|
49
|
+
imagePosition: 'center',
|
|
50
|
+
hoverEffect: 'zoom',
|
|
51
|
+
contentPaddingTop: 0,
|
|
52
|
+
contentPaddingRight: 0,
|
|
53
|
+
contentPaddingBottom: 0,
|
|
54
|
+
contentPaddingLeft: 0,
|
|
55
|
+
};
|
|
56
|
+
const labelDefaults = {
|
|
57
|
+
searchPlaceholder: 'Search products',
|
|
58
|
+
filters: 'Filters',
|
|
59
|
+
sort: 'Sort',
|
|
60
|
+
clearAll: 'Clear all',
|
|
61
|
+
loadMore: 'Load more',
|
|
62
|
+
loading: 'Loading…',
|
|
63
|
+
retry: 'Try again',
|
|
64
|
+
emptyTitle: 'No products found',
|
|
65
|
+
emptyMessage: 'Check the spelling or try a broader search.',
|
|
66
|
+
resultsFor: 'Results for',
|
|
67
|
+
products: 'Products',
|
|
68
|
+
soldOut: 'Sold out',
|
|
69
|
+
sale: 'Sale',
|
|
70
|
+
chooseOptions: 'Choose options',
|
|
71
|
+
selectOption: 'Select {option}',
|
|
72
|
+
checkingAvailability: 'Checking availability…',
|
|
73
|
+
unavailable: 'Unavailable',
|
|
74
|
+
// The renderer composes '+<n> <label>', so this is the tail of "+3 more" and
|
|
75
|
+
// not a whole sentence. 'Show more options' read as "+3 Show more options".
|
|
76
|
+
showMoreOptions: 'more',
|
|
77
|
+
showFewerOptions: 'Show fewer options',
|
|
78
|
+
searching: 'Searching…',
|
|
79
|
+
emptyFilteredTitle: 'No products match these filters',
|
|
80
|
+
emptyFilteredMessage: 'Try removing a filter to see more results.',
|
|
81
|
+
clearFilters: 'Clear filters',
|
|
82
|
+
errorTitle: 'We couldn’t load your search',
|
|
83
|
+
errorMessage: 'Check your connection and try again.',
|
|
84
|
+
resultsStale: 'Results could not be updated.',
|
|
85
|
+
adding: 'Adding…',
|
|
86
|
+
added: 'Added',
|
|
87
|
+
};
|
|
88
|
+
// The stock palette every preset falls back to. Presets keep their own colors
|
|
89
|
+
// slot so one can diverge later without touching the others.
|
|
90
|
+
const stockPalette = {
|
|
91
|
+
text: '#121212',
|
|
92
|
+
muted: '#6b7280',
|
|
93
|
+
background: '#ffffff',
|
|
94
|
+
surface: '#f7f7f7',
|
|
95
|
+
border: '#e5e7eb',
|
|
96
|
+
accent: '#121212',
|
|
97
|
+
sale: '#b42318',
|
|
98
|
+
soldOut: '#667085',
|
|
99
|
+
};
|
|
100
|
+
const SEARCH_PAGE_TOKEN_FALLBACKS = {
|
|
101
|
+
dawn: { colors: { ...stockPalette }, controlRadius: 2, containerWidth: 1200 },
|
|
102
|
+
horizon: {
|
|
103
|
+
colors: { ...stockPalette },
|
|
104
|
+
controlRadius: 999,
|
|
105
|
+
containerWidth: 1440,
|
|
106
|
+
},
|
|
107
|
+
'utility-grid': {
|
|
108
|
+
colors: { ...stockPalette },
|
|
109
|
+
controlRadius: 6,
|
|
110
|
+
containerWidth: 1280,
|
|
111
|
+
},
|
|
112
|
+
'editorial-grid': {
|
|
113
|
+
colors: { ...stockPalette },
|
|
114
|
+
controlRadius: 2,
|
|
115
|
+
containerWidth: 1360,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const common = {
|
|
119
|
+
version: SEARCH_PAGE_DESIGN_VERSION,
|
|
120
|
+
typography: {
|
|
121
|
+
fontFamily: 'inherit',
|
|
122
|
+
headingSize: 28,
|
|
123
|
+
productTitleSize: 15,
|
|
124
|
+
priceSize: 14,
|
|
125
|
+
headingWeight: 600,
|
|
126
|
+
productTitleWeight: 500,
|
|
127
|
+
},
|
|
128
|
+
// Colors adopt the live theme by default; merchants pin explicit hex values
|
|
129
|
+
// in the editor when they want to diverge from the theme.
|
|
130
|
+
colors: {
|
|
131
|
+
text: AUTO_DESIGN_TOKEN,
|
|
132
|
+
muted: AUTO_DESIGN_TOKEN,
|
|
133
|
+
background: AUTO_DESIGN_TOKEN,
|
|
134
|
+
surface: AUTO_DESIGN_TOKEN,
|
|
135
|
+
border: AUTO_DESIGN_TOKEN,
|
|
136
|
+
accent: AUTO_DESIGN_TOKEN,
|
|
137
|
+
sale: AUTO_DESIGN_TOKEN,
|
|
138
|
+
soldOut: AUTO_DESIGN_TOKEN,
|
|
139
|
+
},
|
|
140
|
+
shape: {
|
|
141
|
+
borderWidth: 1,
|
|
142
|
+
cardRadius: 0,
|
|
143
|
+
imageRadius: 0,
|
|
144
|
+
controlRadius: AUTO_DESIGN_TOKEN,
|
|
145
|
+
controlHeight: 44,
|
|
146
|
+
},
|
|
147
|
+
fields: {
|
|
148
|
+
image: true,
|
|
149
|
+
title: true,
|
|
150
|
+
price: true,
|
|
151
|
+
compareAtPrice: true,
|
|
152
|
+
discount: true,
|
|
153
|
+
soldOut: true,
|
|
154
|
+
},
|
|
155
|
+
productCard: { ...SEARCH_PAGE_PRODUCT_CARD_DEFAULTS },
|
|
156
|
+
labels: labelDefaults,
|
|
157
|
+
mount: {},
|
|
158
|
+
facets: {
|
|
159
|
+
facets: DEFAULT_FACETS_CONFIG.facets.map((facet) => ({ ...facet })),
|
|
160
|
+
},
|
|
161
|
+
facetIds: deriveFacetIds(DEFAULT_FACETS_CONFIG),
|
|
162
|
+
advancedCss: '',
|
|
163
|
+
};
|
|
164
|
+
const definePreset = (preset, overrides) => {
|
|
165
|
+
var _a, _b;
|
|
166
|
+
return ({
|
|
167
|
+
...common,
|
|
168
|
+
...overrides,
|
|
169
|
+
version: SEARCH_PAGE_DESIGN_VERSION,
|
|
170
|
+
preset,
|
|
171
|
+
typography: { ...common.typography, ...overrides.typography },
|
|
172
|
+
colors: { ...common.colors, ...overrides.colors },
|
|
173
|
+
shape: { ...common.shape, ...overrides.shape },
|
|
174
|
+
fields: { ...common.fields, ...overrides.fields },
|
|
175
|
+
productCard: {
|
|
176
|
+
...SEARCH_PAGE_PRODUCT_CARD_DEFAULTS,
|
|
177
|
+
...overrides.productCard,
|
|
178
|
+
},
|
|
179
|
+
labels: { ...common.labels, ...overrides.labels },
|
|
180
|
+
mount: { ...common.mount, ...overrides.mount },
|
|
181
|
+
facets: overrides.facets
|
|
182
|
+
? { facets: overrides.facets.facets.map((facet) => ({ ...facet })) }
|
|
183
|
+
: common.facets
|
|
184
|
+
? { facets: common.facets.facets.map((facet) => ({ ...facet })) }
|
|
185
|
+
: undefined,
|
|
186
|
+
facetIds: [...((_a = overrides.facetIds) !== null && _a !== void 0 ? _a : common.facetIds)],
|
|
187
|
+
advancedCss: (_b = overrides.advancedCss) !== null && _b !== void 0 ? _b : '',
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
const SEARCH_PAGE_DESIGN_PRESETS = {
|
|
191
|
+
dawn: definePreset('dawn', {
|
|
192
|
+
layout: {
|
|
193
|
+
filterPlacement: 'sidebar',
|
|
194
|
+
containerWidth: AUTO_DESIGN_TOKEN,
|
|
195
|
+
columns: { desktop: 4, tablet: 3, mobile: 2 },
|
|
196
|
+
columnGap: 24,
|
|
197
|
+
rowGap: 36,
|
|
198
|
+
imageRatio: '1 / 1.2',
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
horizon: definePreset('horizon', {
|
|
202
|
+
layout: {
|
|
203
|
+
filterPlacement: 'toolbar',
|
|
204
|
+
containerWidth: AUTO_DESIGN_TOKEN,
|
|
205
|
+
columns: { desktop: 4, tablet: 3, mobile: 2 },
|
|
206
|
+
columnGap: 16,
|
|
207
|
+
rowGap: 28,
|
|
208
|
+
imageRatio: '4 / 5',
|
|
209
|
+
},
|
|
210
|
+
// controlRadius stays 'auto' (from common): pill controls only when the
|
|
211
|
+
// theme itself uses them; the preset fallback keeps the stock pill look.
|
|
212
|
+
shape: { ...common.shape, imageRadius: 8 },
|
|
213
|
+
}),
|
|
214
|
+
'utility-grid': definePreset('utility-grid', {
|
|
215
|
+
layout: {
|
|
216
|
+
filterPlacement: 'sidebar',
|
|
217
|
+
containerWidth: AUTO_DESIGN_TOKEN,
|
|
218
|
+
columns: { desktop: 4, tablet: 3, mobile: 2 },
|
|
219
|
+
columnGap: 12,
|
|
220
|
+
rowGap: 20,
|
|
221
|
+
imageRatio: '1 / 1',
|
|
222
|
+
},
|
|
223
|
+
typography: {
|
|
224
|
+
...common.typography,
|
|
225
|
+
headingSize: 24,
|
|
226
|
+
productTitleSize: 14,
|
|
227
|
+
productTitleWeight: 600,
|
|
228
|
+
},
|
|
229
|
+
shape: { ...common.shape, cardRadius: 8, imageRadius: 8, controlRadius: 6 },
|
|
230
|
+
}),
|
|
231
|
+
'editorial-grid': definePreset('editorial-grid', {
|
|
232
|
+
layout: {
|
|
233
|
+
filterPlacement: 'toolbar',
|
|
234
|
+
containerWidth: AUTO_DESIGN_TOKEN,
|
|
235
|
+
columns: { desktop: 3, tablet: 2, mobile: 1 },
|
|
236
|
+
columnGap: 32,
|
|
237
|
+
rowGap: 48,
|
|
238
|
+
imageRatio: '3 / 4',
|
|
239
|
+
},
|
|
240
|
+
typography: {
|
|
241
|
+
...common.typography,
|
|
242
|
+
fontFamily: 'serif',
|
|
243
|
+
headingSize: 36,
|
|
244
|
+
productTitleSize: 18,
|
|
245
|
+
},
|
|
246
|
+
// A deliberate editorial look keeps its concrete radius rather than
|
|
247
|
+
// adopting the theme's controls.
|
|
248
|
+
shape: { ...common.shape, controlRadius: 2 },
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Served to every enabled store that has published nothing — including
|
|
253
|
+
* storefronts running an SDK bundle pinned before `'auto'` existed, which
|
|
254
|
+
* would emit `--evs-max-width:autopx`, drop the declaration and render
|
|
255
|
+
* full-bleed. So the blind default keeps a concrete width: `'auto'` reaches a
|
|
256
|
+
* storefront only once a merchant picks a preset in the designer, by which
|
|
257
|
+
* point their tag is serving a bundle that understands it.
|
|
258
|
+
*/
|
|
259
|
+
({
|
|
260
|
+
...SEARCH_PAGE_DESIGN_PRESETS.dawn,
|
|
261
|
+
layout: {
|
|
262
|
+
...SEARCH_PAGE_DESIGN_PRESETS.dawn.layout,
|
|
263
|
+
containerWidth: SEARCH_PAGE_TOKEN_FALLBACKS.dawn.containerWidth,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
3
267
|
/**
|
|
4
268
|
* Known ad-network click ID query parameters and the human-readable provider
|
|
5
269
|
* label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
|
|
@@ -886,6 +1150,7 @@ const downscaleImage = async (source) => {
|
|
|
886
1150
|
// Ask can't reuse withSearchResilience: a half-consumed stream isn't retryable.
|
|
887
1151
|
// Idle timeout resets per chunk; both reject with TimeoutError so submitAsk
|
|
888
1152
|
// routes them to the search fallback rather than treating them as cancellation.
|
|
1153
|
+
const QUICK_ADD_VALIDATE_TIMEOUT_MS = 7000;
|
|
889
1154
|
const ASK_REQUEST_TIMEOUT_MS = 7000;
|
|
890
1155
|
const ASK_SSE_IDLE_TIMEOUT_MS = 10000;
|
|
891
1156
|
// Keep-warm cadence for `warm()`. Chrome reaps used-idle sockets after ~5
|
|
@@ -914,7 +1179,30 @@ const readShopifyCurrency = () => {
|
|
|
914
1179
|
return undefined;
|
|
915
1180
|
}
|
|
916
1181
|
};
|
|
917
|
-
const
|
|
1182
|
+
const parseCountryCode = (value) => typeof value === 'string' && /^[a-z]{2}$/i.test(value)
|
|
1183
|
+
? value.toUpperCase()
|
|
1184
|
+
: undefined;
|
|
1185
|
+
const readShopifyCountry = () => {
|
|
1186
|
+
var _a;
|
|
1187
|
+
try {
|
|
1188
|
+
return parseCountryCode((_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.country);
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
return undefined;
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
/**
|
|
1195
|
+
* A host-supplied market wins over `window.Shopify`: nothing sets it on a
|
|
1196
|
+
* Shopify theme (where the storefront's own globals are the truth), and a
|
|
1197
|
+
* headless host that does set it is telling us the market its own page is
|
|
1198
|
+
* rendering — which is the only source that knows, since `window.Shopify`
|
|
1199
|
+
* does not exist there. `uiPreferences.currency` stays the merchant-level
|
|
1200
|
+
* fallback for a headless host that supplies nothing.
|
|
1201
|
+
*/
|
|
1202
|
+
const resolveDisplayCurrency = (uiPreferences, market) => {
|
|
1203
|
+
var _a, _b, _c;
|
|
1204
|
+
return (_c = (_b = (_a = market.currency) !== null && _a !== void 0 ? _a : readShopifyCurrency()) !== null && _b !== void 0 ? _b : parseCurrencyCode(uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.currency)) !== null && _c !== void 0 ? _c : 'USD';
|
|
1205
|
+
};
|
|
918
1206
|
const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, uiPreferences, }) => {
|
|
919
1207
|
if (!baseURL) {
|
|
920
1208
|
throw new Error('baseURL missing');
|
|
@@ -925,6 +1213,17 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
925
1213
|
if (!sendTag) {
|
|
926
1214
|
throw new Error('sendTag missing');
|
|
927
1215
|
}
|
|
1216
|
+
// Replaced wholesale rather than merged, so a host that drops one field
|
|
1217
|
+
// (leaving a market) does not keep that field's stale value.
|
|
1218
|
+
let market = {};
|
|
1219
|
+
const setMarket = (next) => {
|
|
1220
|
+
const currency = parseCurrencyCode(next === null || next === void 0 ? void 0 : next.currency);
|
|
1221
|
+
const country = parseCountryCode(next === null || next === void 0 ? void 0 : next.country);
|
|
1222
|
+
market = {
|
|
1223
|
+
...(currency ? { currency } : {}),
|
|
1224
|
+
...(country ? { country } : {}),
|
|
1225
|
+
};
|
|
1226
|
+
};
|
|
928
1227
|
const getChannel = () => { var _a; return (((_a = window[registryKey]) === null || _a === void 0 ? void 0 : _a.ui) ? 'widget' : 'headless'); };
|
|
929
1228
|
const getHeaders = () => {
|
|
930
1229
|
const headers = new Headers({ EdgeTagUserId: userId });
|
|
@@ -965,7 +1264,7 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
965
1264
|
const getURL = (path) => {
|
|
966
1265
|
const url = new URL(baseOrigin);
|
|
967
1266
|
url.pathname = `${PROVIDER_PATH_PREFIX}${path}`;
|
|
968
|
-
url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences));
|
|
1267
|
+
url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences, market));
|
|
969
1268
|
return url;
|
|
970
1269
|
};
|
|
971
1270
|
// Only `/user/*` endpoints return per-user PII (recents, profile-bound
|
|
@@ -975,10 +1274,20 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
975
1274
|
// (`Allow-Credentials: true` + non-wildcard origin echo) on every
|
|
976
1275
|
// response — so default to `omit` and opt in below.
|
|
977
1276
|
const needsCredentials = (path) => path.startsWith('/user/');
|
|
978
|
-
const getJSON = async (path, label,
|
|
1277
|
+
const getJSON = async (path, label,
|
|
1278
|
+
// An array value appends one param per entry (repeatable keys, e.g. the
|
|
1279
|
+
// results page's `f.<key>` filters).
|
|
1280
|
+
params, signal) => {
|
|
979
1281
|
const url = getURL(path);
|
|
980
1282
|
for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
|
|
981
|
-
|
|
1283
|
+
if (Array.isArray(value)) {
|
|
1284
|
+
for (const entry of value) {
|
|
1285
|
+
url.searchParams.append(key, entry);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
url.searchParams.set(key, value);
|
|
1290
|
+
}
|
|
982
1291
|
}
|
|
983
1292
|
const credentialed = needsCredentials(path);
|
|
984
1293
|
if (!credentialed) {
|
|
@@ -1007,6 +1316,14 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
1007
1316
|
logger.info('EvoSearch API: Fetching ask config');
|
|
1008
1317
|
return getJSON('/config', 'Config', undefined, signal);
|
|
1009
1318
|
};
|
|
1319
|
+
const searchPageDesign = (signal, previewToken) => {
|
|
1320
|
+
logger.info(`EvoSearch API: Fetching ${previewToken ? 'preview' : 'published'} search page design`);
|
|
1321
|
+
return getJSON('/search-page-design', 'SearchPageDesign',
|
|
1322
|
+
// Not `preview`: that name is the identity param every preview session
|
|
1323
|
+
// sets to `1`, and identity params are written after these, so the token
|
|
1324
|
+
// would be overwritten and the worker would see the session flag instead.
|
|
1325
|
+
previewToken ? { designPreview: previewToken } : undefined, signal);
|
|
1326
|
+
};
|
|
1010
1327
|
// Isolate warmer doubling as connection establisher: primes the worker's
|
|
1011
1328
|
// search index off the request path AND opens the h2/h3 connection before
|
|
1012
1329
|
// the first real keystroke needs it. Runs under the resilience wrapper so a
|
|
@@ -1077,6 +1394,109 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
1077
1394
|
logger.info(`EvoSearch API: Search success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
|
|
1078
1395
|
return data;
|
|
1079
1396
|
};
|
|
1397
|
+
// Results-page fetch: same /search endpoint with the opt-in extensions
|
|
1398
|
+
// (facets, filters, pagination, explicit sort). Kept separate from the
|
|
1399
|
+
// autocomplete `search()` so its hot path stays untouched.
|
|
1400
|
+
const searchResults = (query, options = {}) => {
|
|
1401
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1402
|
+
const params = {
|
|
1403
|
+
q: query,
|
|
1404
|
+
facets: '1',
|
|
1405
|
+
};
|
|
1406
|
+
if (options.limit != null) {
|
|
1407
|
+
params['limit'] = String(options.limit);
|
|
1408
|
+
}
|
|
1409
|
+
if (options.cursor) {
|
|
1410
|
+
params['cursor'] = options.cursor;
|
|
1411
|
+
}
|
|
1412
|
+
if (options.offset) {
|
|
1413
|
+
params['offset'] = String(options.offset);
|
|
1414
|
+
}
|
|
1415
|
+
if (options.sort && options.sort !== 'relevance') {
|
|
1416
|
+
params['sort'] = options.sort;
|
|
1417
|
+
}
|
|
1418
|
+
if ((_a = options.previewToken) === null || _a === void 0 ? void 0 : _a.trim()) {
|
|
1419
|
+
params['designPreview'] = options.previewToken.trim();
|
|
1420
|
+
}
|
|
1421
|
+
const filters = options.filters;
|
|
1422
|
+
for (const [key, values] of Object.entries((_b = filters === null || filters === void 0 ? void 0 : filters.terms) !== null && _b !== void 0 ? _b : {})) {
|
|
1423
|
+
if (values.length) {
|
|
1424
|
+
params[`f.${key}`] = values;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
for (const [key, window] of Object.entries((_c = filters === null || filters === void 0 ? void 0 : filters.ranges) !== null && _c !== void 0 ? _c : {})) {
|
|
1428
|
+
if (window.min != null || window.max != null) {
|
|
1429
|
+
params[`f.${key}`] = `${(_d = window.min) !== null && _d !== void 0 ? _d : ''}..${(_e = window.max) !== null && _e !== void 0 ? _e : ''}`;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if ((filters === null || filters === void 0 ? void 0 : filters.priceMin) != null) {
|
|
1433
|
+
params['priceMin'] = String(filters.priceMin);
|
|
1434
|
+
}
|
|
1435
|
+
if ((filters === null || filters === void 0 ? void 0 : filters.priceMax) != null) {
|
|
1436
|
+
params['priceMax'] = String(filters.priceMax);
|
|
1437
|
+
}
|
|
1438
|
+
return withSearchResilience((attemptSignal) => getJSON('/search', 'SearchResults', params, attemptSignal), (_f = options.signal) !== null && _f !== void 0 ? _f : NEVER_ABORTED);
|
|
1439
|
+
};
|
|
1440
|
+
// Same deadline as `validateQuickAdd`, and for the same reason: the only
|
|
1441
|
+
// caller passes no signal, so without one a stalled request never settles —
|
|
1442
|
+
// and its promise is memoized, so the swatch picker it backs stays pending
|
|
1443
|
+
// for the life of the page rather than retrying.
|
|
1444
|
+
const productVariants = async (productId, signal) => {
|
|
1445
|
+
const controller = new AbortController();
|
|
1446
|
+
const onCallerAbort = () => controller.abort();
|
|
1447
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort);
|
|
1448
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
1449
|
+
controller.abort();
|
|
1450
|
+
}
|
|
1451
|
+
const timer = setTimeout(() => controller.abort(), QUICK_ADD_VALIDATE_TIMEOUT_MS);
|
|
1452
|
+
try {
|
|
1453
|
+
return await getJSON(`/products/${encodeURIComponent(productId)}/variants`, 'ProductVariants', undefined, controller.signal);
|
|
1454
|
+
}
|
|
1455
|
+
finally {
|
|
1456
|
+
clearTimeout(timer);
|
|
1457
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
const validateQuickAdd = async (productId, variantId, signal) => {
|
|
1461
|
+
var _a;
|
|
1462
|
+
const url = getURL('/quick-add/validate');
|
|
1463
|
+
setIdentityParams(url);
|
|
1464
|
+
const headers = getHeaders();
|
|
1465
|
+
headers.set('Content-Type', 'application/json');
|
|
1466
|
+
// Same deadline pattern as the other POSTs: a caller that passes no signal
|
|
1467
|
+
// would otherwise have no timeout, and a stalled request leaves the
|
|
1468
|
+
// quick-add control pending forever.
|
|
1469
|
+
const controller = new AbortController();
|
|
1470
|
+
const onCallerAbort = () => controller.abort();
|
|
1471
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onCallerAbort);
|
|
1472
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
1473
|
+
controller.abort();
|
|
1474
|
+
}
|
|
1475
|
+
const timer = setTimeout(() => controller.abort(), QUICK_ADD_VALIDATE_TIMEOUT_MS);
|
|
1476
|
+
const country = (_a = market.country) !== null && _a !== void 0 ? _a : readShopifyCountry();
|
|
1477
|
+
let response;
|
|
1478
|
+
try {
|
|
1479
|
+
response = await fetchImpl(url, {
|
|
1480
|
+
method: 'POST',
|
|
1481
|
+
headers,
|
|
1482
|
+
credentials: 'omit',
|
|
1483
|
+
signal: controller.signal,
|
|
1484
|
+
body: JSON.stringify({
|
|
1485
|
+
productId,
|
|
1486
|
+
variantId,
|
|
1487
|
+
...(country ? { country } : {}),
|
|
1488
|
+
}),
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
finally {
|
|
1492
|
+
clearTimeout(timer);
|
|
1493
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onCallerAbort);
|
|
1494
|
+
}
|
|
1495
|
+
if (!response.ok) {
|
|
1496
|
+
return { eligible: false, reason: 'context-unavailable' };
|
|
1497
|
+
}
|
|
1498
|
+
return (await response.json());
|
|
1499
|
+
};
|
|
1080
1500
|
// Image search. Unlike `/search` there is no response cache on the worker
|
|
1081
1501
|
// side and every call costs a multimodal embedding, so callers must debounce
|
|
1082
1502
|
// and abort rather than fire per keystroke. A 403 `{disabled:true}` means the
|
|
@@ -1290,6 +1710,8 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
1290
1710
|
return {
|
|
1291
1711
|
trending,
|
|
1292
1712
|
config,
|
|
1713
|
+
setMarket,
|
|
1714
|
+
searchPageDesign,
|
|
1293
1715
|
warm,
|
|
1294
1716
|
cancelWarm,
|
|
1295
1717
|
user: {
|
|
@@ -1298,6 +1720,9 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
|
|
|
1298
1720
|
clearRecents,
|
|
1299
1721
|
},
|
|
1300
1722
|
search,
|
|
1723
|
+
searchResults,
|
|
1724
|
+
productVariants,
|
|
1725
|
+
validateQuickAdd,
|
|
1301
1726
|
searchImage,
|
|
1302
1727
|
askStream,
|
|
1303
1728
|
sendEvent,
|
|
@@ -1367,7 +1792,7 @@ const startHeadlessKeepWarm = (api) => {
|
|
|
1367
1792
|
};
|
|
1368
1793
|
};
|
|
1369
1794
|
const init = (params) => {
|
|
1370
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1795
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1371
1796
|
if (typeof window == 'undefined' || typeof document == 'undefined') {
|
|
1372
1797
|
return;
|
|
1373
1798
|
}
|
|
@@ -1377,8 +1802,13 @@ const init = (params) => {
|
|
|
1377
1802
|
// A re-init can land on a different channel, or bail out entirely, so drop
|
|
1378
1803
|
// any previous cadence before deciding which one this run owns.
|
|
1379
1804
|
disposeKeepWarm === null || disposeKeepWarm === void 0 ? void 0 : disposeKeepWarm();
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1805
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1806
|
+
const evsParam = urlParams.get('evs');
|
|
1807
|
+
// A designer draft link carries only its token; in preview mode the SDK
|
|
1808
|
+
// would otherwise never initialize on it, so the token doubles as the
|
|
1809
|
+
// preview-session trigger. Only `?evs` verbs can turn preview off.
|
|
1810
|
+
const designerPreview = evsParam === null && ((_c = (_b = urlParams.get('evs-preview')) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '') !== '';
|
|
1811
|
+
if (evsParam === 'preview' || designerPreview) {
|
|
1382
1812
|
try {
|
|
1383
1813
|
sessionStorage.removeItem(disableKeyName);
|
|
1384
1814
|
sessionStorage.setItem(previewKeyName, '1');
|
|
@@ -1408,7 +1838,7 @@ const init = (params) => {
|
|
|
1408
1838
|
error('EvoSearch SDK: Failed to clear evoSearch sessionStorage');
|
|
1409
1839
|
}
|
|
1410
1840
|
}
|
|
1411
|
-
const { enabled, uiPreferences, askAi } = (
|
|
1841
|
+
const { enabled, uiPreferences, askAi } = (_e = (_d = params.manifest) === null || _d === void 0 ? void 0 : _d.variables) !== null && _e !== void 0 ? _e : {};
|
|
1412
1842
|
const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
|
|
1413
1843
|
const askAiEnabled = (askAi === null || askAi === void 0 ? void 0 : askAi.enabled) === true;
|
|
1414
1844
|
const visualSearchEnabled = (uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.visualSearch) === true;
|
|
@@ -1424,7 +1854,7 @@ const init = (params) => {
|
|
|
1424
1854
|
const evoSearchAPI = createEvoSearchAPI({
|
|
1425
1855
|
baseURL: params.baseUrl,
|
|
1426
1856
|
userId: params.userId,
|
|
1427
|
-
sessionId: (
|
|
1857
|
+
sessionId: (_f = params.session) === null || _f === void 0 ? void 0 : _f.sessionId,
|
|
1428
1858
|
sendTag: params.sendTag,
|
|
1429
1859
|
uiPreferences,
|
|
1430
1860
|
});
|
|
@@ -1439,7 +1869,7 @@ const init = (params) => {
|
|
|
1439
1869
|
options: { destination: params.baseUrl },
|
|
1440
1870
|
});
|
|
1441
1871
|
if (!uiImplementation) {
|
|
1442
|
-
if (((
|
|
1872
|
+
if (((_h = (_g = params.manifest) === null || _g === void 0 ? void 0 : _g.variables) === null || _h === void 0 ? void 0 : _h.disableDefaultUI) === true) {
|
|
1443
1873
|
logger.info('EvoSearch SDK: Running in headless mode, no default UI will render');
|
|
1444
1874
|
// Deliberately keyed on `disableDefaultUI` rather than bare `headless`:
|
|
1445
1875
|
// in the else branch the UI is missing by mistake, so search isn't
|
|
@@ -1454,7 +1884,7 @@ const init = (params) => {
|
|
|
1454
1884
|
}
|
|
1455
1885
|
uiImplementation.init({
|
|
1456
1886
|
destination: params.baseUrl,
|
|
1457
|
-
sessionId: (
|
|
1887
|
+
sessionId: (_j = params.session) === null || _j === void 0 ? void 0 : _j.sessionId,
|
|
1458
1888
|
userId: params.userId,
|
|
1459
1889
|
trendingSearches: [],
|
|
1460
1890
|
handpickedProducts: [],
|
|
@@ -1475,7 +1905,7 @@ const init = (params) => {
|
|
|
1475
1905
|
}
|
|
1476
1906
|
catch (err) {
|
|
1477
1907
|
error(`EvoSearch SDK: Initialization failed - ${err}`);
|
|
1478
|
-
if ((
|
|
1908
|
+
if ((_k = window[registryKey]) === null || _k === void 0 ? void 0 : _k.api) {
|
|
1479
1909
|
notifyReady();
|
|
1480
1910
|
}
|
|
1481
1911
|
}
|
|
@@ -1484,7 +1914,7 @@ const init = (params) => {
|
|
|
1484
1914
|
const tag = ({ data }) => {
|
|
1485
1915
|
try {
|
|
1486
1916
|
return {
|
|
1487
|
-
sdkVersion: "1.70.
|
|
1917
|
+
sdkVersion: "1.70.2" || '',
|
|
1488
1918
|
searchQuery: data['searchQuery'],
|
|
1489
1919
|
searchResults: data['searchResults'],
|
|
1490
1920
|
timestamp: Date.now(),
|
|
@@ -1493,7 +1923,7 @@ const tag = ({ data }) => {
|
|
|
1493
1923
|
catch (err) {
|
|
1494
1924
|
console.error(`EvoSearch SDK: Tagging failed - ${err}`);
|
|
1495
1925
|
return {
|
|
1496
|
-
sdkVersion: "1.70.
|
|
1926
|
+
sdkVersion: "1.70.2" ,
|
|
1497
1927
|
timestamp: Date.now(),
|
|
1498
1928
|
};
|
|
1499
1929
|
}
|