@nuskin/react-search-sdk 1.0.0-comx-2233.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 +27 -0
- package/dist/index.d.mts +513 -0
- package/dist/index.d.ts +513 -0
- package/dist/index.js +1723 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1687 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1687 @@
|
|
|
1
|
+
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var STOREFRONT_SHOPPING_CONTEXT = "storefront";
|
|
5
|
+
var CATALOG_SUGGESTIONS_PATH = "/storefront/catalogs/search/suggestions";
|
|
6
|
+
var RECENT_SEARCHES_PREFIX = "nuskin:header:recentSearches";
|
|
7
|
+
var RECENT_SEARCHES_DEFAULT_LIMIT = 10;
|
|
8
|
+
var DEFAULT_SUGGESTION_PAGE = 1;
|
|
9
|
+
var DEFAULT_SUGGESTION_SIZE = 6;
|
|
10
|
+
var DEFAULT_SEARCH_PATH = "/search";
|
|
11
|
+
var DEBOUNCE_MS = 220;
|
|
12
|
+
var MAX_VISIBLE_SUGGESTIONS = 6;
|
|
13
|
+
var SEARCH_PLACEHOLDER = "Search by product name, ingredient, benefits, or SKU";
|
|
14
|
+
var TRIGGER_PLACEHOLDER = "Product Search";
|
|
15
|
+
var DEFAULT_AI_CHAT_BASE_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/chat";
|
|
16
|
+
var DEFAULT_AI_CHAT_CONVERSATIONS_URL = `${DEFAULT_AI_CHAT_BASE_URL}/conversations`;
|
|
17
|
+
var DEFAULT_AI_CHAT_MESSAGES_URL = `${DEFAULT_AI_CHAT_BASE_URL}/messages`;
|
|
18
|
+
|
|
19
|
+
// src/services/SearchApiService.ts
|
|
20
|
+
var DEFAULT_SEARCH_PLACEHOLDER = TRIGGER_PLACEHOLDER;
|
|
21
|
+
var DEFAULT_INPUT_ARIA_LABEL = SEARCH_PLACEHOLDER;
|
|
22
|
+
function canUseStorage(storage) {
|
|
23
|
+
return Boolean(storage) && typeof storage.getItem === "function";
|
|
24
|
+
}
|
|
25
|
+
function getSessionStorage(providedStorage) {
|
|
26
|
+
if (providedStorage) return providedStorage;
|
|
27
|
+
if (typeof window !== "undefined") {
|
|
28
|
+
try {
|
|
29
|
+
return window.sessionStorage;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function normalizeLocaleParts({
|
|
37
|
+
country,
|
|
38
|
+
language,
|
|
39
|
+
locale
|
|
40
|
+
} = {}) {
|
|
41
|
+
const normalizedCountry = String(country ?? "").trim().toUpperCase();
|
|
42
|
+
const normalizedLanguage = String(language ?? "").trim().toLowerCase();
|
|
43
|
+
const normalizedLocale = String(locale ?? "").trim() || "UNKNOWN";
|
|
44
|
+
return {
|
|
45
|
+
country: normalizedCountry || "UNKNOWN",
|
|
46
|
+
language: normalizedLanguage || "unknown",
|
|
47
|
+
locale: normalizedLocale
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function getRecentHeaderSearchesStorageKey(localeParts) {
|
|
51
|
+
const { country, language, locale } = normalizeLocaleParts(localeParts);
|
|
52
|
+
return `${RECENT_SEARCHES_PREFIX}:${locale}:${country}:${language}`;
|
|
53
|
+
}
|
|
54
|
+
function getRecentHeaderSearches(localeParts, storage) {
|
|
55
|
+
if (!canUseStorage(storage)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const rawValue = storage.getItem(
|
|
60
|
+
getRecentHeaderSearchesStorageKey(localeParts)
|
|
61
|
+
);
|
|
62
|
+
if (!rawValue) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const parsedValue = JSON.parse(rawValue);
|
|
66
|
+
if (!Array.isArray(parsedValue)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return parsedValue.map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function addRecentHeaderSearch(localeParts, query, storage, limit = RECENT_SEARCHES_DEFAULT_LIMIT) {
|
|
75
|
+
const normalizedQuery = String(query ?? "").trim();
|
|
76
|
+
if (!normalizedQuery || !canUseStorage(storage)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const existingValues = getRecentHeaderSearches(
|
|
81
|
+
localeParts,
|
|
82
|
+
storage
|
|
83
|
+
).filter(
|
|
84
|
+
(item) => item.toLowerCase() !== normalizedQuery.toLowerCase()
|
|
85
|
+
);
|
|
86
|
+
const nextValues = [normalizedQuery, ...existingValues].slice(0, limit);
|
|
87
|
+
storage.setItem(
|
|
88
|
+
getRecentHeaderSearchesStorageKey(localeParts),
|
|
89
|
+
JSON.stringify(nextValues)
|
|
90
|
+
);
|
|
91
|
+
return nextValues;
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function clearRecentHeaderSearches(localeParts, storage) {
|
|
97
|
+
if (!canUseStorage(storage)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
storage.removeItem(getRecentHeaderSearchesStorageKey(localeParts));
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function getBrowserLocation(providedLocation) {
|
|
106
|
+
if (providedLocation) return providedLocation;
|
|
107
|
+
if (typeof window !== "undefined" && window.location)
|
|
108
|
+
return window.location;
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function resolveApiGatewayPath(apiGatewayPath, apiName = STOREFRONT_SHOPPING_CONTEXT) {
|
|
112
|
+
const apiGateway = String(apiGatewayPath ?? "").trim();
|
|
113
|
+
if (!apiGateway) return "";
|
|
114
|
+
return apiGateway.replace(/\{api_name\}/gi, apiName).replace(/\/$/, "");
|
|
115
|
+
}
|
|
116
|
+
function resolveCatalogApiBaseUrl(marketConfig) {
|
|
117
|
+
return resolveApiGatewayPath(marketConfig?.api_gateway_path);
|
|
118
|
+
}
|
|
119
|
+
function resolveCatalogStoreId(marketConfig) {
|
|
120
|
+
return String(marketConfig?.store_id ?? "").trim();
|
|
121
|
+
}
|
|
122
|
+
function buildCatalogSuggestionsUrl(apiBaseUrl) {
|
|
123
|
+
const normalizedBase = String(apiBaseUrl ?? "").trim().replace(/\/$/, "");
|
|
124
|
+
if (!normalizedBase) return "";
|
|
125
|
+
const suggestionsPath = normalizedBase.endsWith(
|
|
126
|
+
`/${STOREFRONT_SHOPPING_CONTEXT}`
|
|
127
|
+
) ? "/catalogs/search/suggestions" : CATALOG_SUGGESTIONS_PATH;
|
|
128
|
+
return `${normalizedBase}${suggestionsPath}`;
|
|
129
|
+
}
|
|
130
|
+
function resolveSuggestionsUrl(suggestionsApi, locationLike) {
|
|
131
|
+
const pathOrUrl = String(suggestionsApi ?? "").trim();
|
|
132
|
+
if (!pathOrUrl) return "";
|
|
133
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
134
|
+
return pathOrUrl;
|
|
135
|
+
}
|
|
136
|
+
const normalizedPath = pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`;
|
|
137
|
+
const origin = String(locationLike?.origin ?? "").trim();
|
|
138
|
+
return origin ? `${origin}${normalizedPath}` : normalizedPath;
|
|
139
|
+
}
|
|
140
|
+
async function resolveCatalogRequestConfig({
|
|
141
|
+
suggestionsApi,
|
|
142
|
+
storeIdOption,
|
|
143
|
+
locationLike,
|
|
144
|
+
country,
|
|
145
|
+
getMarketConfig
|
|
146
|
+
}) {
|
|
147
|
+
const explicitStoreId = storeIdOption !== void 0 && storeIdOption !== null && String(storeIdOption).trim() !== "" ? String(storeIdOption).trim() : "";
|
|
148
|
+
const marketConfig = getMarketConfig ? await getMarketConfig({ locationLike, country }) ?? null : null;
|
|
149
|
+
const configuredApiBase = resolveCatalogApiBaseUrl(marketConfig);
|
|
150
|
+
const configuredStoreId = resolveCatalogStoreId(marketConfig);
|
|
151
|
+
const explicitSuggestionsApi = String(suggestionsApi ?? "").trim();
|
|
152
|
+
return {
|
|
153
|
+
suggestionsUrl: explicitSuggestionsApi ? resolveSuggestionsUrl(explicitSuggestionsApi, locationLike) : buildCatalogSuggestionsUrl(configuredApiBase),
|
|
154
|
+
storeId: explicitStoreId || configuredStoreId
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function toCatalogLocale(localeParts) {
|
|
158
|
+
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
159
|
+
const country = String(localeParts.country ?? "US").toUpperCase();
|
|
160
|
+
return `${language}_${country}`;
|
|
161
|
+
}
|
|
162
|
+
function buildSearchResultsPath(query, localeParts) {
|
|
163
|
+
const trimmedQuery = String(query ?? "").trim();
|
|
164
|
+
const hyphenatedQuery = trimmedQuery.replace(/\s+/g, "-");
|
|
165
|
+
const encodedQuery = encodeURIComponent(hyphenatedQuery).replace(
|
|
166
|
+
/%2D/gi,
|
|
167
|
+
"-"
|
|
168
|
+
);
|
|
169
|
+
const country = localeParts.country.toLowerCase();
|
|
170
|
+
const language = localeParts.language.toLowerCase();
|
|
171
|
+
return `/${country}/${language}${DEFAULT_SEARCH_PATH}/${encodedQuery}`;
|
|
172
|
+
}
|
|
173
|
+
function normalizeOptionUrl(rawValue) {
|
|
174
|
+
if (typeof rawValue !== "string") return "";
|
|
175
|
+
return rawValue.trim();
|
|
176
|
+
}
|
|
177
|
+
function safeDecodeURIComponent(value) {
|
|
178
|
+
try {
|
|
179
|
+
return decodeURIComponent(value);
|
|
180
|
+
} catch {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function trimEdgeHyphens(value) {
|
|
185
|
+
let normalizedValue = value;
|
|
186
|
+
while (normalizedValue.startsWith("-")) {
|
|
187
|
+
normalizedValue = normalizedValue.slice(1);
|
|
188
|
+
}
|
|
189
|
+
while (normalizedValue.endsWith("-")) {
|
|
190
|
+
normalizedValue = normalizedValue.slice(0, -1);
|
|
191
|
+
}
|
|
192
|
+
return normalizedValue;
|
|
193
|
+
}
|
|
194
|
+
function slugifyPathSegment(value) {
|
|
195
|
+
const decoded = safeDecodeURIComponent(String(value ?? "")).replace(
|
|
196
|
+
/\+/g,
|
|
197
|
+
" "
|
|
198
|
+
);
|
|
199
|
+
const normalized = decoded.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/-{2,}/g, "-").toLowerCase();
|
|
200
|
+
return trimEdgeHyphens(normalized);
|
|
201
|
+
}
|
|
202
|
+
function buildProductSlugPath(slugValue, localeParts) {
|
|
203
|
+
const slug = slugifyPathSegment(slugValue);
|
|
204
|
+
if (!slug) return "";
|
|
205
|
+
const country = String(localeParts.country ?? "US").toLowerCase();
|
|
206
|
+
const language = String(localeParts.language ?? "en").toLowerCase();
|
|
207
|
+
return `/${country}/${language}/product/${slug}`;
|
|
208
|
+
}
|
|
209
|
+
function isNavigableUrl(value) {
|
|
210
|
+
if (!value) return false;
|
|
211
|
+
return value.startsWith("/") || /^https?:\/\//i.test(value);
|
|
212
|
+
}
|
|
213
|
+
function stripLocalePrefix(path, localeParts) {
|
|
214
|
+
const prefix = `/${localeParts.country.toLowerCase()}/${localeParts.language.toLowerCase()}`;
|
|
215
|
+
return path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
|
|
216
|
+
}
|
|
217
|
+
function resolveOptionHref(option, localeParts) {
|
|
218
|
+
const productSlugPath = buildProductSlugPath(
|
|
219
|
+
option?.slug ?? option?.productSlug ?? option?.product_slug ?? null,
|
|
220
|
+
localeParts
|
|
221
|
+
);
|
|
222
|
+
if (productSlugPath) {
|
|
223
|
+
return productSlugPath;
|
|
224
|
+
}
|
|
225
|
+
const directUrl = normalizeOptionUrl(option?.href ?? option?.url);
|
|
226
|
+
if (isNavigableUrl(directUrl) && !directUrl.includes("/chr/")) {
|
|
227
|
+
return directUrl;
|
|
228
|
+
}
|
|
229
|
+
return "";
|
|
230
|
+
}
|
|
231
|
+
async function fetchJsonFromCandidates(fetchImpl, requestUrls) {
|
|
232
|
+
let lastError;
|
|
233
|
+
for (const requestUrl of requestUrls) {
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetchImpl(requestUrl, {
|
|
236
|
+
method: "GET",
|
|
237
|
+
credentials: "include",
|
|
238
|
+
headers: { Accept: "application/json" }
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
lastError = new Error(
|
|
242
|
+
`Request failed with status ${response.status}`
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
return await response.json();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
lastError = error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
throw lastError ?? new Error("Unable to fetch header search data");
|
|
252
|
+
}
|
|
253
|
+
function createSearchService(options = {}) {
|
|
254
|
+
const {
|
|
255
|
+
fetchImpl: fetchImplOption,
|
|
256
|
+
location: providedLocation,
|
|
257
|
+
storage: providedStorage,
|
|
258
|
+
suggestionsApi,
|
|
259
|
+
storeId: storeIdOption,
|
|
260
|
+
country,
|
|
261
|
+
language,
|
|
262
|
+
locale,
|
|
263
|
+
searchPlaceholder = DEFAULT_SEARCH_PLACEHOLDER,
|
|
264
|
+
inputAriaLabel = DEFAULT_INPUT_ARIA_LABEL,
|
|
265
|
+
getMarketConfig
|
|
266
|
+
} = options;
|
|
267
|
+
const fetchImpl = fetchImplOption !== void 0 ? fetchImplOption : typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
268
|
+
const locationLike = getBrowserLocation(providedLocation);
|
|
269
|
+
const storage = getSessionStorage(providedStorage);
|
|
270
|
+
const localeParts = normalizeLocaleParts({ country, language, locale });
|
|
271
|
+
let catalogRequestConfigPromise = null;
|
|
272
|
+
function loadCatalogRequestConfig() {
|
|
273
|
+
if (!catalogRequestConfigPromise) {
|
|
274
|
+
catalogRequestConfigPromise = resolveCatalogRequestConfig({
|
|
275
|
+
suggestionsApi,
|
|
276
|
+
storeIdOption,
|
|
277
|
+
locationLike,
|
|
278
|
+
country: localeParts.country,
|
|
279
|
+
getMarketConfig
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return catalogRequestConfigPromise;
|
|
283
|
+
}
|
|
284
|
+
const service = {
|
|
285
|
+
async getSearchChromeCopy() {
|
|
286
|
+
return {
|
|
287
|
+
placeholder: searchPlaceholder || DEFAULT_SEARCH_PLACEHOLDER,
|
|
288
|
+
inputAriaLabel: inputAriaLabel || DEFAULT_INPUT_ARIA_LABEL
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
async fetchSuggestions(searchTerm) {
|
|
292
|
+
const trimmedSearchTerm = String(searchTerm ?? "").trim();
|
|
293
|
+
if (!trimmedSearchTerm || !fetchImpl) {
|
|
294
|
+
return {
|
|
295
|
+
suggestions: [],
|
|
296
|
+
didYouMean: [],
|
|
297
|
+
totalResultsCount: 0
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const { suggestionsUrl, storeId } = await loadCatalogRequestConfig();
|
|
301
|
+
if (!suggestionsUrl || !storeId) {
|
|
302
|
+
return {
|
|
303
|
+
suggestions: [],
|
|
304
|
+
didYouMean: [],
|
|
305
|
+
totalResultsCount: 0
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const queryParams = new URLSearchParams({
|
|
309
|
+
searchTerm: trimmedSearchTerm,
|
|
310
|
+
page: String(DEFAULT_SUGGESTION_PAGE),
|
|
311
|
+
size: String(DEFAULT_SUGGESTION_SIZE),
|
|
312
|
+
locale: toCatalogLocale(localeParts),
|
|
313
|
+
storeId: String(storeId)
|
|
314
|
+
});
|
|
315
|
+
const requestUrls = [`${suggestionsUrl}?${queryParams.toString()}`];
|
|
316
|
+
const responsePayload = await fetchJsonFromCandidates(
|
|
317
|
+
fetchImpl,
|
|
318
|
+
requestUrls
|
|
319
|
+
);
|
|
320
|
+
const suggestionsField = Array.isArray(responsePayload?.suggestions) ? responsePayload?.suggestions : [];
|
|
321
|
+
const productField = Array.isArray(responsePayload?.product) ? responsePayload?.product : null;
|
|
322
|
+
const rawDidYouMean = responsePayload?.didYouMean ?? responsePayload?.did_you_mean;
|
|
323
|
+
const didYouMeanField = Array.isArray(rawDidYouMean) ? rawDidYouMean : [];
|
|
324
|
+
const pageableTotal = Number(
|
|
325
|
+
responsePayload?.pageableInfo?.totalCount ?? Number.NaN
|
|
326
|
+
);
|
|
327
|
+
const totalResultsCount = typeof responsePayload?.totalResultsCount === "number" ? responsePayload.totalResultsCount : !Number.isNaN(pageableTotal) ? pageableTotal : productField?.length ?? suggestionsField.length;
|
|
328
|
+
return {
|
|
329
|
+
...responsePayload ?? {},
|
|
330
|
+
suggestions: suggestionsField,
|
|
331
|
+
didYouMean: didYouMeanField,
|
|
332
|
+
totalResultsCount
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
submitProductSearch(query, option) {
|
|
336
|
+
const trimmedQuery = String(query ?? "").trim();
|
|
337
|
+
if (!trimmedQuery) return void 0;
|
|
338
|
+
addRecentHeaderSearch(localeParts, trimmedQuery, storage);
|
|
339
|
+
const optionHref = resolveOptionHref(option, localeParts);
|
|
340
|
+
if (optionHref && locationLike && typeof locationLike.assign === "function") {
|
|
341
|
+
locationLike.assign(optionHref);
|
|
342
|
+
return stripLocalePrefix(optionHref, localeParts);
|
|
343
|
+
}
|
|
344
|
+
const targetPath = buildSearchResultsPath(
|
|
345
|
+
trimmedQuery,
|
|
346
|
+
localeParts
|
|
347
|
+
);
|
|
348
|
+
if (locationLike && typeof locationLike.assign === "function") {
|
|
349
|
+
locationLike.assign(targetPath);
|
|
350
|
+
}
|
|
351
|
+
return stripLocalePrefix(targetPath, localeParts);
|
|
352
|
+
},
|
|
353
|
+
activateSuggestionAction(option) {
|
|
354
|
+
if (options.onSuggestionAction) {
|
|
355
|
+
const result = options.onSuggestionAction(option);
|
|
356
|
+
if (result instanceof Promise) {
|
|
357
|
+
result.then((handled) => {
|
|
358
|
+
if (!handled) {
|
|
359
|
+
const optionHref2 = resolveOptionHref(
|
|
360
|
+
option,
|
|
361
|
+
localeParts
|
|
362
|
+
);
|
|
363
|
+
if (optionHref2 && locationLike && typeof locationLike.assign === "function") {
|
|
364
|
+
locationLike.assign(optionHref2);
|
|
365
|
+
} else {
|
|
366
|
+
const optionQuery2 = option?.value ?? option?.label ?? option?.identifier ?? "";
|
|
367
|
+
service.submitProductSearch(
|
|
368
|
+
String(optionQuery2)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}).catch((error) => {
|
|
373
|
+
console.error(
|
|
374
|
+
"Error in onSuggestionAction:",
|
|
375
|
+
error
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
return "action-pending";
|
|
379
|
+
} else if (result === true) {
|
|
380
|
+
return "action-handled";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const optionHref = resolveOptionHref(option, localeParts);
|
|
384
|
+
if (optionHref && locationLike && typeof locationLike.assign === "function") {
|
|
385
|
+
locationLike.assign(optionHref);
|
|
386
|
+
return optionHref;
|
|
387
|
+
}
|
|
388
|
+
const optionQuery = option?.value ?? option?.label ?? option?.identifier ?? "";
|
|
389
|
+
return service.submitProductSearch(String(optionQuery));
|
|
390
|
+
},
|
|
391
|
+
getSuggestionHref(option) {
|
|
392
|
+
return stripLocalePrefix(
|
|
393
|
+
resolveOptionHref(option, localeParts),
|
|
394
|
+
localeParts
|
|
395
|
+
);
|
|
396
|
+
},
|
|
397
|
+
viewMoreResults({ query }) {
|
|
398
|
+
return service.submitProductSearch(query);
|
|
399
|
+
},
|
|
400
|
+
getRecentSearches() {
|
|
401
|
+
return getRecentHeaderSearches(localeParts, storage);
|
|
402
|
+
},
|
|
403
|
+
clearRecentSearches() {
|
|
404
|
+
clearRecentHeaderSearches(localeParts, storage);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
return service;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// node_modules/eventsource-parser/dist/errors.js
|
|
411
|
+
var ParseError = class extends Error {
|
|
412
|
+
constructor(message, options) {
|
|
413
|
+
super(message);
|
|
414
|
+
this.name = "ParseError";
|
|
415
|
+
this.type = options.type;
|
|
416
|
+
this.field = options.field;
|
|
417
|
+
this.value = options.value;
|
|
418
|
+
this.line = options.line;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// node_modules/eventsource-parser/dist/parse.js
|
|
423
|
+
var LF = 10;
|
|
424
|
+
var CR = 13;
|
|
425
|
+
var SPACE = 32;
|
|
426
|
+
var MAX_FIELD_PREFIX_LENGTH = 6;
|
|
427
|
+
function noop(_arg) {
|
|
428
|
+
}
|
|
429
|
+
function createParser(config) {
|
|
430
|
+
if (typeof config === "function") {
|
|
431
|
+
throw new TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");
|
|
432
|
+
}
|
|
433
|
+
const { onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize } = config;
|
|
434
|
+
const pendingFragments = [];
|
|
435
|
+
let pendingFragmentsLength = 0;
|
|
436
|
+
let isFirstChunk = true;
|
|
437
|
+
let id;
|
|
438
|
+
let data = "";
|
|
439
|
+
let dataLines = 0;
|
|
440
|
+
let eventType;
|
|
441
|
+
let terminated = false;
|
|
442
|
+
let skippingLine = false;
|
|
443
|
+
let skipNextLineFeed = false;
|
|
444
|
+
function feed(chunk) {
|
|
445
|
+
if (terminated) {
|
|
446
|
+
throw new Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");
|
|
447
|
+
}
|
|
448
|
+
if (isFirstChunk) {
|
|
449
|
+
isFirstChunk = false;
|
|
450
|
+
if (chunk.charCodeAt(0) === 65279) {
|
|
451
|
+
chunk = chunk.slice(1);
|
|
452
|
+
} else if (chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191) {
|
|
453
|
+
chunk = chunk.slice(3);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (skippingLine || skipNextLineFeed) {
|
|
457
|
+
chunk = resumeAfterSkip(chunk);
|
|
458
|
+
if (!chunk) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!pendingFragments.length) {
|
|
463
|
+
const trailing = processLines(chunk);
|
|
464
|
+
if (trailing !== "") {
|
|
465
|
+
storeTrailing(trailing);
|
|
466
|
+
}
|
|
467
|
+
checkBufferSize();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (chunk.indexOf("\n") === -1 && chunk.indexOf("\r") === -1) {
|
|
471
|
+
if (pendingFragmentsLength < MAX_FIELD_PREFIX_LENGTH) {
|
|
472
|
+
const head = pendingFragments.join("") + chunk.slice(0, MAX_FIELD_PREFIX_LENGTH - pendingFragmentsLength);
|
|
473
|
+
if (!shouldBufferTrailing(head)) {
|
|
474
|
+
pendingFragments.length = 0;
|
|
475
|
+
pendingFragmentsLength = 0;
|
|
476
|
+
skippingLine = true;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
pendingFragments.push(chunk);
|
|
481
|
+
pendingFragmentsLength += chunk.length;
|
|
482
|
+
checkBufferSize();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
pendingFragments.push(chunk);
|
|
486
|
+
const input = pendingFragments.join("");
|
|
487
|
+
pendingFragments.length = 0;
|
|
488
|
+
pendingFragmentsLength = 0;
|
|
489
|
+
storeTrailing(processLines(input));
|
|
490
|
+
checkBufferSize();
|
|
491
|
+
}
|
|
492
|
+
function resumeAfterSkip(chunk) {
|
|
493
|
+
if (chunk.length === 0) {
|
|
494
|
+
return chunk;
|
|
495
|
+
}
|
|
496
|
+
if (skipNextLineFeed) {
|
|
497
|
+
skipNextLineFeed = false;
|
|
498
|
+
return chunk.charCodeAt(0) === LF ? chunk.slice(1) : chunk;
|
|
499
|
+
}
|
|
500
|
+
const crIndex = chunk.indexOf("\r");
|
|
501
|
+
const lfIndex = chunk.indexOf("\n");
|
|
502
|
+
const lineEnd = crIndex === -1 ? lfIndex : lfIndex === -1 ? crIndex : crIndex < lfIndex ? crIndex : lfIndex;
|
|
503
|
+
if (lineEnd === -1) {
|
|
504
|
+
return "";
|
|
505
|
+
}
|
|
506
|
+
if (lineEnd === chunk.length - 1 && chunk.charCodeAt(lineEnd) === CR) {
|
|
507
|
+
skippingLine = false;
|
|
508
|
+
skipNextLineFeed = true;
|
|
509
|
+
return "";
|
|
510
|
+
}
|
|
511
|
+
skippingLine = false;
|
|
512
|
+
return chunk.slice(lineEnd + (chunk.charCodeAt(lineEnd) === CR && chunk.charCodeAt(lineEnd + 1) === LF ? 2 : 1));
|
|
513
|
+
}
|
|
514
|
+
function storeTrailing(trailing) {
|
|
515
|
+
if (!trailing)
|
|
516
|
+
return;
|
|
517
|
+
if (trailing.charCodeAt(trailing.length - 1) === CR) {
|
|
518
|
+
parseLine(trailing, 0, trailing.length - 1);
|
|
519
|
+
skipNextLineFeed = true;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (shouldBufferTrailing(trailing)) {
|
|
523
|
+
pendingFragments.push(trailing);
|
|
524
|
+
pendingFragmentsLength = trailing.length;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
skippingLine = true;
|
|
528
|
+
}
|
|
529
|
+
function shouldBufferTrailing(trailing) {
|
|
530
|
+
const firstCharCode = trailing.charCodeAt(0);
|
|
531
|
+
return firstCharCode === 58 && !!onComment || firstCharCode === 100 && isPotentialField(trailing, "data") || firstCharCode === 101 && isPotentialField(trailing, "event") || firstCharCode === 105 && isPotentialField(trailing, "id") || firstCharCode === 114 && isPotentialField(trailing, "retry");
|
|
532
|
+
}
|
|
533
|
+
function checkBufferSize() {
|
|
534
|
+
if (maxBufferSize === void 0)
|
|
535
|
+
return;
|
|
536
|
+
if (pendingFragmentsLength + data.length <= maxBufferSize)
|
|
537
|
+
return;
|
|
538
|
+
terminated = true;
|
|
539
|
+
pendingFragments.length = 0;
|
|
540
|
+
pendingFragmentsLength = 0;
|
|
541
|
+
id = void 0;
|
|
542
|
+
data = "";
|
|
543
|
+
dataLines = 0;
|
|
544
|
+
eventType = void 0;
|
|
545
|
+
skippingLine = false;
|
|
546
|
+
skipNextLineFeed = false;
|
|
547
|
+
onError(new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {
|
|
548
|
+
type: "max-buffer-size-exceeded"
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
function processLines(chunk) {
|
|
552
|
+
let searchIndex = 0;
|
|
553
|
+
if (chunk.indexOf("\r") === -1) {
|
|
554
|
+
let lfIndex = chunk.indexOf("\n", searchIndex);
|
|
555
|
+
while (lfIndex !== -1) {
|
|
556
|
+
if (searchIndex === lfIndex) {
|
|
557
|
+
if (dataLines > 0) {
|
|
558
|
+
onEvent({ id, event: eventType, data });
|
|
559
|
+
}
|
|
560
|
+
id = void 0;
|
|
561
|
+
data = "";
|
|
562
|
+
dataLines = 0;
|
|
563
|
+
eventType = void 0;
|
|
564
|
+
searchIndex = lfIndex + 1;
|
|
565
|
+
lfIndex = chunk.indexOf("\n", searchIndex);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const firstCharCode = chunk.charCodeAt(searchIndex);
|
|
569
|
+
if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
|
|
570
|
+
const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5;
|
|
571
|
+
const value = chunk.slice(valueStart, lfIndex);
|
|
572
|
+
if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
|
|
573
|
+
onEvent({ id, event: eventType, data: value });
|
|
574
|
+
id = void 0;
|
|
575
|
+
data = "";
|
|
576
|
+
eventType = void 0;
|
|
577
|
+
searchIndex = lfIndex + 2;
|
|
578
|
+
lfIndex = chunk.indexOf("\n", searchIndex);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
data = dataLines === 0 ? value : `${data}
|
|
582
|
+
${value}`;
|
|
583
|
+
dataLines++;
|
|
584
|
+
} else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {
|
|
585
|
+
eventType = chunk.slice(chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, lfIndex) || void 0;
|
|
586
|
+
} else {
|
|
587
|
+
parseLine(chunk, searchIndex, lfIndex);
|
|
588
|
+
}
|
|
589
|
+
searchIndex = lfIndex + 1;
|
|
590
|
+
lfIndex = chunk.indexOf("\n", searchIndex);
|
|
591
|
+
}
|
|
592
|
+
return chunk.slice(searchIndex);
|
|
593
|
+
}
|
|
594
|
+
while (searchIndex < chunk.length) {
|
|
595
|
+
const crIndex = chunk.indexOf("\r", searchIndex);
|
|
596
|
+
const lfIndex = chunk.indexOf("\n", searchIndex);
|
|
597
|
+
let lineEnd = -1;
|
|
598
|
+
if (crIndex !== -1 && lfIndex !== -1) {
|
|
599
|
+
lineEnd = crIndex < lfIndex ? crIndex : lfIndex;
|
|
600
|
+
} else if (crIndex !== -1) {
|
|
601
|
+
if (crIndex === chunk.length - 1) {
|
|
602
|
+
lineEnd = -1;
|
|
603
|
+
} else {
|
|
604
|
+
lineEnd = crIndex;
|
|
605
|
+
}
|
|
606
|
+
} else if (lfIndex !== -1) {
|
|
607
|
+
lineEnd = lfIndex;
|
|
608
|
+
}
|
|
609
|
+
if (lineEnd === -1) {
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
parseLine(chunk, searchIndex, lineEnd);
|
|
613
|
+
searchIndex = lineEnd + 1;
|
|
614
|
+
if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {
|
|
615
|
+
searchIndex++;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return chunk.slice(searchIndex);
|
|
619
|
+
}
|
|
620
|
+
function parseLine(chunk, start, end) {
|
|
621
|
+
if (start === end) {
|
|
622
|
+
dispatchEvent();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const firstCharCode = chunk.charCodeAt(start);
|
|
626
|
+
if (isDataPrefix(chunk, start, firstCharCode)) {
|
|
627
|
+
const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5;
|
|
628
|
+
const value2 = chunk.slice(valueStart, end);
|
|
629
|
+
data = dataLines === 0 ? value2 : `${data}
|
|
630
|
+
${value2}`;
|
|
631
|
+
dataLines++;
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (isEventPrefix(chunk, start, firstCharCode)) {
|
|
635
|
+
eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
|
|
639
|
+
const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
|
|
640
|
+
if (!value2.includes("\0"))
|
|
641
|
+
id = value2;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (firstCharCode === 58) {
|
|
645
|
+
if (onComment) {
|
|
646
|
+
const line2 = chunk.slice(start, end);
|
|
647
|
+
onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const line = chunk.slice(start, end);
|
|
652
|
+
const fieldSeparatorIndex = line.indexOf(":");
|
|
653
|
+
if (fieldSeparatorIndex === -1) {
|
|
654
|
+
processField(line, "", line);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const field = line.slice(0, fieldSeparatorIndex);
|
|
658
|
+
const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1;
|
|
659
|
+
const value = line.slice(fieldSeparatorIndex + offset);
|
|
660
|
+
processField(field, value, line);
|
|
661
|
+
}
|
|
662
|
+
function processField(field, value, line) {
|
|
663
|
+
switch (field) {
|
|
664
|
+
case "event":
|
|
665
|
+
eventType = value || void 0;
|
|
666
|
+
break;
|
|
667
|
+
case "data":
|
|
668
|
+
data = dataLines === 0 ? value : `${data}
|
|
669
|
+
${value}`;
|
|
670
|
+
dataLines++;
|
|
671
|
+
break;
|
|
672
|
+
case "id":
|
|
673
|
+
if (!value.includes("\0"))
|
|
674
|
+
id = value;
|
|
675
|
+
break;
|
|
676
|
+
case "retry":
|
|
677
|
+
if (/^\d+$/.test(value)) {
|
|
678
|
+
onRetry(parseInt(value, 10));
|
|
679
|
+
} else {
|
|
680
|
+
onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
|
|
681
|
+
type: "invalid-retry",
|
|
682
|
+
value,
|
|
683
|
+
line
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
687
|
+
default:
|
|
688
|
+
onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`, { type: "unknown-field", field, value, line }));
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function dispatchEvent() {
|
|
693
|
+
if (dataLines > 0) {
|
|
694
|
+
onEvent({
|
|
695
|
+
id,
|
|
696
|
+
event: eventType,
|
|
697
|
+
data
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
id = void 0;
|
|
701
|
+
data = "";
|
|
702
|
+
dataLines = 0;
|
|
703
|
+
eventType = void 0;
|
|
704
|
+
}
|
|
705
|
+
function reset(options = {}) {
|
|
706
|
+
if (options.consume && pendingFragments.length > 0) {
|
|
707
|
+
const incompleteLine = pendingFragments.join("");
|
|
708
|
+
parseLine(incompleteLine, 0, incompleteLine.length);
|
|
709
|
+
}
|
|
710
|
+
isFirstChunk = true;
|
|
711
|
+
id = void 0;
|
|
712
|
+
data = "";
|
|
713
|
+
dataLines = 0;
|
|
714
|
+
eventType = void 0;
|
|
715
|
+
pendingFragments.length = 0;
|
|
716
|
+
pendingFragmentsLength = 0;
|
|
717
|
+
terminated = false;
|
|
718
|
+
skippingLine = false;
|
|
719
|
+
skipNextLineFeed = false;
|
|
720
|
+
}
|
|
721
|
+
return { feed, reset };
|
|
722
|
+
}
|
|
723
|
+
function isDataPrefix(chunk, i, firstCharCode) {
|
|
724
|
+
return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
|
|
725
|
+
}
|
|
726
|
+
function isEventPrefix(chunk, i, firstCharCode) {
|
|
727
|
+
return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
|
|
728
|
+
}
|
|
729
|
+
function isPotentialField(line, field) {
|
|
730
|
+
let i = 1;
|
|
731
|
+
while (i < line.length && i < field.length) {
|
|
732
|
+
if (line.charCodeAt(i) !== field.charCodeAt(i)) {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
i++;
|
|
736
|
+
}
|
|
737
|
+
return line.length <= field.length || line.charCodeAt(field.length) === 58;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/services/ChatService.ts
|
|
741
|
+
var PRODUCTS_OPENING_FENCE = "```products";
|
|
742
|
+
var MARKDOWN_CLOSING_FENCE = "\n```";
|
|
743
|
+
function findProductsBlockStart(content, openingFenceIndex) {
|
|
744
|
+
let blockStart = openingFenceIndex + PRODUCTS_OPENING_FENCE.length;
|
|
745
|
+
while (blockStart < content.length && (content[blockStart] === " " || content[blockStart] === " " || content[blockStart] === "\r")) {
|
|
746
|
+
blockStart += 1;
|
|
747
|
+
}
|
|
748
|
+
return content[blockStart] === "\n" ? blockStart + 1 : null;
|
|
749
|
+
}
|
|
750
|
+
function isStandaloneClosingFence(content, fenceEnd) {
|
|
751
|
+
const lineEnd = content.indexOf("\n", fenceEnd);
|
|
752
|
+
const suffixEnd = lineEnd === -1 ? content.length : lineEnd;
|
|
753
|
+
return content.slice(fenceEnd, suffixEnd).trim() === "";
|
|
754
|
+
}
|
|
755
|
+
function findClosingFence(content, fromIndex) {
|
|
756
|
+
let candidate = content.indexOf(MARKDOWN_CLOSING_FENCE, fromIndex);
|
|
757
|
+
while (candidate !== -1) {
|
|
758
|
+
const fenceEnd = candidate + MARKDOWN_CLOSING_FENCE.length;
|
|
759
|
+
if (isStandaloneClosingFence(content, fenceEnd)) {
|
|
760
|
+
return candidate;
|
|
761
|
+
}
|
|
762
|
+
candidate = content.indexOf(MARKDOWN_CLOSING_FENCE, fenceEnd);
|
|
763
|
+
}
|
|
764
|
+
return -1;
|
|
765
|
+
}
|
|
766
|
+
function extractProductsFromMarkdown(content) {
|
|
767
|
+
const products = [];
|
|
768
|
+
const summaryParts = [];
|
|
769
|
+
let summaryCursor = 0;
|
|
770
|
+
let searchCursor = 0;
|
|
771
|
+
while (searchCursor < content.length) {
|
|
772
|
+
const start = content.indexOf(PRODUCTS_OPENING_FENCE, searchCursor);
|
|
773
|
+
if (start === -1) break;
|
|
774
|
+
const blockStart = findProductsBlockStart(content, start);
|
|
775
|
+
if (blockStart === null) {
|
|
776
|
+
searchCursor = start + PRODUCTS_OPENING_FENCE.length;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const nextOpeningFence = content.indexOf(
|
|
780
|
+
PRODUCTS_OPENING_FENCE,
|
|
781
|
+
blockStart
|
|
782
|
+
);
|
|
783
|
+
const end = findClosingFence(content, blockStart);
|
|
784
|
+
if (end === -1) break;
|
|
785
|
+
if (nextOpeningFence !== -1 && nextOpeningFence < end) {
|
|
786
|
+
searchCursor = nextOpeningFence;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
const parsed = JSON.parse(content.slice(blockStart, end));
|
|
791
|
+
if (Array.isArray(parsed)) {
|
|
792
|
+
products.push(...parsed);
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
summaryParts.push(content.slice(summaryCursor, start));
|
|
797
|
+
summaryCursor = end + MARKDOWN_CLOSING_FENCE.length;
|
|
798
|
+
searchCursor = summaryCursor;
|
|
799
|
+
}
|
|
800
|
+
summaryParts.push(content.slice(summaryCursor));
|
|
801
|
+
return { summary: summaryParts.join("").trim(), products };
|
|
802
|
+
}
|
|
803
|
+
function createChatService(options) {
|
|
804
|
+
const {
|
|
805
|
+
conversationsUrl = DEFAULT_AI_CHAT_CONVERSATIONS_URL,
|
|
806
|
+
messagesUrl = DEFAULT_AI_CHAT_MESSAGES_URL,
|
|
807
|
+
tenantId,
|
|
808
|
+
getAuthToken,
|
|
809
|
+
fetchImpl: fetchImplOption
|
|
810
|
+
} = options;
|
|
811
|
+
let fetchImpl;
|
|
812
|
+
if (fetchImplOption !== void 0) {
|
|
813
|
+
fetchImpl = fetchImplOption;
|
|
814
|
+
} else if (typeof fetch === "function") {
|
|
815
|
+
fetchImpl = fetch.bind(globalThis);
|
|
816
|
+
} else {
|
|
817
|
+
fetchImpl = () => {
|
|
818
|
+
throw new Error("ChatService: no fetch implementation available");
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function buildHeaders(extra) {
|
|
822
|
+
const headers = {
|
|
823
|
+
"Content-Type": "application/json",
|
|
824
|
+
...extra
|
|
825
|
+
};
|
|
826
|
+
if (tenantId) headers["X-Tenant-Id"] = tenantId;
|
|
827
|
+
const token = getAuthToken?.();
|
|
828
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
829
|
+
return headers;
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
async createConversation(title = "Search Session") {
|
|
833
|
+
const res = await fetchImpl(conversationsUrl, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
headers: buildHeaders(),
|
|
836
|
+
body: JSON.stringify({ title })
|
|
837
|
+
});
|
|
838
|
+
if (!res.ok) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`ChatService.createConversation: ${res.status} ${res.statusText}`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const data = await res.json();
|
|
844
|
+
const id = data["id"] ?? data["conversation_id"];
|
|
845
|
+
if (typeof id !== "number") {
|
|
846
|
+
throw new TypeError(
|
|
847
|
+
"ChatService.createConversation: response missing numeric id"
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
return id;
|
|
851
|
+
},
|
|
852
|
+
async sendMessage(conversationId, content, onChunk) {
|
|
853
|
+
const res = await fetchImpl(messagesUrl, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
headers: buildHeaders({ Accept: "application/json" }),
|
|
856
|
+
body: JSON.stringify({
|
|
857
|
+
conversation_id: conversationId,
|
|
858
|
+
content,
|
|
859
|
+
compact: false
|
|
860
|
+
})
|
|
861
|
+
});
|
|
862
|
+
if (!res.ok) {
|
|
863
|
+
throw new Error(
|
|
864
|
+
`ChatService.sendMessage: ${res.status} ${res.statusText}`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
let accumulated = "";
|
|
868
|
+
const parser = createParser({
|
|
869
|
+
onEvent(event) {
|
|
870
|
+
const data = event.data;
|
|
871
|
+
if (!data || data === "[DONE]") return;
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(data);
|
|
874
|
+
const parsedType = parsed["type"];
|
|
875
|
+
const type = typeof parsedType === "string" ? parsedType : "";
|
|
876
|
+
if (type === "content" && typeof parsed["text"] === "string") {
|
|
877
|
+
const text = parsed["text"];
|
|
878
|
+
accumulated += text;
|
|
879
|
+
onChunk?.(type, text);
|
|
880
|
+
} else if (type === "status" && typeof parsed["message"] === "string") {
|
|
881
|
+
const message = parsed["message"];
|
|
882
|
+
onChunk?.(type, message);
|
|
883
|
+
}
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
const body = res.body;
|
|
889
|
+
if (body && typeof body.getReader === "function") {
|
|
890
|
+
const reader = body.getReader();
|
|
891
|
+
const decoder = new TextDecoder();
|
|
892
|
+
while (true) {
|
|
893
|
+
const { value, done } = await reader.read();
|
|
894
|
+
if (done) break;
|
|
895
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
896
|
+
}
|
|
897
|
+
parser.feed(decoder.decode());
|
|
898
|
+
} else {
|
|
899
|
+
const rawBody = await res.text();
|
|
900
|
+
parser.feed(rawBody);
|
|
901
|
+
}
|
|
902
|
+
const { summary, products } = extractProductsFromMarkdown(accumulated);
|
|
903
|
+
return { summary, products, rawContent: accumulated };
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/utils/catalogHelpers.ts
|
|
909
|
+
var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
|
|
910
|
+
"discontinued",
|
|
911
|
+
"preview",
|
|
912
|
+
"previewproduct",
|
|
913
|
+
"test",
|
|
914
|
+
"stopped",
|
|
915
|
+
"replacement",
|
|
916
|
+
"bundleonly",
|
|
917
|
+
"inactive",
|
|
918
|
+
"notavailable"
|
|
919
|
+
]);
|
|
920
|
+
function normalizeProductStatusValue(value) {
|
|
921
|
+
return String(value ?? "").trim().toLowerCase().replace(/\s+/g, "");
|
|
922
|
+
}
|
|
923
|
+
function isSellableProductStatus(productStatus) {
|
|
924
|
+
return normalizeProductStatusValue(productStatus) === "sellable";
|
|
925
|
+
}
|
|
926
|
+
function resolveSkuStatusValues(suggestionOption) {
|
|
927
|
+
return [
|
|
928
|
+
suggestionOption["productStatus"],
|
|
929
|
+
suggestionOption["product_status"],
|
|
930
|
+
suggestionOption["status"],
|
|
931
|
+
suggestionOption["sku_status"]
|
|
932
|
+
].map(normalizeProductStatusValue).filter(Boolean);
|
|
933
|
+
}
|
|
934
|
+
function isNonSellableStatusValue(statusValue) {
|
|
935
|
+
if (CART_DISABLED_PRODUCT_STATUSES.has(statusValue)) {
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
return statusValue !== "sellable" && statusValue !== "active";
|
|
939
|
+
}
|
|
940
|
+
function isCartActionDisabled(suggestionOption) {
|
|
941
|
+
if (!suggestionOption) {
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
const outOfStock = suggestionOption["out_of_stock"] ?? suggestionOption["outOfStock"];
|
|
945
|
+
if (outOfStock) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
const statusValues = resolveSkuStatusValues(suggestionOption);
|
|
949
|
+
if (statusValues.length === 0) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
return statusValues.some(isNonSellableStatusValue);
|
|
953
|
+
}
|
|
954
|
+
function isSuggestionDisabled(suggestionOption) {
|
|
955
|
+
return Boolean(
|
|
956
|
+
!suggestionOption || suggestionOption["out_of_stock"] || suggestionOption["outOfStock"]
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
function getImageFromProperty(properties = {}) {
|
|
960
|
+
let image = String(properties["primaryimage"] ?? "").trim() || String(properties["imageURL"] ?? "").trim();
|
|
961
|
+
if (!image) {
|
|
962
|
+
try {
|
|
963
|
+
const parsed = JSON.parse(
|
|
964
|
+
String(properties["productImages"] ?? "{}")
|
|
965
|
+
);
|
|
966
|
+
if (Array.isArray(parsed) && parsed[0] && typeof parsed[0] === "object") {
|
|
967
|
+
image = String(
|
|
968
|
+
parsed[0]["url"] ?? ""
|
|
969
|
+
).trim();
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
image = "";
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return image;
|
|
976
|
+
}
|
|
977
|
+
function resolveSuggestionImageSrc(imageSrc) {
|
|
978
|
+
const trimmed = typeof imageSrc === "string" ? imageSrc.trim() : String(imageSrc ?? "").trim();
|
|
979
|
+
return trimmed;
|
|
980
|
+
}
|
|
981
|
+
function safeParseJsonArray(value) {
|
|
982
|
+
if (Array.isArray(value)) {
|
|
983
|
+
return value;
|
|
984
|
+
}
|
|
985
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
986
|
+
return [];
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
const parsed = JSON.parse(value);
|
|
990
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
991
|
+
} catch {
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function getIngredientTerms(rawIngredients) {
|
|
996
|
+
const ingredientGroups = safeParseJsonArray(rawIngredients);
|
|
997
|
+
return ingredientGroups.flatMap((ingredientGroup) => {
|
|
998
|
+
if (!ingredientGroup || typeof ingredientGroup !== "object") {
|
|
999
|
+
return [];
|
|
1000
|
+
}
|
|
1001
|
+
const group = ingredientGroup;
|
|
1002
|
+
const keyIngredients = Array.isArray(group["keyIngredients"]) ? group["keyIngredients"].map((ingredient) => ingredient["name"]) : [];
|
|
1003
|
+
return [
|
|
1004
|
+
group["productName"],
|
|
1005
|
+
group["activeIngredients"],
|
|
1006
|
+
group["inactiveIngredients"],
|
|
1007
|
+
group["allIngredients"],
|
|
1008
|
+
group["otherIngredients"],
|
|
1009
|
+
...keyIngredients
|
|
1010
|
+
];
|
|
1011
|
+
}).filter(Boolean).map((term) => String(term).trim()).filter(Boolean);
|
|
1012
|
+
}
|
|
1013
|
+
function getSkuProperties(product) {
|
|
1014
|
+
const skuList = Array.isArray(product["sku"]) ? product["sku"] : [];
|
|
1015
|
+
const defaultSku = skuList.find((skuEntry) => skuEntry?.["default"] === true) ?? skuList[0] ?? null;
|
|
1016
|
+
return {
|
|
1017
|
+
defaultSku: defaultSku ?? null,
|
|
1018
|
+
skuProperties: defaultSku?.["properties"] ?? {}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function getIdentifier(product, defaultSku, index) {
|
|
1022
|
+
return String(
|
|
1023
|
+
defaultSku?.["identifier"] ?? product["identifier"] ?? `catalog-product-${index}`
|
|
1024
|
+
).trim();
|
|
1025
|
+
}
|
|
1026
|
+
function getDisplayLabel(identifier, productProperties, skuProperties) {
|
|
1027
|
+
return String(
|
|
1028
|
+
skuProperties["title"] ?? productProperties["name"] ?? productProperties["title"] ?? skuProperties["name"] ?? identifier
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
function getTypeData(product, productProperties, skuProperties) {
|
|
1032
|
+
const customProductType = String(
|
|
1033
|
+
productProperties["customProductType"] ?? skuProperties["customProductType"] ?? ""
|
|
1034
|
+
).trim();
|
|
1035
|
+
const normalizedType = String(
|
|
1036
|
+
product["type"] ?? productProperties["producttype"] ?? skuProperties["producttype"] ?? ""
|
|
1037
|
+
).trim().toLowerCase();
|
|
1038
|
+
return {
|
|
1039
|
+
customProductType,
|
|
1040
|
+
isBundle: normalizedType === "bundle" || customProductType.toLowerCase().includes("bundle")
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function getNavigationUrls(productProperties, skuProperties) {
|
|
1044
|
+
const slug = String(
|
|
1045
|
+
skuProperties["slug"] ?? productProperties["slug"] ?? ""
|
|
1046
|
+
).trim() || null;
|
|
1047
|
+
const navigableUrl = String(
|
|
1048
|
+
skuProperties["url"] ?? skuProperties["href"] ?? productProperties["url"] ?? productProperties["href"] ?? ""
|
|
1049
|
+
).trim() || null;
|
|
1050
|
+
return { navigableUrl, slug };
|
|
1051
|
+
}
|
|
1052
|
+
function isOutOfStock(inventoryStatus) {
|
|
1053
|
+
return String(inventoryStatus).toUpperCase() === "OUT OF STOCK";
|
|
1054
|
+
}
|
|
1055
|
+
function getCatalogStatusFields(skuProperties, productProperties) {
|
|
1056
|
+
const inventoryStatus = skuProperties["inventoryStatus"] ?? productProperties["inventoryStatus"] ?? "";
|
|
1057
|
+
const productStatus = skuProperties["productStatus"] ?? productProperties["productStatus"] ?? "";
|
|
1058
|
+
const skuStatus = skuProperties["status"] ?? productProperties["status"] ?? "";
|
|
1059
|
+
const normalizedProductStatus = String(productStatus).trim();
|
|
1060
|
+
const normalizedSkuStatus = String(skuStatus).trim();
|
|
1061
|
+
const outOfStock = isOutOfStock(inventoryStatus);
|
|
1062
|
+
return {
|
|
1063
|
+
normalizedProductStatus,
|
|
1064
|
+
normalizedSkuStatus,
|
|
1065
|
+
outOfStock,
|
|
1066
|
+
cartActionDisabled: isCartActionDisabled({
|
|
1067
|
+
outOfStock,
|
|
1068
|
+
productStatus: normalizedProductStatus || void 0,
|
|
1069
|
+
status: normalizedSkuStatus || void 0
|
|
1070
|
+
})
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function optionalNonEmptyString(value) {
|
|
1074
|
+
const normalized = String(value ?? "").trim();
|
|
1075
|
+
return normalized || void 0;
|
|
1076
|
+
}
|
|
1077
|
+
function getProductImageSrc(opts) {
|
|
1078
|
+
const normalizedType = String(opts.product["type"] ?? "").trim().toLowerCase();
|
|
1079
|
+
if (normalizedType === "kit" || normalizedType === "bundle") {
|
|
1080
|
+
return getImageFromProperty(opts.productProperties);
|
|
1081
|
+
}
|
|
1082
|
+
return getImageFromProperty(opts.skuProperties);
|
|
1083
|
+
}
|
|
1084
|
+
function mapCatalogProductToSuggestion(product, index = 0) {
|
|
1085
|
+
if (!product || typeof product !== "object") {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
const p = product;
|
|
1089
|
+
const productProperties = p["properties"] ?? {};
|
|
1090
|
+
const { defaultSku, skuProperties } = getSkuProperties(p);
|
|
1091
|
+
const identifier = getIdentifier(p, defaultSku, index);
|
|
1092
|
+
const { customProductType, isBundle } = getTypeData(
|
|
1093
|
+
p,
|
|
1094
|
+
productProperties,
|
|
1095
|
+
skuProperties
|
|
1096
|
+
);
|
|
1097
|
+
const { navigableUrl, slug } = getNavigationUrls(
|
|
1098
|
+
productProperties,
|
|
1099
|
+
skuProperties
|
|
1100
|
+
);
|
|
1101
|
+
const statusFields = getCatalogStatusFields(
|
|
1102
|
+
skuProperties,
|
|
1103
|
+
productProperties
|
|
1104
|
+
);
|
|
1105
|
+
const imageSrc = resolveSuggestionImageSrc(
|
|
1106
|
+
getProductImageSrc({ product: p, productProperties, skuProperties })
|
|
1107
|
+
);
|
|
1108
|
+
const mappedProductType = isBundle ? "bundle" : "single";
|
|
1109
|
+
return {
|
|
1110
|
+
id: identifier,
|
|
1111
|
+
identifier,
|
|
1112
|
+
sku: identifier,
|
|
1113
|
+
label: getDisplayLabel(identifier, productProperties, skuProperties),
|
|
1114
|
+
value: getDisplayLabel(identifier, productProperties, skuProperties),
|
|
1115
|
+
imageURL: imageSrc,
|
|
1116
|
+
thumbnail: imageSrc,
|
|
1117
|
+
productType: mappedProductType,
|
|
1118
|
+
product_type: mappedProductType,
|
|
1119
|
+
outOfStock: statusFields.outOfStock,
|
|
1120
|
+
productStatus: optionalNonEmptyString(
|
|
1121
|
+
statusFields.normalizedProductStatus
|
|
1122
|
+
),
|
|
1123
|
+
status: optionalNonEmptyString(statusFields.normalizedSkuStatus),
|
|
1124
|
+
cartActionDisabled: statusFields.cartActionDisabled,
|
|
1125
|
+
customProductType,
|
|
1126
|
+
slug,
|
|
1127
|
+
productSlug: slug,
|
|
1128
|
+
url: navigableUrl,
|
|
1129
|
+
href: navigableUrl,
|
|
1130
|
+
keywords: String(
|
|
1131
|
+
productProperties["keyword"] ?? skuProperties["keyword"] ?? ""
|
|
1132
|
+
),
|
|
1133
|
+
ingredientTerms: getIngredientTerms(
|
|
1134
|
+
skuProperties["ingredients"] ?? productProperties["ingredients"]
|
|
1135
|
+
)
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function normalizeDidYouMean(didYouMean) {
|
|
1139
|
+
if (!Array.isArray(didYouMean)) {
|
|
1140
|
+
return [];
|
|
1141
|
+
}
|
|
1142
|
+
return didYouMean.map((item, index) => {
|
|
1143
|
+
if (typeof item === "string") {
|
|
1144
|
+
return {
|
|
1145
|
+
id: `did-you-mean-${index}-${item}`,
|
|
1146
|
+
value: item,
|
|
1147
|
+
suggestionType: "did-you-mean"
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (item && typeof item === "object") {
|
|
1151
|
+
const obj = item;
|
|
1152
|
+
return {
|
|
1153
|
+
...obj,
|
|
1154
|
+
id: String(
|
|
1155
|
+
obj["id"] ?? `did-you-mean-${index}-${obj["value"] ?? obj["label"] ?? ""}`
|
|
1156
|
+
),
|
|
1157
|
+
value: String(obj["value"] ?? ""),
|
|
1158
|
+
suggestionType: "did-you-mean"
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1162
|
+
}).filter((item) => item !== null);
|
|
1163
|
+
}
|
|
1164
|
+
function getVisibleSuggestions(opts) {
|
|
1165
|
+
if (opts.didYouMean.length > 0) {
|
|
1166
|
+
return opts.didYouMean;
|
|
1167
|
+
}
|
|
1168
|
+
return opts.suggestions;
|
|
1169
|
+
}
|
|
1170
|
+
function getSeeMoreResultsPayload(query, totalResultsCount, suggestions) {
|
|
1171
|
+
return {
|
|
1172
|
+
query,
|
|
1173
|
+
source: "autocomplete-footer",
|
|
1174
|
+
totalResultsCount,
|
|
1175
|
+
visibleResultsCount: suggestions.length
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/hooks/useSearchSuggestions.ts
|
|
1180
|
+
function normalizeSuggestionsResponse(response) {
|
|
1181
|
+
if (Array.isArray(response)) {
|
|
1182
|
+
return {
|
|
1183
|
+
suggestions: response,
|
|
1184
|
+
didYouMean: []
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (!response || typeof response !== "object") {
|
|
1188
|
+
return { suggestions: [], didYouMean: [] };
|
|
1189
|
+
}
|
|
1190
|
+
const r = response;
|
|
1191
|
+
if (Array.isArray(r.product)) {
|
|
1192
|
+
return {
|
|
1193
|
+
suggestions: r.product.map(
|
|
1194
|
+
(product, index) => mapCatalogProductToSuggestion(product, index)
|
|
1195
|
+
).filter((item) => item !== null),
|
|
1196
|
+
didYouMean: normalizeDidYouMean(r.didYouMean ?? r.did_you_mean)
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
suggestions: Array.isArray(r.suggestions) ? r.suggestions : [],
|
|
1201
|
+
didYouMean: normalizeDidYouMean(r.didYouMean)
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function useSearchSuggestions({
|
|
1205
|
+
service,
|
|
1206
|
+
enabled = true
|
|
1207
|
+
}) {
|
|
1208
|
+
const [query, setQuery] = useState("");
|
|
1209
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
1210
|
+
const [didYouMean, setDidYouMean] = useState([]);
|
|
1211
|
+
const [loading, setLoading] = useState(false);
|
|
1212
|
+
const [emptyFetched, setEmptyFetched] = useState(false);
|
|
1213
|
+
const [hasMoreResults, setHasMoreResults] = useState(false);
|
|
1214
|
+
const [totalResultsCount, setTotalResultsCount] = useState(0);
|
|
1215
|
+
const fetchDebounced = useRef(null);
|
|
1216
|
+
const latestRequestId = useRef(0);
|
|
1217
|
+
const resetSuggestionsState = useCallback(() => {
|
|
1218
|
+
setSuggestions([]);
|
|
1219
|
+
setDidYouMean([]);
|
|
1220
|
+
setEmptyFetched(false);
|
|
1221
|
+
setHasMoreResults(false);
|
|
1222
|
+
setTotalResultsCount(0);
|
|
1223
|
+
setLoading(false);
|
|
1224
|
+
}, []);
|
|
1225
|
+
const updateQuery = useCallback(
|
|
1226
|
+
(nextQuery) => {
|
|
1227
|
+
const hasNextQuery = Boolean(nextQuery.trim());
|
|
1228
|
+
setEmptyFetched(false);
|
|
1229
|
+
setLoading(enabled && hasNextQuery);
|
|
1230
|
+
setQuery(nextQuery);
|
|
1231
|
+
},
|
|
1232
|
+
[enabled]
|
|
1233
|
+
);
|
|
1234
|
+
const clearQuery = useCallback(() => {
|
|
1235
|
+
resetSuggestionsState();
|
|
1236
|
+
setQuery("");
|
|
1237
|
+
}, [resetSuggestionsState]);
|
|
1238
|
+
const runFetch = useCallback(
|
|
1239
|
+
async (nextQuery) => {
|
|
1240
|
+
const trimmedQuery = nextQuery.trim();
|
|
1241
|
+
const requestId = latestRequestId.current;
|
|
1242
|
+
if (!trimmedQuery || !enabled) {
|
|
1243
|
+
resetSuggestionsState();
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
setLoading(true);
|
|
1247
|
+
setEmptyFetched(false);
|
|
1248
|
+
try {
|
|
1249
|
+
const fetchedSuggestions = await service.fetchSuggestions(trimmedQuery);
|
|
1250
|
+
if (requestId !== latestRequestId.current) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const {
|
|
1254
|
+
suggestions: allSuggestions,
|
|
1255
|
+
didYouMean: suggestedTerms
|
|
1256
|
+
} = normalizeSuggestionsResponse(fetchedSuggestions);
|
|
1257
|
+
const visibleSuggestions = allSuggestions.slice(
|
|
1258
|
+
0,
|
|
1259
|
+
MAX_VISIBLE_SUGGESTIONS
|
|
1260
|
+
);
|
|
1261
|
+
const hasDidYouMean = suggestedTerms.length > 0;
|
|
1262
|
+
const normalizedTotal = typeof fetchedSuggestions.totalResultsCount === "number" ? fetchedSuggestions.totalResultsCount : typeof fetchedSuggestions.pageableInfo?.totalCount === "number" ? fetchedSuggestions.pageableInfo.totalCount : allSuggestions.length;
|
|
1263
|
+
const effectiveTotal = hasDidYouMean ? suggestedTerms.length : Math.max(normalizedTotal, allSuggestions.length);
|
|
1264
|
+
setDidYouMean(suggestedTerms);
|
|
1265
|
+
setSuggestions(hasDidYouMean ? [] : visibleSuggestions);
|
|
1266
|
+
setHasMoreResults(
|
|
1267
|
+
!hasDidYouMean && effectiveTotal > MAX_VISIBLE_SUGGESTIONS
|
|
1268
|
+
);
|
|
1269
|
+
setTotalResultsCount(effectiveTotal);
|
|
1270
|
+
setEmptyFetched(
|
|
1271
|
+
hasDidYouMean ? false : visibleSuggestions.length === 0 && suggestedTerms.length === 0
|
|
1272
|
+
);
|
|
1273
|
+
} catch {
|
|
1274
|
+
if (requestId !== latestRequestId.current) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
setSuggestions([]);
|
|
1278
|
+
setDidYouMean([]);
|
|
1279
|
+
setEmptyFetched(true);
|
|
1280
|
+
setHasMoreResults(false);
|
|
1281
|
+
setTotalResultsCount(0);
|
|
1282
|
+
} finally {
|
|
1283
|
+
if (requestId === latestRequestId.current) {
|
|
1284
|
+
setLoading(false);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
[enabled, service, resetSuggestionsState]
|
|
1289
|
+
);
|
|
1290
|
+
useEffect(() => {
|
|
1291
|
+
if (!enabled) {
|
|
1292
|
+
resetSuggestionsState();
|
|
1293
|
+
return void 0;
|
|
1294
|
+
}
|
|
1295
|
+
if (fetchDebounced.current) {
|
|
1296
|
+
clearTimeout(fetchDebounced.current);
|
|
1297
|
+
}
|
|
1298
|
+
latestRequestId.current += 1;
|
|
1299
|
+
fetchDebounced.current = setTimeout(() => {
|
|
1300
|
+
void runFetch(query);
|
|
1301
|
+
}, DEBOUNCE_MS);
|
|
1302
|
+
return () => {
|
|
1303
|
+
if (fetchDebounced.current) {
|
|
1304
|
+
clearTimeout(fetchDebounced.current);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
}, [enabled, query, resetSuggestionsState, runFetch]);
|
|
1308
|
+
return {
|
|
1309
|
+
clearQuery,
|
|
1310
|
+
emptyFetched,
|
|
1311
|
+
hasMoreResults,
|
|
1312
|
+
hasQuery: Boolean(query.trim()),
|
|
1313
|
+
didYouMean,
|
|
1314
|
+
loading,
|
|
1315
|
+
query,
|
|
1316
|
+
setQuery,
|
|
1317
|
+
suggestions,
|
|
1318
|
+
totalResultsCount,
|
|
1319
|
+
updateQuery
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// node_modules/zustand/esm/vanilla.mjs
|
|
1324
|
+
var createStoreImpl = (createState) => {
|
|
1325
|
+
let state;
|
|
1326
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1327
|
+
const setState = (partial, replace) => {
|
|
1328
|
+
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
1329
|
+
if (!Object.is(nextState, state)) {
|
|
1330
|
+
const previousState = state;
|
|
1331
|
+
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
|
|
1332
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
const getState = () => state;
|
|
1336
|
+
const getInitialState = () => initialState;
|
|
1337
|
+
const subscribe = (listener) => {
|
|
1338
|
+
listeners.add(listener);
|
|
1339
|
+
return () => listeners.delete(listener);
|
|
1340
|
+
};
|
|
1341
|
+
const api = { setState, getState, getInitialState, subscribe };
|
|
1342
|
+
const initialState = state = createState(setState, getState, api);
|
|
1343
|
+
return api;
|
|
1344
|
+
};
|
|
1345
|
+
var createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
|
|
1346
|
+
var identity = (arg) => arg;
|
|
1347
|
+
function useStore(api, selector = identity) {
|
|
1348
|
+
const slice = React.useSyncExternalStore(
|
|
1349
|
+
api.subscribe,
|
|
1350
|
+
() => selector(api.getState()),
|
|
1351
|
+
() => selector(api.getInitialState())
|
|
1352
|
+
);
|
|
1353
|
+
React.useDebugValue(slice);
|
|
1354
|
+
return slice;
|
|
1355
|
+
}
|
|
1356
|
+
var createImpl = (createState) => {
|
|
1357
|
+
const api = createStore(createState);
|
|
1358
|
+
const useBoundStore = (selector) => useStore(api, selector);
|
|
1359
|
+
Object.assign(useBoundStore, api);
|
|
1360
|
+
return useBoundStore;
|
|
1361
|
+
};
|
|
1362
|
+
var create = (createState) => createImpl;
|
|
1363
|
+
|
|
1364
|
+
// node_modules/zustand/esm/middleware.mjs
|
|
1365
|
+
function createJSONStorage(getStorage, options) {
|
|
1366
|
+
let storage;
|
|
1367
|
+
try {
|
|
1368
|
+
storage = getStorage();
|
|
1369
|
+
} catch (e) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const persistStorage = {
|
|
1373
|
+
getItem: (name) => {
|
|
1374
|
+
var _a;
|
|
1375
|
+
const parse = (str2) => {
|
|
1376
|
+
if (str2 === null) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
return JSON.parse(str2, void 0 );
|
|
1380
|
+
};
|
|
1381
|
+
const str = (_a = storage.getItem(name)) != null ? _a : null;
|
|
1382
|
+
if (str instanceof Promise) {
|
|
1383
|
+
return str.then(parse);
|
|
1384
|
+
}
|
|
1385
|
+
return parse(str);
|
|
1386
|
+
},
|
|
1387
|
+
setItem: (name, newValue) => storage.setItem(
|
|
1388
|
+
name,
|
|
1389
|
+
JSON.stringify(newValue, void 0 )
|
|
1390
|
+
),
|
|
1391
|
+
removeItem: (name) => storage.removeItem(name)
|
|
1392
|
+
};
|
|
1393
|
+
return persistStorage;
|
|
1394
|
+
}
|
|
1395
|
+
var toThenable = (fn) => (input) => {
|
|
1396
|
+
try {
|
|
1397
|
+
const result = fn(input);
|
|
1398
|
+
if (result instanceof Promise) {
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
then(onFulfilled) {
|
|
1403
|
+
return toThenable(onFulfilled)(result);
|
|
1404
|
+
},
|
|
1405
|
+
catch(_onRejected) {
|
|
1406
|
+
return this;
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
return {
|
|
1411
|
+
then(_onFulfilled) {
|
|
1412
|
+
return this;
|
|
1413
|
+
},
|
|
1414
|
+
catch(onRejected) {
|
|
1415
|
+
return toThenable(onRejected)(e);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
var persistImpl = (config, baseOptions) => (set, get, api) => {
|
|
1421
|
+
let options = {
|
|
1422
|
+
storage: createJSONStorage(() => localStorage),
|
|
1423
|
+
partialize: (state) => state,
|
|
1424
|
+
version: 0,
|
|
1425
|
+
merge: (persistedState, currentState) => ({
|
|
1426
|
+
...currentState,
|
|
1427
|
+
...persistedState
|
|
1428
|
+
}),
|
|
1429
|
+
...baseOptions
|
|
1430
|
+
};
|
|
1431
|
+
let hasHydrated = false;
|
|
1432
|
+
const hydrationListeners = /* @__PURE__ */ new Set();
|
|
1433
|
+
const finishHydrationListeners = /* @__PURE__ */ new Set();
|
|
1434
|
+
let storage = options.storage;
|
|
1435
|
+
if (!storage) {
|
|
1436
|
+
return config(
|
|
1437
|
+
(...args) => {
|
|
1438
|
+
console.warn(
|
|
1439
|
+
`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`
|
|
1440
|
+
);
|
|
1441
|
+
set(...args);
|
|
1442
|
+
},
|
|
1443
|
+
get,
|
|
1444
|
+
api
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const setItem = () => {
|
|
1448
|
+
const state = options.partialize({ ...get() });
|
|
1449
|
+
return storage.setItem(options.name, {
|
|
1450
|
+
state,
|
|
1451
|
+
version: options.version
|
|
1452
|
+
});
|
|
1453
|
+
};
|
|
1454
|
+
const savedSetState = api.setState;
|
|
1455
|
+
api.setState = (state, replace) => {
|
|
1456
|
+
savedSetState(state, replace);
|
|
1457
|
+
void setItem();
|
|
1458
|
+
};
|
|
1459
|
+
const configResult = config(
|
|
1460
|
+
(...args) => {
|
|
1461
|
+
set(...args);
|
|
1462
|
+
void setItem();
|
|
1463
|
+
},
|
|
1464
|
+
get,
|
|
1465
|
+
api
|
|
1466
|
+
);
|
|
1467
|
+
api.getInitialState = () => configResult;
|
|
1468
|
+
let stateFromStorage;
|
|
1469
|
+
const hydrate = () => {
|
|
1470
|
+
var _a, _b;
|
|
1471
|
+
if (!storage) return;
|
|
1472
|
+
hasHydrated = false;
|
|
1473
|
+
hydrationListeners.forEach((cb) => {
|
|
1474
|
+
var _a2;
|
|
1475
|
+
return cb((_a2 = get()) != null ? _a2 : configResult);
|
|
1476
|
+
});
|
|
1477
|
+
const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;
|
|
1478
|
+
return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {
|
|
1479
|
+
if (deserializedStorageValue) {
|
|
1480
|
+
if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) {
|
|
1481
|
+
if (options.migrate) {
|
|
1482
|
+
return [
|
|
1483
|
+
true,
|
|
1484
|
+
options.migrate(
|
|
1485
|
+
deserializedStorageValue.state,
|
|
1486
|
+
deserializedStorageValue.version
|
|
1487
|
+
)
|
|
1488
|
+
];
|
|
1489
|
+
}
|
|
1490
|
+
console.error(
|
|
1491
|
+
`State loaded from storage couldn't be migrated since no migrate function was provided`
|
|
1492
|
+
);
|
|
1493
|
+
} else {
|
|
1494
|
+
return [false, deserializedStorageValue.state];
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return [false, void 0];
|
|
1498
|
+
}).then((migrationResult) => {
|
|
1499
|
+
var _a2;
|
|
1500
|
+
const [migrated, migratedState] = migrationResult;
|
|
1501
|
+
stateFromStorage = options.merge(
|
|
1502
|
+
migratedState,
|
|
1503
|
+
(_a2 = get()) != null ? _a2 : configResult
|
|
1504
|
+
);
|
|
1505
|
+
set(stateFromStorage, true);
|
|
1506
|
+
if (migrated) {
|
|
1507
|
+
return setItem();
|
|
1508
|
+
}
|
|
1509
|
+
}).then(() => {
|
|
1510
|
+
postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);
|
|
1511
|
+
stateFromStorage = get();
|
|
1512
|
+
hasHydrated = true;
|
|
1513
|
+
finishHydrationListeners.forEach((cb) => cb(stateFromStorage));
|
|
1514
|
+
}).catch((e) => {
|
|
1515
|
+
postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);
|
|
1516
|
+
});
|
|
1517
|
+
};
|
|
1518
|
+
api.persist = {
|
|
1519
|
+
setOptions: (newOptions) => {
|
|
1520
|
+
options = {
|
|
1521
|
+
...options,
|
|
1522
|
+
...newOptions
|
|
1523
|
+
};
|
|
1524
|
+
if (newOptions.storage) {
|
|
1525
|
+
storage = newOptions.storage;
|
|
1526
|
+
}
|
|
1527
|
+
},
|
|
1528
|
+
clearStorage: () => {
|
|
1529
|
+
storage == null ? void 0 : storage.removeItem(options.name);
|
|
1530
|
+
},
|
|
1531
|
+
getOptions: () => options,
|
|
1532
|
+
rehydrate: () => hydrate(),
|
|
1533
|
+
hasHydrated: () => hasHydrated,
|
|
1534
|
+
onHydrate: (cb) => {
|
|
1535
|
+
hydrationListeners.add(cb);
|
|
1536
|
+
return () => {
|
|
1537
|
+
hydrationListeners.delete(cb);
|
|
1538
|
+
};
|
|
1539
|
+
},
|
|
1540
|
+
onFinishHydration: (cb) => {
|
|
1541
|
+
finishHydrationListeners.add(cb);
|
|
1542
|
+
return () => {
|
|
1543
|
+
finishHydrationListeners.delete(cb);
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
if (!options.skipHydration) {
|
|
1548
|
+
hydrate();
|
|
1549
|
+
}
|
|
1550
|
+
return stateFromStorage || configResult;
|
|
1551
|
+
};
|
|
1552
|
+
var persist = persistImpl;
|
|
1553
|
+
|
|
1554
|
+
// src/store/searchHistoryStore.ts
|
|
1555
|
+
var MAX_HISTORY = 10;
|
|
1556
|
+
var useSearchHistoryStore = create()(
|
|
1557
|
+
persist(
|
|
1558
|
+
(set) => ({
|
|
1559
|
+
entries: [],
|
|
1560
|
+
addEntry: (entry) => {
|
|
1561
|
+
const trimmed = entry.query.trim();
|
|
1562
|
+
if (!trimmed) return;
|
|
1563
|
+
set((state) => {
|
|
1564
|
+
const deduped = state.entries.filter(
|
|
1565
|
+
(e) => e.query.toLowerCase() !== trimmed.toLowerCase()
|
|
1566
|
+
);
|
|
1567
|
+
const newEntry = {
|
|
1568
|
+
...entry,
|
|
1569
|
+
query: trimmed,
|
|
1570
|
+
timestamp: Date.now()
|
|
1571
|
+
};
|
|
1572
|
+
return {
|
|
1573
|
+
entries: [newEntry, ...deduped].slice(0, MAX_HISTORY)
|
|
1574
|
+
};
|
|
1575
|
+
});
|
|
1576
|
+
},
|
|
1577
|
+
addQuery: (query) => {
|
|
1578
|
+
const trimmed = query.trim();
|
|
1579
|
+
if (!trimmed) return;
|
|
1580
|
+
set((state) => {
|
|
1581
|
+
const deduped = state.entries.filter(
|
|
1582
|
+
(e) => e.query.toLowerCase() !== trimmed.toLowerCase()
|
|
1583
|
+
);
|
|
1584
|
+
return {
|
|
1585
|
+
entries: [
|
|
1586
|
+
{
|
|
1587
|
+
query: trimmed,
|
|
1588
|
+
isAI: false,
|
|
1589
|
+
timestamp: Date.now()
|
|
1590
|
+
},
|
|
1591
|
+
...deduped
|
|
1592
|
+
].slice(0, MAX_HISTORY)
|
|
1593
|
+
};
|
|
1594
|
+
});
|
|
1595
|
+
},
|
|
1596
|
+
removeEntry: (query) => set((state) => ({
|
|
1597
|
+
entries: state.entries.filter((e) => e.query !== query)
|
|
1598
|
+
})),
|
|
1599
|
+
clearAll: () => set({ entries: [] })
|
|
1600
|
+
}),
|
|
1601
|
+
{
|
|
1602
|
+
name: "ai_search_recent_queries",
|
|
1603
|
+
version: 1,
|
|
1604
|
+
storage: createJSONStorage(
|
|
1605
|
+
() => typeof window !== "undefined" ? window.sessionStorage : {}
|
|
1606
|
+
),
|
|
1607
|
+
// Migrate from v0 (queries: string[]) to v1 (entries: SearchHistoryEntry[])
|
|
1608
|
+
migrate(persistedState, version) {
|
|
1609
|
+
if (version === 0) {
|
|
1610
|
+
const old = persistedState;
|
|
1611
|
+
if (Array.isArray(old?.queries)) {
|
|
1612
|
+
return {
|
|
1613
|
+
entries: old.queries.map((q) => ({
|
|
1614
|
+
query: q,
|
|
1615
|
+
isAI: false,
|
|
1616
|
+
timestamp: Date.now()
|
|
1617
|
+
}))
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return persistedState;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
)
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
// src/hooks/useSearchHistory.ts
|
|
1628
|
+
function useSearchHistory() {
|
|
1629
|
+
const entries = useSearchHistoryStore((s) => s.entries);
|
|
1630
|
+
const addEntry = useSearchHistoryStore((s) => s.addEntry);
|
|
1631
|
+
const addQuery = useSearchHistoryStore((s) => s.addQuery);
|
|
1632
|
+
const removeEntry = useSearchHistoryStore((s) => s.removeEntry);
|
|
1633
|
+
const clearAll = useSearchHistoryStore((s) => s.clearAll);
|
|
1634
|
+
return {
|
|
1635
|
+
entries,
|
|
1636
|
+
queries: entries.map((e) => e.query),
|
|
1637
|
+
addEntry,
|
|
1638
|
+
addQuery,
|
|
1639
|
+
removeEntry,
|
|
1640
|
+
clearAll
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function getSpeechRecognition() {
|
|
1644
|
+
if (typeof window === "undefined") return null;
|
|
1645
|
+
const w = window;
|
|
1646
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
1647
|
+
}
|
|
1648
|
+
function useVoiceSearch({
|
|
1649
|
+
lang,
|
|
1650
|
+
onResult,
|
|
1651
|
+
onError
|
|
1652
|
+
}) {
|
|
1653
|
+
const [listening, setListening] = useState(false);
|
|
1654
|
+
const recognitionRef = useRef(null);
|
|
1655
|
+
const supported = getSpeechRecognition() !== null;
|
|
1656
|
+
const stop = useCallback(() => {
|
|
1657
|
+
recognitionRef.current?.stop();
|
|
1658
|
+
recognitionRef.current = null;
|
|
1659
|
+
setListening(false);
|
|
1660
|
+
}, []);
|
|
1661
|
+
const start = useCallback(() => {
|
|
1662
|
+
const SR = getSpeechRecognition();
|
|
1663
|
+
if (!SR) return;
|
|
1664
|
+
stop();
|
|
1665
|
+
const rec = new SR();
|
|
1666
|
+
rec.lang = lang ?? navigator.language ?? "en-US";
|
|
1667
|
+
rec.interimResults = false;
|
|
1668
|
+
rec.maxAlternatives = 1;
|
|
1669
|
+
rec.onresult = (event) => {
|
|
1670
|
+
const transcript = event.results[0]?.[0]?.transcript ?? "";
|
|
1671
|
+
if (transcript) onResult(transcript);
|
|
1672
|
+
};
|
|
1673
|
+
rec.onerror = (event) => {
|
|
1674
|
+
onError?.(event);
|
|
1675
|
+
setListening(false);
|
|
1676
|
+
};
|
|
1677
|
+
rec.onend = () => setListening(false);
|
|
1678
|
+
recognitionRef.current = rec;
|
|
1679
|
+
rec.start();
|
|
1680
|
+
setListening(true);
|
|
1681
|
+
}, [lang, onResult, onError, stop]);
|
|
1682
|
+
return { listening, supported, start, stop };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
export { CATALOG_SUGGESTIONS_PATH, DEBOUNCE_MS, DEFAULT_AI_CHAT_BASE_URL, DEFAULT_AI_CHAT_CONVERSATIONS_URL, DEFAULT_AI_CHAT_MESSAGES_URL, DEFAULT_SEARCH_PATH, DEFAULT_SUGGESTION_PAGE, DEFAULT_SUGGESTION_SIZE, MAX_VISIBLE_SUGGESTIONS, RECENT_SEARCHES_DEFAULT_LIMIT, RECENT_SEARCHES_PREFIX, SEARCH_PLACEHOLDER, STOREFRONT_SHOPPING_CONTEXT, TRIGGER_PLACEHOLDER, createChatService, createSearchService, getImageFromProperty, getIngredientTerms, getSeeMoreResultsPayload, getVisibleSuggestions, isCartActionDisabled, isSellableProductStatus, isSuggestionDisabled, mapCatalogProductToSuggestion, normalizeDidYouMean, resolveApiGatewayPath, resolveSuggestionImageSrc, useSearchHistory, useSearchHistoryStore, useSearchSuggestions, useVoiceSearch };
|
|
1686
|
+
//# sourceMappingURL=index.mjs.map
|
|
1687
|
+
//# sourceMappingURL=index.mjs.map
|