@nuskin/react-search-sdk 1.0.0-comx-2233.3 → 1.0.0-trying-again.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 CHANGED
@@ -1,4 +1,6 @@
1
- import React, { useState, useRef, useCallback, useEffect } from 'react';
1
+ import { lazy, useState, useRef, useCallback, useEffect, Suspense, Component } from 'react';
2
+ import { SearchModal } from '@nuskin/react-search-components';
3
+ import { jsx } from 'react/jsx-runtime';
2
4
 
3
5
  // src/constants.ts
4
6
  var STOREFRONT_SHOPPING_CONTEXT = "storefront";
@@ -12,9 +14,6 @@ var DEBOUNCE_MS = 220;
12
14
  var MAX_VISIBLE_SUGGESTIONS = 6;
13
15
  var SEARCH_PLACEHOLDER = "Search by product name, ingredient, benefits, or SKU";
14
16
  var TRIGGER_PLACEHOLDER = "Product Search";
15
- var DEFAULT_AI_CHAT_BASE_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed";
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
17
 
19
18
  // src/services/SearchApiService.ts
20
19
  var DEFAULT_SEARCH_PLACEHOLDER = TRIGGER_PLACEHOLDER;
@@ -400,549 +399,6 @@ function createSearchService(options = {}) {
400
399
  return service;
401
400
  }
402
401
 
403
- // node_modules/eventsource-parser/dist/errors.js
404
- var ParseError = class extends Error {
405
- constructor(message, options) {
406
- super(message);
407
- this.name = "ParseError";
408
- this.type = options.type;
409
- this.field = options.field;
410
- this.value = options.value;
411
- this.line = options.line;
412
- }
413
- };
414
-
415
- // node_modules/eventsource-parser/dist/parse.js
416
- var LF = 10;
417
- var CR = 13;
418
- var SPACE = 32;
419
- var MAX_FIELD_PREFIX_LENGTH = 6;
420
- function noop(_arg) {
421
- }
422
- function createParser(config) {
423
- if (typeof config === "function") {
424
- throw new TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");
425
- }
426
- const { onEvent = noop, onError = noop, onRetry = noop, onComment, maxBufferSize } = config;
427
- const pendingFragments = [];
428
- let pendingFragmentsLength = 0;
429
- let isFirstChunk = true;
430
- let id;
431
- let data = "";
432
- let dataLines = 0;
433
- let eventType;
434
- let terminated = false;
435
- let skippingLine = false;
436
- let skipNextLineFeed = false;
437
- function feed(chunk) {
438
- if (terminated) {
439
- throw new Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");
440
- }
441
- if (isFirstChunk) {
442
- isFirstChunk = false;
443
- if (chunk.charCodeAt(0) === 65279) {
444
- chunk = chunk.slice(1);
445
- } else if (chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191) {
446
- chunk = chunk.slice(3);
447
- }
448
- }
449
- if (skippingLine || skipNextLineFeed) {
450
- chunk = resumeAfterSkip(chunk);
451
- if (!chunk) {
452
- return;
453
- }
454
- }
455
- if (!pendingFragments.length) {
456
- const trailing = processLines(chunk);
457
- if (trailing !== "") {
458
- storeTrailing(trailing);
459
- }
460
- checkBufferSize();
461
- return;
462
- }
463
- if (chunk.indexOf("\n") === -1 && chunk.indexOf("\r") === -1) {
464
- if (pendingFragmentsLength < MAX_FIELD_PREFIX_LENGTH) {
465
- const head = pendingFragments.join("") + chunk.slice(0, MAX_FIELD_PREFIX_LENGTH - pendingFragmentsLength);
466
- if (!shouldBufferTrailing(head)) {
467
- pendingFragments.length = 0;
468
- pendingFragmentsLength = 0;
469
- skippingLine = true;
470
- return;
471
- }
472
- }
473
- pendingFragments.push(chunk);
474
- pendingFragmentsLength += chunk.length;
475
- checkBufferSize();
476
- return;
477
- }
478
- pendingFragments.push(chunk);
479
- const input = pendingFragments.join("");
480
- pendingFragments.length = 0;
481
- pendingFragmentsLength = 0;
482
- storeTrailing(processLines(input));
483
- checkBufferSize();
484
- }
485
- function resumeAfterSkip(chunk) {
486
- if (chunk.length === 0) {
487
- return chunk;
488
- }
489
- if (skipNextLineFeed) {
490
- skipNextLineFeed = false;
491
- return chunk.charCodeAt(0) === LF ? chunk.slice(1) : chunk;
492
- }
493
- const crIndex = chunk.indexOf("\r");
494
- const lfIndex = chunk.indexOf("\n");
495
- const lineEnd = crIndex === -1 ? lfIndex : lfIndex === -1 ? crIndex : crIndex < lfIndex ? crIndex : lfIndex;
496
- if (lineEnd === -1) {
497
- return "";
498
- }
499
- if (lineEnd === chunk.length - 1 && chunk.charCodeAt(lineEnd) === CR) {
500
- skippingLine = false;
501
- skipNextLineFeed = true;
502
- return "";
503
- }
504
- skippingLine = false;
505
- return chunk.slice(lineEnd + (chunk.charCodeAt(lineEnd) === CR && chunk.charCodeAt(lineEnd + 1) === LF ? 2 : 1));
506
- }
507
- function storeTrailing(trailing) {
508
- if (!trailing)
509
- return;
510
- if (trailing.charCodeAt(trailing.length - 1) === CR) {
511
- parseLine(trailing, 0, trailing.length - 1);
512
- skipNextLineFeed = true;
513
- return;
514
- }
515
- if (shouldBufferTrailing(trailing)) {
516
- pendingFragments.push(trailing);
517
- pendingFragmentsLength = trailing.length;
518
- return;
519
- }
520
- skippingLine = true;
521
- }
522
- function shouldBufferTrailing(trailing) {
523
- const firstCharCode = trailing.charCodeAt(0);
524
- return firstCharCode === 58 && !!onComment || firstCharCode === 100 && isPotentialField(trailing, "data") || firstCharCode === 101 && isPotentialField(trailing, "event") || firstCharCode === 105 && isPotentialField(trailing, "id") || firstCharCode === 114 && isPotentialField(trailing, "retry");
525
- }
526
- function checkBufferSize() {
527
- if (maxBufferSize === void 0)
528
- return;
529
- if (pendingFragmentsLength + data.length <= maxBufferSize)
530
- return;
531
- terminated = true;
532
- pendingFragments.length = 0;
533
- pendingFragmentsLength = 0;
534
- id = void 0;
535
- data = "";
536
- dataLines = 0;
537
- eventType = void 0;
538
- skippingLine = false;
539
- skipNextLineFeed = false;
540
- onError(new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {
541
- type: "max-buffer-size-exceeded"
542
- }));
543
- }
544
- function processLines(chunk) {
545
- let searchIndex = 0;
546
- if (chunk.indexOf("\r") === -1) {
547
- let lfIndex = chunk.indexOf("\n", searchIndex);
548
- while (lfIndex !== -1) {
549
- if (searchIndex === lfIndex) {
550
- if (dataLines > 0) {
551
- onEvent({ id, event: eventType, data });
552
- }
553
- id = void 0;
554
- data = "";
555
- dataLines = 0;
556
- eventType = void 0;
557
- searchIndex = lfIndex + 1;
558
- lfIndex = chunk.indexOf("\n", searchIndex);
559
- continue;
560
- }
561
- const firstCharCode = chunk.charCodeAt(searchIndex);
562
- if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
563
- const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5;
564
- const value = chunk.slice(valueStart, lfIndex);
565
- if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
566
- onEvent({ id, event: eventType, data: value });
567
- id = void 0;
568
- data = "";
569
- eventType = void 0;
570
- searchIndex = lfIndex + 2;
571
- lfIndex = chunk.indexOf("\n", searchIndex);
572
- continue;
573
- }
574
- data = dataLines === 0 ? value : `${data}
575
- ${value}`;
576
- dataLines++;
577
- } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {
578
- eventType = chunk.slice(chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, lfIndex) || void 0;
579
- } else {
580
- parseLine(chunk, searchIndex, lfIndex);
581
- }
582
- searchIndex = lfIndex + 1;
583
- lfIndex = chunk.indexOf("\n", searchIndex);
584
- }
585
- return chunk.slice(searchIndex);
586
- }
587
- while (searchIndex < chunk.length) {
588
- const crIndex = chunk.indexOf("\r", searchIndex);
589
- const lfIndex = chunk.indexOf("\n", searchIndex);
590
- let lineEnd = -1;
591
- if (crIndex !== -1 && lfIndex !== -1) {
592
- lineEnd = crIndex < lfIndex ? crIndex : lfIndex;
593
- } else if (crIndex !== -1) {
594
- if (crIndex === chunk.length - 1) {
595
- lineEnd = -1;
596
- } else {
597
- lineEnd = crIndex;
598
- }
599
- } else if (lfIndex !== -1) {
600
- lineEnd = lfIndex;
601
- }
602
- if (lineEnd === -1) {
603
- break;
604
- }
605
- parseLine(chunk, searchIndex, lineEnd);
606
- searchIndex = lineEnd + 1;
607
- if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {
608
- searchIndex++;
609
- }
610
- }
611
- return chunk.slice(searchIndex);
612
- }
613
- function parseLine(chunk, start, end) {
614
- if (start === end) {
615
- dispatchEvent();
616
- return;
617
- }
618
- const firstCharCode = chunk.charCodeAt(start);
619
- if (isDataPrefix(chunk, start, firstCharCode)) {
620
- const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5;
621
- const value2 = chunk.slice(valueStart, end);
622
- data = dataLines === 0 ? value2 : `${data}
623
- ${value2}`;
624
- dataLines++;
625
- return;
626
- }
627
- if (isEventPrefix(chunk, start, firstCharCode)) {
628
- eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
629
- return;
630
- }
631
- if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
632
- const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
633
- if (!value2.includes("\0"))
634
- id = value2;
635
- return;
636
- }
637
- if (firstCharCode === 58) {
638
- if (onComment) {
639
- const line2 = chunk.slice(start, end);
640
- onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
641
- }
642
- return;
643
- }
644
- const line = chunk.slice(start, end);
645
- const fieldSeparatorIndex = line.indexOf(":");
646
- if (fieldSeparatorIndex === -1) {
647
- processField(line, "", line);
648
- return;
649
- }
650
- const field = line.slice(0, fieldSeparatorIndex);
651
- const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1;
652
- const value = line.slice(fieldSeparatorIndex + offset);
653
- processField(field, value, line);
654
- }
655
- function processField(field, value, line) {
656
- switch (field) {
657
- case "event":
658
- eventType = value || void 0;
659
- break;
660
- case "data":
661
- data = dataLines === 0 ? value : `${data}
662
- ${value}`;
663
- dataLines++;
664
- break;
665
- case "id":
666
- if (!value.includes("\0"))
667
- id = value;
668
- break;
669
- case "retry":
670
- if (/^\d+$/.test(value)) {
671
- onRetry(parseInt(value, 10));
672
- } else {
673
- onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
674
- type: "invalid-retry",
675
- value,
676
- line
677
- }));
678
- }
679
- break;
680
- default:
681
- onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`, { type: "unknown-field", field, value, line }));
682
- break;
683
- }
684
- }
685
- function dispatchEvent() {
686
- if (dataLines > 0) {
687
- onEvent({
688
- id,
689
- event: eventType,
690
- data
691
- });
692
- }
693
- id = void 0;
694
- data = "";
695
- dataLines = 0;
696
- eventType = void 0;
697
- }
698
- function reset(options = {}) {
699
- if (options.consume && pendingFragments.length > 0) {
700
- const incompleteLine = pendingFragments.join("");
701
- parseLine(incompleteLine, 0, incompleteLine.length);
702
- }
703
- isFirstChunk = true;
704
- id = void 0;
705
- data = "";
706
- dataLines = 0;
707
- eventType = void 0;
708
- pendingFragments.length = 0;
709
- pendingFragmentsLength = 0;
710
- terminated = false;
711
- skippingLine = false;
712
- skipNextLineFeed = false;
713
- }
714
- return { feed, reset };
715
- }
716
- function isDataPrefix(chunk, i, firstCharCode) {
717
- return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
718
- }
719
- function isEventPrefix(chunk, i, firstCharCode) {
720
- 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;
721
- }
722
- function isPotentialField(line, field) {
723
- let i = 1;
724
- while (i < line.length && i < field.length) {
725
- if (line.charCodeAt(i) !== field.charCodeAt(i)) {
726
- return false;
727
- }
728
- i++;
729
- }
730
- return line.length <= field.length || line.charCodeAt(field.length) === 58;
731
- }
732
-
733
- // src/services/ChatService.ts
734
- var PRODUCTS_OPENING_FENCE = "```products";
735
- var MARKDOWN_CLOSING_FENCE = "\n```";
736
- function findProductsBlockStart(content, openingFenceIndex) {
737
- let blockStart = openingFenceIndex + PRODUCTS_OPENING_FENCE.length;
738
- while (blockStart < content.length && (content[blockStart] === " " || content[blockStart] === " " || content[blockStart] === "\r")) {
739
- blockStart += 1;
740
- }
741
- return content[blockStart] === "\n" ? blockStart + 1 : null;
742
- }
743
- function isStandaloneClosingFence(content, fenceEnd) {
744
- const lineEnd = content.indexOf("\n", fenceEnd);
745
- const suffixEnd = lineEnd === -1 ? content.length : lineEnd;
746
- return content.slice(fenceEnd, suffixEnd).trim() === "";
747
- }
748
- function findClosingFence(content, fromIndex) {
749
- let candidate = content.indexOf(MARKDOWN_CLOSING_FENCE, fromIndex);
750
- while (candidate !== -1) {
751
- const fenceEnd = candidate + MARKDOWN_CLOSING_FENCE.length;
752
- if (isStandaloneClosingFence(content, fenceEnd)) {
753
- return candidate;
754
- }
755
- candidate = content.indexOf(MARKDOWN_CLOSING_FENCE, fenceEnd);
756
- }
757
- return -1;
758
- }
759
- function isRecord(value) {
760
- return value !== null && typeof value === "object";
761
- }
762
- function getRenderProductListItems(event) {
763
- if (event["type"] !== "render" || event["pattern"] !== "product_list") {
764
- return [];
765
- }
766
- const data = event["data"];
767
- if (!isRecord(data) || !Array.isArray(data["items"])) {
768
- return [];
769
- }
770
- return data["items"].filter(isRecord);
771
- }
772
- function extractProductsFromMarkdown(content) {
773
- const products = [];
774
- const summaryParts = [];
775
- let summaryCursor = 0;
776
- let searchCursor = 0;
777
- while (searchCursor < content.length) {
778
- const start = content.indexOf(PRODUCTS_OPENING_FENCE, searchCursor);
779
- if (start === -1) break;
780
- const blockStart = findProductsBlockStart(content, start);
781
- if (blockStart === null) {
782
- searchCursor = start + PRODUCTS_OPENING_FENCE.length;
783
- continue;
784
- }
785
- const nextOpeningFence = content.indexOf(
786
- PRODUCTS_OPENING_FENCE,
787
- blockStart
788
- );
789
- const end = findClosingFence(content, blockStart);
790
- if (end === -1) break;
791
- if (nextOpeningFence !== -1 && nextOpeningFence < end) {
792
- searchCursor = nextOpeningFence;
793
- continue;
794
- }
795
- try {
796
- const parsed = JSON.parse(content.slice(blockStart, end));
797
- if (Array.isArray(parsed)) {
798
- products.push(...parsed);
799
- }
800
- } catch (e) {
801
- console.error("Failed to parse products block:", e);
802
- }
803
- summaryParts.push(content.slice(summaryCursor, start));
804
- summaryCursor = end + MARKDOWN_CLOSING_FENCE.length;
805
- searchCursor = summaryCursor;
806
- }
807
- summaryParts.push(content.slice(summaryCursor));
808
- return { summary: summaryParts.join("").trim(), products };
809
- }
810
- function createChatService(options) {
811
- const {
812
- conversationsUrl = DEFAULT_AI_CHAT_CONVERSATIONS_URL,
813
- messagesUrl = DEFAULT_AI_CHAT_MESSAGES_URL,
814
- salesAiAppKey,
815
- salesAiParentOrigin,
816
- market,
817
- language,
818
- fetchImpl: fetchImplOption
819
- } = options;
820
- let fetchImpl;
821
- if (fetchImplOption !== void 0) {
822
- fetchImpl = fetchImplOption;
823
- } else if (typeof fetch === "function") {
824
- fetchImpl = fetch.bind(globalThis);
825
- } else {
826
- fetchImpl = () => {
827
- throw new Error("ChatService: no fetch implementation available");
828
- };
829
- }
830
- function buildHeaders(extra, includeJsonContentType = true) {
831
- const headers = {
832
- ...extra
833
- };
834
- if (includeJsonContentType) {
835
- headers["Content-Type"] = "application/json";
836
- }
837
- const accessToken = typeof window !== "undefined" ? window.localStorage?.getItem("accessToken") : null;
838
- if (accessToken) {
839
- headers["Authorization"] = `Bearer ${accessToken}`;
840
- }
841
- if (salesAiAppKey) {
842
- headers["X-Sales-AI-App-Key"] = salesAiAppKey;
843
- }
844
- if (salesAiParentOrigin) {
845
- headers["X-Sales-AI-Parent-Origin"] = salesAiParentOrigin;
846
- }
847
- return headers;
848
- }
849
- return {
850
- async createConversation() {
851
- const res = await fetchImpl(conversationsUrl, {
852
- method: "POST",
853
- headers: buildHeaders(void 0, false)
854
- });
855
- if (!res.ok) {
856
- throw new Error(
857
- `ChatService.createConversation: ${res.status} ${res.statusText}`
858
- );
859
- }
860
- const data = await res.json();
861
- const id = data["id"] ?? data["conversation_id"];
862
- if (typeof id !== "number") {
863
- throw new TypeError(
864
- "ChatService.createConversation: response missing numeric id"
865
- );
866
- }
867
- return id;
868
- },
869
- async sendMessage(conversationId, content, onChunk) {
870
- const requestBody = {
871
- conversation_id: conversationId,
872
- content
873
- };
874
- if (market || language) {
875
- requestBody.scope_context = {
876
- market: market ?? "US",
877
- language: language ?? "en"
878
- };
879
- }
880
- requestBody.client_capabilities = {
881
- patterns: {
882
- product_list: [1],
883
- follow_up: [1]
884
- }
885
- };
886
- const res = await fetchImpl(messagesUrl, {
887
- method: "POST",
888
- headers: buildHeaders({ Accept: "text/event-stream" }),
889
- body: JSON.stringify(requestBody)
890
- });
891
- if (!res.ok) {
892
- throw new Error(
893
- `ChatService.sendMessage: ${res.status} ${res.statusText}`
894
- );
895
- }
896
- let accumulated = "";
897
- const renderedProducts = [];
898
- const parser = createParser({
899
- onEvent(event) {
900
- const data = event.data;
901
- if (!data || data === "[DONE]") return;
902
- try {
903
- const parsed = JSON.parse(data);
904
- const parsedType = parsed["type"];
905
- const type = typeof parsedType === "string" ? parsedType : "";
906
- renderedProducts.push(
907
- ...getRenderProductListItems(parsed)
908
- );
909
- if (type === "content" && typeof parsed["text"] === "string") {
910
- const text = parsed["text"];
911
- accumulated += text;
912
- onChunk?.(type, text);
913
- } else if (type === "status" && typeof parsed["message"] === "string") {
914
- const message = parsed["message"];
915
- onChunk?.(type, message);
916
- }
917
- } catch {
918
- }
919
- }
920
- });
921
- const body = res.body;
922
- if (body && typeof body.getReader === "function") {
923
- const reader = body.getReader();
924
- const decoder = new TextDecoder();
925
- while (true) {
926
- const { value, done } = await reader.read();
927
- if (done) break;
928
- parser.feed(decoder.decode(value, { stream: true }));
929
- }
930
- parser.feed(decoder.decode());
931
- } else {
932
- const rawBody = await res.text();
933
- parser.feed(rawBody);
934
- }
935
- const { summary, products } = extractProductsFromMarkdown(accumulated);
936
- const finalProducts = renderedProducts.length > 0 ? renderedProducts : products;
937
- return {
938
- summary,
939
- products: finalProducts,
940
- rawContent: accumulated
941
- };
942
- }
943
- };
944
- }
945
-
946
402
  // src/utils/catalogHelpers.ts
947
403
  var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
948
404
  "discontinued",
@@ -1248,22 +704,27 @@ function useSearchSuggestions({
1248
704
  const [didYouMean, setDidYouMean] = useState([]);
1249
705
  const [loading, setLoading] = useState(false);
1250
706
  const [emptyFetched, setEmptyFetched] = useState(false);
707
+ const [suggestionFetchFailed, setSuggestionFetchFailed] = useState(false);
1251
708
  const [hasMoreResults, setHasMoreResults] = useState(false);
1252
709
  const [totalResultsCount, setTotalResultsCount] = useState(0);
710
+ const [settledQuery, setSettledQuery] = useState("");
1253
711
  const fetchDebounced = useRef(null);
1254
712
  const latestRequestId = useRef(0);
1255
713
  const resetSuggestionsState = useCallback(() => {
1256
714
  setSuggestions([]);
1257
715
  setDidYouMean([]);
1258
716
  setEmptyFetched(false);
717
+ setSuggestionFetchFailed(false);
1259
718
  setHasMoreResults(false);
1260
719
  setTotalResultsCount(0);
720
+ setSettledQuery("");
1261
721
  setLoading(false);
1262
722
  }, []);
1263
723
  const updateQuery = useCallback(
1264
724
  (nextQuery) => {
1265
725
  const hasNextQuery = Boolean(nextQuery.trim());
1266
726
  setEmptyFetched(false);
727
+ setSuggestionFetchFailed(false);
1267
728
  setLoading(enabled && hasNextQuery);
1268
729
  setQuery(nextQuery);
1269
730
  },
@@ -1283,6 +744,7 @@ function useSearchSuggestions({
1283
744
  }
1284
745
  setLoading(true);
1285
746
  setEmptyFetched(false);
747
+ setSuggestionFetchFailed(false);
1286
748
  try {
1287
749
  const fetchedSuggestions = await service.fetchSuggestions(trimmedQuery);
1288
750
  if (requestId !== latestRequestId.current) {
@@ -1305,6 +767,8 @@ function useSearchSuggestions({
1305
767
  !hasDidYouMean && effectiveTotal > MAX_VISIBLE_SUGGESTIONS
1306
768
  );
1307
769
  setTotalResultsCount(effectiveTotal);
770
+ setSuggestionFetchFailed(false);
771
+ setSettledQuery(trimmedQuery);
1308
772
  setEmptyFetched(
1309
773
  hasDidYouMean ? false : visibleSuggestions.length === 0 && suggestedTerms.length === 0
1310
774
  );
@@ -1315,8 +779,10 @@ function useSearchSuggestions({
1315
779
  setSuggestions([]);
1316
780
  setDidYouMean([]);
1317
781
  setEmptyFetched(true);
782
+ setSuggestionFetchFailed(true);
1318
783
  setHasMoreResults(false);
1319
784
  setTotalResultsCount(0);
785
+ setSettledQuery(trimmedQuery);
1320
786
  } finally {
1321
787
  if (requestId === latestRequestId.current) {
1322
788
  setLoading(false);
@@ -1351,389 +817,587 @@ function useSearchSuggestions({
1351
817
  didYouMean,
1352
818
  loading,
1353
819
  query,
820
+ settledQuery,
1354
821
  setQuery,
822
+ suggestionFetchFailed,
1355
823
  suggestions,
1356
824
  totalResultsCount,
1357
825
  updateQuery
1358
826
  };
1359
827
  }
1360
828
 
1361
- // node_modules/zustand/esm/vanilla.mjs
1362
- var createStoreImpl = (createState) => {
1363
- let state;
1364
- const listeners = /* @__PURE__ */ new Set();
1365
- const setState = (partial, replace) => {
1366
- const nextState = typeof partial === "function" ? partial(state) : partial;
1367
- if (!Object.is(nextState, state)) {
1368
- const previousState = state;
1369
- state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
1370
- listeners.forEach((listener) => listener(state, previousState));
1371
- }
1372
- };
1373
- const getState = () => state;
1374
- const getInitialState = () => initialState;
1375
- const subscribe = (listener) => {
1376
- listeners.add(listener);
1377
- return () => listeners.delete(listener);
1378
- };
1379
- const api = { setState, getState, getInitialState, subscribe };
1380
- const initialState = state = createState(setState, getState, api);
1381
- return api;
829
+ // src/utils/gtmConstants.ts
830
+ var SEARCH_GTM_EVENTS = {
831
+ CHAT_ENTRY_POINT: "Begin_chat",
832
+ SEARCH_TYPE: "Search_Type",
833
+ PRODUCT_SEARCH: "ProductSearch",
834
+ ERROR_POPUP: "Error_popup"
1382
835
  };
1383
- var createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
1384
- var identity = (arg) => arg;
1385
- function useStore(api, selector = identity) {
1386
- const slice = React.useSyncExternalStore(
1387
- api.subscribe,
1388
- () => selector(api.getState()),
1389
- () => selector(api.getInitialState())
1390
- );
1391
- React.useDebugValue(slice);
1392
- return slice;
1393
- }
1394
- var createImpl = (createState) => {
1395
- const api = createStore(createState);
1396
- const useBoundStore = (selector) => useStore(api, selector);
1397
- Object.assign(useBoundStore, api);
1398
- return useBoundStore;
836
+ var SEARCH_GTM_SEARCH_TYPES = {
837
+ SEMANTIC: "semantic search",
838
+ AI_AGENT: "AI agent"
1399
839
  };
1400
- var create = (createState) => createImpl;
840
+ var SEARCH_GTM_USER_STATUS = {
841
+ GUEST: "guest",
842
+ LOGGED_IN: "logged_in"
843
+ };
844
+ var IS_AUTHENTICATED_KEY = "IsAuthenticated";
845
+ var ACCOUNT_TYPE_KEY = "accountType";
846
+ var NUSKIN_ACCOUNT_DATA_KEY = "nuskin.account.data";
847
+ var GTM_FALLBACK_NA = "NA";
1401
848
 
1402
- // node_modules/zustand/esm/middleware.mjs
1403
- function createJSONStorage(getStorage, options) {
1404
- let storage;
849
+ // src/utils/commonUtils.ts
850
+ var IS_BROWSER = typeof window !== "undefined";
851
+ function toEventString(value, fallback = " ") {
852
+ if (value === void 0 || value === null) return fallback;
853
+ const str = String(value).trim();
854
+ return str.length > 0 ? str : fallback;
855
+ }
856
+ function getLocaleFromUrl(pathname) {
857
+ const path = pathname ?? (IS_BROWSER ? window.location.pathname : "");
858
+ const [country, language] = path.split("/").filter(Boolean);
859
+ if (!/^[a-z]{2}$/i.test(country ?? "") || !/^[a-z]{2}$/i.test(language ?? "")) {
860
+ return {};
861
+ }
862
+ return {
863
+ countryCode: country.toUpperCase(),
864
+ language: language.toLowerCase()
865
+ };
866
+ }
867
+ function safeParse(raw) {
1405
868
  try {
1406
- storage = getStorage();
1407
- } catch (e) {
1408
- return;
869
+ return JSON.parse(raw);
870
+ } catch {
871
+ return raw;
1409
872
  }
1410
- const persistStorage = {
1411
- getItem: (name) => {
1412
- var _a;
1413
- const parse = (str2) => {
1414
- if (str2 === null) {
1415
- return null;
1416
- }
1417
- return JSON.parse(str2, void 0 );
1418
- };
1419
- const str = (_a = storage.getItem(name)) != null ? _a : null;
1420
- if (str instanceof Promise) {
1421
- return str.then(parse);
1422
- }
1423
- return parse(str);
1424
- },
1425
- setItem: (name, newValue) => storage.setItem(
1426
- name,
1427
- JSON.stringify(newValue, void 0 )
873
+ }
874
+ function getLocalStorage(key) {
875
+ if (!IS_BROWSER) return void 0;
876
+ try {
877
+ const raw = window.localStorage.getItem(key);
878
+ return raw === null ? void 0 : safeParse(raw);
879
+ } catch {
880
+ return void 0;
881
+ }
882
+ }
883
+
884
+ // src/utils/gtmTracking.ts
885
+ function readUserStatusFromStorage() {
886
+ const value = getLocalStorage(IS_AUTHENTICATED_KEY);
887
+ if (value === void 0) return void 0;
888
+ if (value === SEARCH_GTM_USER_STATUS.GUEST || value === SEARCH_GTM_USER_STATUS.LOGGED_IN) {
889
+ return value;
890
+ }
891
+ const isAuthenticated = value === true || typeof value === "string" && ["true", "1", "yes", "y"].includes(value.toLowerCase().trim());
892
+ return isAuthenticated ? SEARCH_GTM_USER_STATUS.LOGGED_IN : SEARCH_GTM_USER_STATUS.GUEST;
893
+ }
894
+ function readAccountTypeFromStorage() {
895
+ const value = getLocalStorage(ACCOUNT_TYPE_KEY);
896
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
897
+ }
898
+ function readUserIdFromStorage() {
899
+ const raw = getLocalStorage(NUSKIN_ACCOUNT_DATA_KEY);
900
+ if (!raw || typeof raw !== "object") return void 0;
901
+ const user = raw.user;
902
+ if (!user || typeof user !== "object") return void 0;
903
+ const id = user.id;
904
+ return typeof id === "string" && id.trim().length > 0 ? id : void 0;
905
+ }
906
+ function resolveUserId(explicit) {
907
+ return toEventString(explicit ?? readUserIdFromStorage(), GTM_FALLBACK_NA);
908
+ }
909
+ function baseEventPayload(ctx) {
910
+ const urlLocale = getLocaleFromUrl();
911
+ const userStatus = ctx.userStatus ?? readUserStatusFromStorage();
912
+ const accountType = ctx.accountType ?? readAccountTypeFromStorage();
913
+ const resolvedUserStatus = userStatus ?? SEARCH_GTM_USER_STATUS.GUEST;
914
+ const documentTitle = IS_BROWSER && typeof document?.title === "string" ? document.title : void 0;
915
+ return {
916
+ Chat_ID: toEventString(ctx.chatId, GTM_FALLBACK_NA),
917
+ page_type: toEventString(
918
+ ctx.pageType ?? documentTitle,
919
+ GTM_FALLBACK_NA
920
+ ),
921
+ country_code: toEventString(
922
+ ctx.countryCode ?? urlLocale.countryCode,
923
+ GTM_FALLBACK_NA
1428
924
  ),
1429
- removeItem: (name) => storage.removeItem(name)
925
+ language: toEventString(
926
+ ctx.language ?? urlLocale.language,
927
+ GTM_FALLBACK_NA
928
+ ),
929
+ user_status: toEventString(
930
+ resolvedUserStatus,
931
+ SEARCH_GTM_USER_STATUS.GUEST
932
+ ),
933
+ account_type: toEventString(accountType, GTM_FALLBACK_NA)
1430
934
  };
1431
- return persistStorage;
1432
935
  }
1433
- var toThenable = (fn) => (input) => {
936
+ function serializeProductResults(productResultsData, productResults) {
937
+ if (productResultsData !== void 0) {
938
+ try {
939
+ return JSON.stringify(productResultsData);
940
+ } catch {
941
+ return GTM_FALLBACK_NA;
942
+ }
943
+ }
944
+ if (productResults === void 0 || productResults === null) {
945
+ return GTM_FALLBACK_NA;
946
+ }
947
+ return String(productResults);
948
+ }
949
+ function pushGtmEvent(event) {
950
+ if (!IS_BROWSER) return;
1434
951
  try {
1435
- const result = fn(input);
1436
- if (result instanceof Promise) {
1437
- return result;
952
+ const layer = window.dataLayer ?? (window.dataLayer = []);
953
+ layer.push(event);
954
+ } catch (err) {
955
+ if (typeof console !== "undefined") {
956
+ console.warn("[react-search-sdk] pushGtmEvent failed:", err);
1438
957
  }
1439
- return {
1440
- then(onFulfilled) {
1441
- return toThenable(onFulfilled)(result);
1442
- },
1443
- catch(_onRejected) {
1444
- return this;
1445
- }
1446
- };
1447
- } catch (e) {
1448
- return {
1449
- then(_onFulfilled) {
1450
- return this;
1451
- },
1452
- catch(onRejected) {
1453
- return toThenable(onRejected)(e);
1454
- }
1455
- };
1456
958
  }
1457
- };
1458
- var persistImpl = (config, baseOptions) => (set, get, api) => {
1459
- let options = {
1460
- storage: createJSONStorage(() => localStorage),
1461
- partialize: (state) => state,
1462
- version: 0,
1463
- merge: (persistedState, currentState) => ({
1464
- ...currentState,
1465
- ...persistedState
1466
- }),
1467
- ...baseOptions
1468
- };
1469
- let hasHydrated = false;
1470
- const hydrationListeners = /* @__PURE__ */ new Set();
1471
- const finishHydrationListeners = /* @__PURE__ */ new Set();
1472
- let storage = options.storage;
1473
- if (!storage) {
1474
- return config(
1475
- (...args) => {
1476
- console.warn(
1477
- `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`
1478
- );
1479
- set(...args);
1480
- },
1481
- get,
1482
- api
959
+ }
960
+ function trackChatEntryPoint(ctx) {
961
+ pushGtmEvent({
962
+ event: SEARCH_GTM_EVENTS.CHAT_ENTRY_POINT,
963
+ ...baseEventPayload(ctx)
964
+ });
965
+ }
966
+ function trackSearchType(input) {
967
+ pushGtmEvent({
968
+ event: SEARCH_GTM_EVENTS.SEARCH_TYPE,
969
+ ...baseEventPayload(input),
970
+ id: resolveUserId(input.userId),
971
+ type: input.searchType ?? SEARCH_GTM_SEARCH_TYPES.SEMANTIC,
972
+ search_response: toEventString(input.searchResponse, GTM_FALLBACK_NA)
973
+ });
974
+ }
975
+ function trackProductSearch(input) {
976
+ pushGtmEvent({
977
+ event: SEARCH_GTM_EVENTS.PRODUCT_SEARCH,
978
+ ...baseEventPayload(input),
979
+ id: resolveUserId(input.userId),
980
+ search_type: input.searchType ?? SEARCH_GTM_SEARCH_TYPES.SEMANTIC,
981
+ Product_results: serializeProductResults(
982
+ input.productResultsData,
983
+ input.productResults
984
+ )
985
+ });
986
+ }
987
+ function trackErrorPopup(input) {
988
+ pushGtmEvent({
989
+ event: SEARCH_GTM_EVENTS.ERROR_POPUP,
990
+ ...baseEventPayload(input),
991
+ id: resolveUserId(input.userId),
992
+ search_type: input.searchType ?? SEARCH_GTM_SEARCH_TYPES.SEMANTIC
993
+ });
994
+ }
995
+
996
+ // src/utils/nuiSearchGate.ts
997
+ var NUI_SEARCH_FLAG = "enableNuiSearch";
998
+ function parseFlagValue(raw) {
999
+ const value = raw?.trim().toLowerCase();
1000
+ if (value === "true" || value === "1") return true;
1001
+ if (value === "false" || value === "0") return false;
1002
+ return void 0;
1003
+ }
1004
+ function readCookieFlag(cookie) {
1005
+ for (const part of cookie.split(";")) {
1006
+ const separator = part.indexOf("=");
1007
+ if (separator < 0) continue;
1008
+ if (part.slice(0, separator).trim() !== NUI_SEARCH_FLAG) continue;
1009
+ const rawValue = part.slice(separator + 1);
1010
+ try {
1011
+ return parseFlagValue(decodeURIComponent(rawValue));
1012
+ } catch {
1013
+ return void 0;
1014
+ }
1015
+ }
1016
+ return void 0;
1017
+ }
1018
+ function resolveNuiSearchFlag(env = {}) {
1019
+ const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
1020
+ const search = env.search ?? (inBrowser ? window.location.search : "");
1021
+ const cookie = env.cookie ?? (inBrowser ? document.cookie : "");
1022
+ const setCookie = env.setCookie ?? (inBrowser ? (value) => {
1023
+ document.cookie = value;
1024
+ } : void 0);
1025
+ const fromQuery = parseFlagValue(
1026
+ new URLSearchParams(search).get(NUI_SEARCH_FLAG)
1027
+ );
1028
+ if (fromQuery !== void 0) {
1029
+ const secure = inBrowser && window.location.protocol === "https:" ? "; Secure" : "";
1030
+ setCookie?.(
1031
+ `${NUI_SEARCH_FLAG}=${fromQuery}; path=/; SameSite=Lax${secure}`
1483
1032
  );
1033
+ return fromQuery;
1484
1034
  }
1485
- const setItem = () => {
1486
- const state = options.partialize({ ...get() });
1487
- return storage.setItem(options.name, {
1488
- state,
1489
- version: options.version
1490
- });
1491
- };
1492
- const savedSetState = api.setState;
1493
- api.setState = (state, replace) => {
1494
- savedSetState(state, replace);
1495
- void setItem();
1496
- };
1497
- const configResult = config(
1498
- (...args) => {
1499
- set(...args);
1500
- void setItem();
1035
+ return readCookieFlag(cookie);
1036
+ }
1037
+ function isNuiSearchEnabled({
1038
+ isAuthenticated,
1039
+ flag
1040
+ }) {
1041
+ return isAuthenticated === true && flag === true;
1042
+ }
1043
+
1044
+ // src/utils/searchRouting.ts
1045
+ function decideSearchRoute({
1046
+ enabled,
1047
+ snapshot
1048
+ }) {
1049
+ const term = snapshot.query.trim();
1050
+ if (!enabled || !term) return "search";
1051
+ if (snapshot.loading || snapshot.settledQuery !== term) return "wait";
1052
+ if (snapshot.suggestionFetchFailed) return "search";
1053
+ return snapshot.suggestions.length > 0 ? "search" : "handoff";
1054
+ }
1055
+ var HANDLE_POLL_MS = 50;
1056
+ var HANDLE_TIMEOUT_MS = 15e3;
1057
+ var SalesAIChatWidget = lazy(async () => {
1058
+ const [widget] = await Promise.all([
1059
+ import('@nuskin/react-nui-chat-sdk'),
1060
+ import('@nuskin/react-nui-chat-sdk/styles.css')
1061
+ ]);
1062
+ return { default: widget.SalesAIChatWidget };
1063
+ });
1064
+ function getSalesAIHandle() {
1065
+ return window.SalesAI;
1066
+ }
1067
+ var AgentErrorBoundary = class extends Component {
1068
+ constructor() {
1069
+ super(...arguments);
1070
+ this.state = { failed: false };
1071
+ }
1072
+ static getDerivedStateFromError() {
1073
+ return { failed: true };
1074
+ }
1075
+ componentDidCatch(_error, _info) {
1076
+ this.props.onError();
1077
+ }
1078
+ render() {
1079
+ return this.state.failed ? null : this.props.children;
1080
+ }
1081
+ };
1082
+ function useHandoff({
1083
+ query,
1084
+ handoffContext,
1085
+ agentContext,
1086
+ onUnavailable
1087
+ }) {
1088
+ useEffect(() => {
1089
+ let cancelled = false;
1090
+ let timer;
1091
+ const startedAt = Date.now();
1092
+ const tryOpen = () => {
1093
+ if (cancelled) return;
1094
+ const handle = getSalesAIHandle();
1095
+ if (handle) {
1096
+ handle.open({
1097
+ query,
1098
+ context: { ...agentContext, ...handoffContext }
1099
+ });
1100
+ return;
1101
+ }
1102
+ if (Date.now() - startedAt >= HANDLE_TIMEOUT_MS) {
1103
+ onUnavailable();
1104
+ return;
1105
+ }
1106
+ timer = setTimeout(tryOpen, HANDLE_POLL_MS);
1107
+ };
1108
+ tryOpen();
1109
+ return () => {
1110
+ cancelled = true;
1111
+ if (timer !== void 0) clearTimeout(timer);
1112
+ };
1113
+ }, [query]);
1114
+ }
1115
+ function NuiAgentPanel({
1116
+ agent,
1117
+ query,
1118
+ handoffContext,
1119
+ onUnavailable
1120
+ }) {
1121
+ useHandoff({
1122
+ query,
1123
+ handoffContext,
1124
+ agentContext: agent.context,
1125
+ onUnavailable
1126
+ });
1127
+ return /* @__PURE__ */ jsx(AgentErrorBoundary, { onError: onUnavailable, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
1128
+ SalesAIChatWidget,
1129
+ {
1130
+ appKey: agent.appKey,
1131
+ apiBaseUrl: agent.apiBaseUrl,
1132
+ theme: agent.theme ?? "nui-light",
1133
+ defaultOpen: true,
1134
+ hideFab: true,
1135
+ placement: "host",
1136
+ ...agent.language === void 0 ? {} : { language: agent.language },
1137
+ ...agent.renderProduct === void 0 ? {} : { renderProduct: agent.renderProduct }
1138
+ }
1139
+ ) }) });
1140
+ }
1141
+ var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1142
+ function uniqueRecentSearches(queries) {
1143
+ const seen = /* @__PURE__ */ new Set();
1144
+ return queries.filter((query) => {
1145
+ const key = query.trim().toLowerCase();
1146
+ if (!key || seen.has(key)) return false;
1147
+ seen.add(key);
1148
+ return true;
1149
+ });
1150
+ }
1151
+ function ProductSearch({
1152
+ isOpen,
1153
+ onClose,
1154
+ service,
1155
+ navigate,
1156
+ isAuthenticated,
1157
+ agent,
1158
+ logo,
1159
+ cancelLabel,
1160
+ fallbackImageUrl,
1161
+ copy,
1162
+ gtmContext
1163
+ }) {
1164
+ const [flag, setFlag] = useState(void 0);
1165
+ const [handoff, setHandoff] = useState(null);
1166
+ const agentQuery = handoff?.query ?? null;
1167
+ const [pendingTerm, setPendingTerm] = useState(null);
1168
+ const [recentSearches, setRecentSearches] = useState([]);
1169
+ const search = useSearchSuggestions({
1170
+ service,
1171
+ enabled: isOpen && agentQuery === null
1172
+ });
1173
+ const {
1174
+ clearQuery,
1175
+ didYouMean,
1176
+ emptyFetched,
1177
+ hasMoreResults,
1178
+ loading,
1179
+ query,
1180
+ settledQuery,
1181
+ suggestionFetchFailed,
1182
+ suggestions,
1183
+ updateQuery
1184
+ } = search;
1185
+ const enabled = isNuiSearchEnabled({ isAuthenticated, flag });
1186
+ const items = getVisibleSuggestions({
1187
+ didYouMean,
1188
+ suggestions
1189
+ });
1190
+ const showDidYouMean = didYouMean.length > 0;
1191
+ useEffect(() => {
1192
+ if (isOpen) setFlag(resolveNuiSearchFlag());
1193
+ }, [isOpen]);
1194
+ useEffect(() => {
1195
+ if (isOpen) {
1196
+ setRecentSearches(
1197
+ uniqueRecentSearches(service.getRecentSearches())
1198
+ );
1199
+ }
1200
+ }, [isOpen, service, agentQuery]);
1201
+ const close = useCallback(() => {
1202
+ setHandoff(null);
1203
+ setPendingTerm(null);
1204
+ clearQuery();
1205
+ onClose();
1206
+ }, [clearQuery, onClose]);
1207
+ useEffect(() => {
1208
+ if (!isOpen) {
1209
+ setHandoff(null);
1210
+ setPendingTerm(null);
1211
+ clearQuery();
1212
+ }
1213
+ }, [isOpen, clearQuery]);
1214
+ const navigateTo = useCallback(
1215
+ (path) => {
1216
+ if (!path || path === "action-handled" || path === "action-pending") {
1217
+ return;
1218
+ }
1219
+ navigate(path);
1220
+ close();
1501
1221
  },
1502
- get,
1503
- api
1222
+ [navigate, close]
1504
1223
  );
1505
- api.getInitialState = () => configResult;
1506
- let stateFromStorage;
1507
- const hydrate = () => {
1508
- var _a, _b;
1509
- if (!storage) return;
1510
- hasHydrated = false;
1511
- hydrationListeners.forEach((cb) => {
1512
- var _a2;
1513
- return cb((_a2 = get()) != null ? _a2 : configResult);
1514
- });
1515
- const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;
1516
- return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {
1517
- if (deserializedStorageValue) {
1518
- if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) {
1519
- if (options.migrate) {
1520
- return [
1521
- true,
1522
- options.migrate(
1523
- deserializedStorageValue.state,
1524
- deserializedStorageValue.version
1525
- )
1526
- ];
1527
- }
1528
- console.error(
1529
- `State loaded from storage couldn't be migrated since no migrate function was provided`
1530
- );
1531
- } else {
1532
- return [false, deserializedStorageValue.state];
1533
- }
1224
+ const trackSemantic = useCallback(
1225
+ (productResultsData) => {
1226
+ trackProductSearch({
1227
+ ...gtmContext,
1228
+ searchType: SEARCH_GTM_SEARCH_TYPES.SEMANTIC,
1229
+ productResultsData
1230
+ });
1231
+ },
1232
+ [gtmContext]
1233
+ );
1234
+ const runSearch = useCallback(
1235
+ (term) => {
1236
+ trackSemantic(items.length > 0 ? items : { query: term });
1237
+ navigateTo(service.submitProductSearch(term));
1238
+ },
1239
+ [items, navigateTo, service, trackSemantic]
1240
+ );
1241
+ const decide = useCallback(
1242
+ (term) => decideSearchRoute({
1243
+ enabled,
1244
+ snapshot: {
1245
+ query: term,
1246
+ loading,
1247
+ settledQuery,
1248
+ suggestions,
1249
+ suggestionFetchFailed
1534
1250
  }
1535
- return [false, void 0];
1536
- }).then((migrationResult) => {
1537
- var _a2;
1538
- const [migrated, migratedState] = migrationResult;
1539
- stateFromStorage = options.merge(
1540
- migratedState,
1541
- (_a2 = get()) != null ? _a2 : configResult
1542
- );
1543
- set(stateFromStorage, true);
1544
- if (migrated) {
1545
- return setItem();
1251
+ }),
1252
+ [enabled, loading, settledQuery, suggestions, suggestionFetchFailed]
1253
+ );
1254
+ const act = useCallback(
1255
+ (term, route) => {
1256
+ setPendingTerm(null);
1257
+ if (route === "handoff") {
1258
+ trackChatEntryPoint(gtmContext ?? {});
1259
+ setHandoff({
1260
+ query: term,
1261
+ didYouMean: didYouMean.map((item) => item.value)
1262
+ });
1263
+ return;
1546
1264
  }
1547
- }).then(() => {
1548
- postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);
1549
- stateFromStorage = get();
1550
- hasHydrated = true;
1551
- finishHydrationListeners.forEach((cb) => cb(stateFromStorage));
1552
- }).catch((e) => {
1553
- postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);
1554
- });
1555
- };
1556
- api.persist = {
1557
- setOptions: (newOptions) => {
1558
- options = {
1559
- ...options,
1560
- ...newOptions
1561
- };
1562
- if (newOptions.storage) {
1563
- storage = newOptions.storage;
1265
+ runSearch(term);
1266
+ },
1267
+ [didYouMean, gtmContext, runSearch]
1268
+ );
1269
+ const handleSubmit = useCallback(
1270
+ (value) => {
1271
+ const term = value.trim();
1272
+ if (!term) return;
1273
+ const route = decide(term);
1274
+ if (route === "wait") {
1275
+ setPendingTerm(term);
1276
+ return;
1277
+ }
1278
+ act(term, route);
1279
+ },
1280
+ [act, decide]
1281
+ );
1282
+ const latest = useRef({ act, decide });
1283
+ latest.current = { act, decide };
1284
+ useEffect(() => {
1285
+ if (pendingTerm === null) return void 0;
1286
+ const route = latest.current.decide(pendingTerm);
1287
+ if (route !== "wait") {
1288
+ latest.current.act(pendingTerm, route);
1289
+ return void 0;
1290
+ }
1291
+ const timer = setTimeout(
1292
+ () => latest.current.act(pendingTerm, "search"),
1293
+ SUBMIT_WAIT_TIMEOUT_MS
1294
+ );
1295
+ return () => clearTimeout(timer);
1296
+ }, [
1297
+ pendingTerm,
1298
+ loading,
1299
+ settledQuery,
1300
+ suggestions,
1301
+ suggestionFetchFailed
1302
+ ]);
1303
+ const handleSelect = useCallback(
1304
+ (option) => {
1305
+ if (isSuggestionDisabled(
1306
+ option
1307
+ )) {
1308
+ return;
1564
1309
  }
1310
+ const term = String(option["value"] ?? option["label"] ?? "");
1311
+ if (term.trim()) trackSemantic([option]);
1312
+ const suggestionOption = option;
1313
+ const href = service.getSuggestionHref(suggestionOption);
1314
+ navigateTo(
1315
+ href || service.submitProductSearch(term, suggestionOption)
1316
+ );
1565
1317
  },
1566
- clearStorage: () => {
1567
- storage == null ? void 0 : storage.removeItem(options.name);
1318
+ [navigateTo, service, trackSemantic]
1319
+ );
1320
+ const handleRunAction = useCallback(
1321
+ (option) => {
1322
+ if (isSuggestionDisabled(
1323
+ option
1324
+ )) {
1325
+ return;
1326
+ }
1327
+ const result = service.activateSuggestionAction(
1328
+ option
1329
+ );
1330
+ void Promise.resolve(result).then(navigateTo).catch(() => void 0);
1568
1331
  },
1569
- getOptions: () => options,
1570
- rehydrate: () => hydrate(),
1571
- hasHydrated: () => hasHydrated,
1572
- onHydrate: (cb) => {
1573
- hydrationListeners.add(cb);
1574
- return () => {
1575
- hydrationListeners.delete(cb);
1576
- };
1332
+ [navigateTo, service]
1333
+ );
1334
+ const handleSeeMore = useCallback(() => {
1335
+ const term = query.trim();
1336
+ if (term) navigateTo(service.viewMoreResults({ query: term }));
1337
+ }, [navigateTo, query, service]);
1338
+ const handleRecentSelect = useCallback(
1339
+ (recentQuery) => {
1340
+ trackSemantic({ query: recentQuery });
1341
+ navigateTo(service.submitProductSearch(recentQuery));
1577
1342
  },
1578
- onFinishHydration: (cb) => {
1579
- finishHydrationListeners.add(cb);
1580
- return () => {
1581
- finishHydrationListeners.delete(cb);
1582
- };
1583
- }
1584
- };
1585
- if (!options.skipHydration) {
1586
- hydrate();
1587
- }
1588
- return stateFromStorage || configResult;
1589
- };
1590
- var persist = persistImpl;
1591
-
1592
- // src/store/searchHistoryStore.ts
1593
- var MAX_HISTORY = 10;
1594
- var useSearchHistoryStore = create()(
1595
- persist(
1596
- (set) => ({
1597
- entries: [],
1598
- addEntry: (entry) => {
1599
- const trimmed = entry.query.trim();
1600
- if (!trimmed) return;
1601
- set((state) => {
1602
- const deduped = state.entries.filter(
1603
- (e) => e.query.toLowerCase() !== trimmed.toLowerCase()
1604
- );
1605
- const newEntry = {
1606
- ...entry,
1607
- query: trimmed,
1608
- timestamp: Date.now()
1609
- };
1610
- return {
1611
- entries: [newEntry, ...deduped].slice(0, MAX_HISTORY)
1612
- };
1613
- });
1343
+ [navigateTo, service, trackSemantic]
1344
+ );
1345
+ const handleClearRecent = useCallback(() => {
1346
+ service.clearRecentSearches();
1347
+ setRecentSearches([]);
1348
+ }, [service]);
1349
+ const handleAgentUnavailable = useCallback(() => {
1350
+ if (agentQuery !== null)
1351
+ navigateTo(service.submitProductSearch(agentQuery));
1352
+ }, [agentQuery, navigateTo, service]);
1353
+ return /* @__PURE__ */ jsx(
1354
+ SearchModal,
1355
+ {
1356
+ isOpen,
1357
+ onClose: close,
1358
+ logo,
1359
+ ...cancelLabel === void 0 ? {} : { cancelLabel },
1360
+ ...fallbackImageUrl === void 0 ? {} : { fallbackImageUrl },
1361
+ ...copy === void 0 ? {} : { copy },
1362
+ input: {
1363
+ value: query,
1364
+ onChange: updateQuery,
1365
+ onSubmit: handleSubmit
1614
1366
  },
1615
- addQuery: (query) => {
1616
- const trimmed = query.trim();
1617
- if (!trimmed) return;
1618
- set((state) => {
1619
- const deduped = state.entries.filter(
1620
- (e) => e.query.toLowerCase() !== trimmed.toLowerCase()
1621
- );
1622
- return {
1623
- entries: [
1624
- {
1625
- query: trimmed,
1626
- isAI: false,
1627
- timestamp: Date.now()
1628
- },
1629
- ...deduped
1630
- ].slice(0, MAX_HISTORY)
1631
- };
1632
- });
1367
+ suggestions: {
1368
+ items,
1369
+ loading,
1370
+ showNoResults: !loading && emptyFetched && items.length === 0,
1371
+ showDidYouMean,
1372
+ hasMore: hasMoreResults,
1373
+ query,
1374
+ onSelect: handleSelect,
1375
+ onRunAction: handleRunAction,
1376
+ onSeeMore: handleSeeMore
1633
1377
  },
1634
- removeEntry: (query) => set((state) => ({
1635
- entries: state.entries.filter((e) => e.query !== query)
1636
- })),
1637
- clearAll: () => set({ entries: [] })
1638
- }),
1639
- {
1640
- name: "ai_search_recent_queries",
1641
- version: 1,
1642
- storage: createJSONStorage(
1643
- () => typeof window !== "undefined" ? window.sessionStorage : {}
1644
- ),
1645
- // Migrate from v0 (queries: string[]) to v1 (entries: SearchHistoryEntry[])
1646
- migrate(persistedState, version) {
1647
- if (version === 0) {
1648
- const old = persistedState;
1649
- if (Array.isArray(old?.queries)) {
1650
- return {
1651
- entries: old.queries.map((q) => ({
1652
- query: q,
1653
- isAI: false,
1654
- timestamp: Date.now()
1655
- }))
1656
- };
1657
- }
1378
+ recentSearches: {
1379
+ items: recentSearches,
1380
+ onSelect: handleRecentSelect,
1381
+ onClear: handleClearRecent
1382
+ },
1383
+ agentPanel: handoff === null ? void 0 : /* @__PURE__ */ jsx(
1384
+ NuiAgentPanel,
1385
+ {
1386
+ agent,
1387
+ query: handoff.query,
1388
+ handoffContext: {
1389
+ source: "storefront_search",
1390
+ trigger: "zero_results",
1391
+ query: handoff.query,
1392
+ did_you_mean: handoff.didYouMean
1393
+ },
1394
+ onUnavailable: handleAgentUnavailable
1658
1395
  }
1659
- return persistedState;
1660
- }
1396
+ )
1661
1397
  }
1662
- )
1663
- );
1664
-
1665
- // src/hooks/useSearchHistory.ts
1666
- function useSearchHistory() {
1667
- const entries = useSearchHistoryStore((s) => s.entries);
1668
- const addEntry = useSearchHistoryStore((s) => s.addEntry);
1669
- const addQuery = useSearchHistoryStore((s) => s.addQuery);
1670
- const removeEntry = useSearchHistoryStore((s) => s.removeEntry);
1671
- const clearAll = useSearchHistoryStore((s) => s.clearAll);
1672
- return {
1673
- entries,
1674
- queries: entries.map((e) => e.query),
1675
- addEntry,
1676
- addQuery,
1677
- removeEntry,
1678
- clearAll
1679
- };
1680
- }
1681
- function getSpeechRecognition() {
1682
- if (typeof window === "undefined") return null;
1683
- const w = window;
1684
- return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
1685
- }
1686
- function useVoiceSearch({
1687
- lang,
1688
- onInterimResult,
1689
- onResult,
1690
- onError
1691
- }) {
1692
- const [listening, setListening] = useState(false);
1693
- const recognitionRef = useRef(null);
1694
- const supported = getSpeechRecognition() !== null;
1695
- const stop = useCallback(() => {
1696
- recognitionRef.current?.stop();
1697
- recognitionRef.current = null;
1698
- setListening(false);
1699
- }, []);
1700
- const start = useCallback(() => {
1701
- const SR = getSpeechRecognition();
1702
- if (!SR) return;
1703
- stop();
1704
- const rec = new SR();
1705
- rec.lang = lang ?? navigator.language ?? "en-US";
1706
- rec.interimResults = true;
1707
- rec.maxAlternatives = 1;
1708
- rec.onresult = (event) => {
1709
- let finalTranscript = "";
1710
- let interimTranscript = "";
1711
- const startIndex = event.resultIndex ?? 0;
1712
- for (let index = startIndex; index < event.results.length; index += 1) {
1713
- const result = event.results[index];
1714
- const transcript = result?.[0]?.transcript ?? "";
1715
- if (!transcript) continue;
1716
- if (result?.isFinal) {
1717
- finalTranscript += transcript;
1718
- } else {
1719
- interimTranscript += transcript;
1720
- }
1721
- }
1722
- if (interimTranscript) onInterimResult?.(interimTranscript);
1723
- if (finalTranscript) onResult(finalTranscript);
1724
- };
1725
- rec.onerror = (event) => {
1726
- onError?.(event);
1727
- setListening(false);
1728
- };
1729
- rec.onend = () => setListening(false);
1730
- recognitionRef.current = rec;
1731
- rec.start();
1732
- setListening(true);
1733
- }, [lang, onInterimResult, onResult, onError, stop]);
1734
- return { listening, supported, start, stop };
1398
+ );
1735
1399
  }
1736
1400
 
1737
- 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 };
1401
+ export { CATALOG_SUGGESTIONS_PATH, DEBOUNCE_MS, DEFAULT_SEARCH_PATH, DEFAULT_SUGGESTION_PAGE, DEFAULT_SUGGESTION_SIZE, MAX_VISIBLE_SUGGESTIONS, NUI_SEARCH_FLAG, ProductSearch, RECENT_SEARCHES_DEFAULT_LIMIT, RECENT_SEARCHES_PREFIX, SEARCH_GTM_EVENTS, SEARCH_GTM_SEARCH_TYPES, SEARCH_GTM_USER_STATUS, SEARCH_PLACEHOLDER, STOREFRONT_SHOPPING_CONTEXT, SUBMIT_WAIT_TIMEOUT_MS, TRIGGER_PLACEHOLDER, createSearchService, decideSearchRoute, getImageFromProperty, getIngredientTerms, getLocaleFromUrl, getSeeMoreResultsPayload, getVisibleSuggestions, isCartActionDisabled, isNuiSearchEnabled, isSellableProductStatus, isSuggestionDisabled, mapCatalogProductToSuggestion, normalizeDidYouMean, pushGtmEvent, resolveApiGatewayPath, resolveNuiSearchFlag, resolveSuggestionImageSrc, trackChatEntryPoint, trackErrorPopup, trackProductSearch, trackSearchType, useSearchSuggestions };
1738
1402
  //# sourceMappingURL=index.mjs.map
1739
1403
  //# sourceMappingURL=index.mjs.map