@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/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