@nuskin/react-search-sdk 1.0.0-comx-2233.4 → 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.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import React from 'react';
2
- import * as zustand_middleware from 'zustand/middleware';
3
- import * as zustand from 'zustand';
1
+ import React, { ReactNode, ReactElement } from 'react';
2
+ import { SearchModalCopy } from '@nuskin/react-search-components';
3
+ import { SalesAIChatWidgetProps } from '@nuskin/react-nui-chat-sdk';
4
4
 
5
5
  /**
6
6
  * Shared TypeScript types for the search SDK.
@@ -156,6 +156,12 @@ interface UseSearchSuggestionsResult {
156
156
  loading: boolean;
157
157
  /** The current query string (unmodified, as typed by the user). */
158
158
  query: string;
159
+ /**
160
+ * The trimmed query the current results (or failure) belong to; `""` until a
161
+ * fetch has finished. Compare with `query.trim()` to know whether the
162
+ * results on screen are for the term the user has typed.
163
+ */
164
+ settledQuery: string;
159
165
  /** Raw React state setter — prefer `updateQuery` for controlled inputs. */
160
166
  setQuery: React.Dispatch<React.SetStateAction<string>>;
161
167
  /** Normalised product suggestion items from the catalog API. */
@@ -264,55 +270,6 @@ declare function resolveApiGatewayPath(apiGatewayPath: string | undefined | null
264
270
  */
265
271
  declare function createSearchService(options?: SearchServiceOptions): SearchService;
266
272
 
267
- /**
268
- * ChatService — two-step NuSkin AI chat API.
269
- *
270
- * Step 1: POST /chat/conversations → receives conversation_id
271
- * Step 2: POST /chat/messages → SSE stream parsed with eventsource-parser
272
- *
273
- * Products are embedded in AI response content as ```products [...] ``` code blocks.
274
- */
275
- interface ChatProduct {
276
- sku: string;
277
- name: string;
278
- category?: string;
279
- pv?: number;
280
- retail_price?: number;
281
- description?: string;
282
- image_url?: string;
283
- badge?: string;
284
- [key: string]: unknown;
285
- }
286
- interface ChatMessageResult {
287
- summary: string;
288
- products: ChatProduct[];
289
- /** Raw accumulated content including markdown, for downstream rendering. */
290
- rawContent: string;
291
- }
292
- interface ChatServiceOptions {
293
- /** POST endpoint for creating a conversation. */
294
- conversationsUrl?: string;
295
- /** POST endpoint for sending a message (SSE response). */
296
- messagesUrl?: string;
297
- /** X-Sales-AI-App-Key header value (from CS configurations). */
298
- salesAiAppKey?: string;
299
- /** X-Sales-AI-Parent-Origin header value (from CS configurations). */
300
- salesAiParentOrigin?: string;
301
- /** Market code (e.g., "US", "JP") for scope_context in message requests. */
302
- market?: string;
303
- /** Language code (e.g., "en", "ja") for scope_context in message requests. */
304
- language?: string;
305
- /** Override fetch for testing. */
306
- fetchImpl?: typeof fetch | null;
307
- }
308
- interface ChatService {
309
- /** Creates a conversation and returns the numeric conversation ID. */
310
- createConversation: (title?: string) => Promise<number>;
311
- /** Sends a message and streams back a parsed result. */
312
- sendMessage: (conversationId: number, content: string, onChunk?: (type: string, text: string) => void) => Promise<ChatMessageResult>;
313
- }
314
- declare function createChatService(options: ChatServiceOptions): ChatService;
315
-
316
273
  /**
317
274
  * useSearchSuggestions — Phase 0 lift-and-shift of
318
275
  * `header-mfe/src/components/search/auto-complete/hooks/useAutoComplete.js`
@@ -344,64 +301,6 @@ declare function createChatService(options: ChatServiceOptions): ChatService;
344
301
  */
345
302
  declare function useSearchSuggestions({ service, enabled, }: UseSearchSuggestionsOptions): UseSearchSuggestionsResult;
346
303
 
347
- interface SearchHistoryEntry {
348
- query: string;
349
- isAI: boolean;
350
- /** Serialized AI result payload; consumers cast to their concrete result type */
351
- aiResult?: unknown;
352
- timestamp: number;
353
- }
354
- interface SearchHistoryState {
355
- entries: SearchHistoryEntry[];
356
- /** Save a full entry (AI or non-AI); deduplicates by query (case-insensitive). */
357
- addEntry: (entry: Omit<SearchHistoryEntry, 'timestamp'>) => void;
358
- /** Convenience alias — saves a plain non-AI query entry. */
359
- addQuery: (query: string) => void;
360
- removeEntry: (query: string) => void;
361
- clearAll: () => void;
362
- }
363
- declare const useSearchHistoryStore: zustand.UseBoundStore<Omit<zustand.StoreApi<SearchHistoryState>, "persist"> & {
364
- persist: {
365
- setOptions: (options: Partial<zustand_middleware.PersistOptions<SearchHistoryState, unknown>>) => void;
366
- clearStorage: () => void;
367
- rehydrate: () => Promise<void> | void;
368
- hasHydrated: () => boolean;
369
- onHydrate: (fn: (state: SearchHistoryState) => void) => () => void;
370
- onFinishHydration: (fn: (state: SearchHistoryState) => void) => () => void;
371
- getOptions: () => Partial<zustand_middleware.PersistOptions<SearchHistoryState, unknown>>;
372
- };
373
- }>;
374
-
375
- interface UseSearchHistoryResult {
376
- entries: SearchHistoryEntry[];
377
- /** Derived string list of queries (backward compat) */
378
- queries: string[];
379
- addEntry: (entry: Omit<SearchHistoryEntry, 'timestamp'>) => void;
380
- /** @deprecated prefer addEntry */
381
- addQuery: (query: string) => void;
382
- removeEntry: (query: string) => void;
383
- clearAll: () => void;
384
- }
385
- /** React hook that subscribes to the tab-scoped search history store. */
386
- declare function useSearchHistory(): UseSearchHistoryResult;
387
-
388
- interface SpeechRecognitionErrorEventLike {
389
- error: string;
390
- }
391
- interface UseVoiceSearchOptions {
392
- lang?: string;
393
- onResult: (transcript: string) => void;
394
- onInterimResult?: (transcript: string) => void;
395
- onError?: (error: SpeechRecognitionErrorEventLike) => void;
396
- }
397
- interface UseVoiceSearchResult {
398
- listening: boolean;
399
- supported: boolean;
400
- start: () => void;
401
- stop: () => void;
402
- }
403
- declare function useVoiceSearch({ lang, onInterimResult, onResult, onError, }: UseVoiceSearchOptions): UseVoiceSearchResult;
404
-
405
304
  /**
406
305
  * Catalog-product normalisation helpers — Phase 0 lift-and-shift of the
407
306
  * relevant portions of
@@ -491,7 +390,7 @@ type SearchGtmSearchType = 'semantic search' | 'AI agent';
491
390
  * (matches the placeholder shape the data team provided in COMX-3277).
492
391
  */
493
392
  interface SearchGtmContext {
494
- /** Conversation id from ChatService.createConversation, coerced to string. */
393
+ /** NUi conversation id (the widget's `onConversationChange`), coerced to string. */
495
394
  chatId?: string | number;
496
395
  /** Page type where the chat was initiated (e.g. `plp`, `pdp`, `home`). */
497
396
  pageType?: string;
@@ -534,6 +433,122 @@ interface ErrorPopupEventInput extends SearchGtmContext {
534
433
  searchType?: SearchGtmSearchType;
535
434
  }
536
435
 
436
+ /** How the host's NUi app is reached and presented. */
437
+ interface NuiAgentConfig {
438
+ /** The NUi app key for the search surface. */
439
+ appKey: string;
440
+ /** Origin of the NUi API. */
441
+ apiBaseUrl: string;
442
+ /** UI language, e.g. `en-US`. */
443
+ language?: string;
444
+ /** Widget theme. Defaults to `nui-light`. */
445
+ theme?: SalesAIChatWidgetProps['theme'];
446
+ /** Context sent with every turn of the conversation, under the handoff context. */
447
+ context?: Record<string, unknown>;
448
+ /** Lets the host draw the agent's product recommendations. */
449
+ renderProduct?: SalesAIChatWidgetProps['renderProduct'];
450
+ }
451
+
452
+ /** How long a submit waits for the typeahead of that exact term before it stays on search. */
453
+ declare const SUBMIT_WAIT_TIMEOUT_MS = 2000;
454
+ interface ProductSearchProps {
455
+ isOpen: boolean;
456
+ onClose: () => void;
457
+ /** From `createSearchService`. Keep its identity stable (memoize it). */
458
+ service: SearchService;
459
+ /** Takes the shopper to a path, using the host's router. The panel closes afterwards. */
460
+ navigate: (path: string) => void;
461
+ /** Whether the visitor is signed in (the storefront's `customerData.isAuthenticated`). */
462
+ isAuthenticated: boolean;
463
+ /** The NUi app the agent runs on. */
464
+ agent: NuiAgentConfig;
465
+ logo?: ReactNode;
466
+ cancelLabel?: string;
467
+ fallbackImageUrl?: string;
468
+ copy?: SearchModalCopy;
469
+ /** Host-known values for the search analytics events. */
470
+ gtmContext?: Omit<SearchGtmContext, 'chatId'>;
471
+ }
472
+ /**
473
+ * Product search with an agent fallback.
474
+ *
475
+ * Shows Solr typeahead and recent searches in the library's `SearchModal`. When a signed-in
476
+ * shopper submits a term whose typeahead shows no products, the NUi agent takes over in the same
477
+ * panel with that term as the first message. Everything else behaves as plain search.
478
+ */
479
+ declare function ProductSearch({ isOpen, onClose, service, navigate, isAuthenticated, agent, logo, cancelLabel, fallbackImageUrl, copy, gtmContext, }: Readonly<ProductSearchProps>): ReactElement;
480
+
481
+ /**
482
+ * The enable gate for NUi search.
483
+ *
484
+ * NUi search is on only for signed-in visitors who have switched it on with
485
+ * the `enableNuiSearch` flag, set by a cookie or by a URL query parameter
486
+ * (`?enableNuiSearch=true` or `=false`). The query parameter also writes the
487
+ * cookie, so the choice sticks while the visitor navigates. Off by default.
488
+ *
489
+ * The flag is a client-side feature toggle, not a security control. Real
490
+ * per-market and per-locale switching is a later step.
491
+ */
492
+ /** Name shared by the cookie and the URL query parameter. */
493
+ declare const NUI_SEARCH_FLAG = "enableNuiSearch";
494
+ /** Where the flag is read from and written to. Defaults to the browser. */
495
+ interface NuiSearchFlagEnv {
496
+ /** Query string, with or without the leading `?`. */
497
+ search?: string;
498
+ /** Cookie string, as in `document.cookie`. */
499
+ cookie?: string;
500
+ /** Called with a cookie string to store when the query parameter is set. */
501
+ setCookie?: (cookie: string) => void;
502
+ }
503
+ /**
504
+ * Resolves the flag. A valid query parameter wins over the cookie and is
505
+ * written back to the cookie; otherwise the cookie decides. `undefined` means
506
+ * it was never set. Safe to call during server rendering.
507
+ */
508
+ declare function resolveNuiSearchFlag(env?: NuiSearchFlagEnv): boolean | undefined;
509
+ interface IsNuiSearchEnabledInput {
510
+ /** From the host (the storefront's `customerData.isAuthenticated`). */
511
+ isAuthenticated: boolean | undefined;
512
+ /** The resolved flag; see `resolveNuiSearchFlag`. */
513
+ flag: boolean | undefined;
514
+ }
515
+ /** On only when the visitor is signed in and the flag is explicitly on. */
516
+ declare function isNuiSearchEnabled({ isAuthenticated, flag, }: IsNuiSearchEnabledInput): boolean;
517
+
518
+ /**
519
+ * Routing between Solr search and the NUi agent.
520
+ *
521
+ * Rule: when the shopper submits a search and the typeahead response for that
522
+ * exact term shows no products, the NUi widget takes over in the same panel.
523
+ * Anything else stays plain search. A pure function, so a smarter intent
524
+ * heuristic can replace it later without touching any UI.
525
+ */
526
+
527
+ /**
528
+ * - `search` — stay on Solr search (submit behaves as it does today).
529
+ * - `handoff` — hand the query to the NUi agent.
530
+ * - `wait` — the typeahead for this term has not settled yet; ask again when
531
+ * the snapshot changes. Callers should apply their own timeout and
532
+ * fall back to `search`, so a slow request never blocks a submit.
533
+ */
534
+ type SearchRoute = 'search' | 'handoff' | 'wait';
535
+ /** The slice of `useSearchSuggestions` state the rule reads. */
536
+ type RoutingSnapshot = Pick<UseSearchSuggestionsResult, 'query' | 'loading' | 'settledQuery' | 'suggestions' | 'suggestionFetchFailed'>;
537
+ interface DecideSearchRouteInput {
538
+ /** Whether NUi search is on for this visitor (see `isNuiSearchEnabled`). */
539
+ enabled: boolean;
540
+ snapshot: RoutingSnapshot;
541
+ }
542
+ /**
543
+ * Decides what a submitted search does.
544
+ *
545
+ * "No products" means the hook has nothing to show for the term. A response
546
+ * that only offers "did you mean" terms counts as no products: the hook hides
547
+ * the products in that case, so the shopper sees none either. A failed request
548
+ * is not "no products": an outage must not send every shopper to the agent.
549
+ */
550
+ declare function decideSearchRoute({ enabled, snapshot, }: DecideSearchRouteInput): SearchRoute;
551
+
537
552
  /**
538
553
  * Extracts `{ countryCode, language }` from a storefront-shaped path
539
554
  * (`/{country}/{language}/...`, e.g. `/us/en/product/x`).
@@ -633,15 +648,5 @@ declare const MAX_VISIBLE_SUGGESTIONS = 6;
633
648
  declare const SEARCH_PLACEHOLDER = "Search by product name, ingredient, benefits, or SKU";
634
649
  /** Fallback placeholder shown on the collapsed search trigger. */
635
650
  declare const TRIGGER_PLACEHOLDER = "Product Search";
636
- /**
637
- * Full path segment for the AI commerce chat SSE endpoint.
638
- * Appended to the consumer-provided `chatBaseUrl`.
639
- * Source: https://admin.tst.nuskin.ai/api/analytics/v1/embed
640
- */
641
- declare const DEFAULT_AI_CHAT_BASE_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed";
642
- /** Default POST endpoint for creating an AI chat conversation. */
643
- declare const DEFAULT_AI_CHAT_CONVERSATIONS_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed/conversations";
644
- /** Default POST endpoint for sending AI chat messages. */
645
- declare const DEFAULT_AI_CHAT_MESSAGES_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed/messages";
646
651
 
647
- export { CATALOG_SUGGESTIONS_PATH, type CatalogSuggestionItem, type ChatMessageResult, type ChatProduct, type ChatService, type ChatServiceOptions, 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, type DidYouMeanItem, type ErrorPopupEventInput, type GetMarketConfig, type LocaleParts, type LocationLike, MAX_VISIBLE_SUGGESTIONS, type MarketConfig, type ProductSearchEventInput, type ProductType, RECENT_SEARCHES_DEFAULT_LIMIT, RECENT_SEARCHES_PREFIX, SEARCH_GTM_EVENTS, SEARCH_GTM_SEARCH_TYPES, SEARCH_GTM_USER_STATUS, SEARCH_PLACEHOLDER, STOREFRONT_SHOPPING_CONTEXT, type SearchChromeCopy, type SearchGtmContext, type SearchGtmSearchType, type SearchGtmUserStatus, type SearchHistoryEntry, type SearchHistoryState, type SearchHistoryEntry as SearchHistoryStoreEntry, type SearchService, type SearchServiceOptions, type SearchTypeEventInput, type SuggestionOption, type SuggestionsResponse, TRIGGER_PLACEHOLDER, type UseSearchHistoryResult, type UseSearchSuggestionsOptions, type UseSearchSuggestionsResult, type UseVoiceSearchOptions, type UseVoiceSearchResult, createChatService, createSearchService, getImageFromProperty, getIngredientTerms, getLocaleFromUrl, getSeeMoreResultsPayload, getVisibleSuggestions, isCartActionDisabled, isSellableProductStatus, isSuggestionDisabled, mapCatalogProductToSuggestion, normalizeDidYouMean, pushGtmEvent, resolveApiGatewayPath, resolveSuggestionImageSrc, trackChatEntryPoint, trackErrorPopup, trackProductSearch, trackSearchType, useSearchHistory, useSearchHistoryStore, useSearchSuggestions, useVoiceSearch };
652
+ export { CATALOG_SUGGESTIONS_PATH, type CatalogSuggestionItem, DEBOUNCE_MS, DEFAULT_SEARCH_PATH, DEFAULT_SUGGESTION_PAGE, DEFAULT_SUGGESTION_SIZE, type DecideSearchRouteInput, type DidYouMeanItem, type ErrorPopupEventInput, type GetMarketConfig, type IsNuiSearchEnabledInput, type LocaleParts, type LocationLike, MAX_VISIBLE_SUGGESTIONS, type MarketConfig, NUI_SEARCH_FLAG, type NuiAgentConfig, type NuiSearchFlagEnv, ProductSearch, type ProductSearchEventInput, type ProductSearchProps, type ProductType, RECENT_SEARCHES_DEFAULT_LIMIT, RECENT_SEARCHES_PREFIX, type RoutingSnapshot, SEARCH_GTM_EVENTS, SEARCH_GTM_SEARCH_TYPES, SEARCH_GTM_USER_STATUS, SEARCH_PLACEHOLDER, STOREFRONT_SHOPPING_CONTEXT, SUBMIT_WAIT_TIMEOUT_MS, type SearchChromeCopy, type SearchGtmContext, type SearchGtmSearchType, type SearchGtmUserStatus, type SearchRoute, type SearchService, type SearchServiceOptions, type SearchTypeEventInput, type SuggestionOption, type SuggestionsResponse, TRIGGER_PLACEHOLDER, type UseSearchSuggestionsOptions, type UseSearchSuggestionsResult, createSearchService, decideSearchRoute, getImageFromProperty, getIngredientTerms, getLocaleFromUrl, getSeeMoreResultsPayload, getVisibleSuggestions, isCartActionDisabled, isNuiSearchEnabled, isSellableProductStatus, isSuggestionDisabled, mapCatalogProductToSuggestion, normalizeDidYouMean, pushGtmEvent, resolveApiGatewayPath, resolveNuiSearchFlag, resolveSuggestionImageSrc, trackChatEntryPoint, trackErrorPopup, trackProductSearch, trackSearchType, useSearchSuggestions };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import React from 'react';
2
- import * as zustand_middleware from 'zustand/middleware';
3
- import * as zustand from 'zustand';
1
+ import React, { ReactNode, ReactElement } from 'react';
2
+ import { SearchModalCopy } from '@nuskin/react-search-components';
3
+ import { SalesAIChatWidgetProps } from '@nuskin/react-nui-chat-sdk';
4
4
 
5
5
  /**
6
6
  * Shared TypeScript types for the search SDK.
@@ -156,6 +156,12 @@ interface UseSearchSuggestionsResult {
156
156
  loading: boolean;
157
157
  /** The current query string (unmodified, as typed by the user). */
158
158
  query: string;
159
+ /**
160
+ * The trimmed query the current results (or failure) belong to; `""` until a
161
+ * fetch has finished. Compare with `query.trim()` to know whether the
162
+ * results on screen are for the term the user has typed.
163
+ */
164
+ settledQuery: string;
159
165
  /** Raw React state setter — prefer `updateQuery` for controlled inputs. */
160
166
  setQuery: React.Dispatch<React.SetStateAction<string>>;
161
167
  /** Normalised product suggestion items from the catalog API. */
@@ -264,55 +270,6 @@ declare function resolveApiGatewayPath(apiGatewayPath: string | undefined | null
264
270
  */
265
271
  declare function createSearchService(options?: SearchServiceOptions): SearchService;
266
272
 
267
- /**
268
- * ChatService — two-step NuSkin AI chat API.
269
- *
270
- * Step 1: POST /chat/conversations → receives conversation_id
271
- * Step 2: POST /chat/messages → SSE stream parsed with eventsource-parser
272
- *
273
- * Products are embedded in AI response content as ```products [...] ``` code blocks.
274
- */
275
- interface ChatProduct {
276
- sku: string;
277
- name: string;
278
- category?: string;
279
- pv?: number;
280
- retail_price?: number;
281
- description?: string;
282
- image_url?: string;
283
- badge?: string;
284
- [key: string]: unknown;
285
- }
286
- interface ChatMessageResult {
287
- summary: string;
288
- products: ChatProduct[];
289
- /** Raw accumulated content including markdown, for downstream rendering. */
290
- rawContent: string;
291
- }
292
- interface ChatServiceOptions {
293
- /** POST endpoint for creating a conversation. */
294
- conversationsUrl?: string;
295
- /** POST endpoint for sending a message (SSE response). */
296
- messagesUrl?: string;
297
- /** X-Sales-AI-App-Key header value (from CS configurations). */
298
- salesAiAppKey?: string;
299
- /** X-Sales-AI-Parent-Origin header value (from CS configurations). */
300
- salesAiParentOrigin?: string;
301
- /** Market code (e.g., "US", "JP") for scope_context in message requests. */
302
- market?: string;
303
- /** Language code (e.g., "en", "ja") for scope_context in message requests. */
304
- language?: string;
305
- /** Override fetch for testing. */
306
- fetchImpl?: typeof fetch | null;
307
- }
308
- interface ChatService {
309
- /** Creates a conversation and returns the numeric conversation ID. */
310
- createConversation: (title?: string) => Promise<number>;
311
- /** Sends a message and streams back a parsed result. */
312
- sendMessage: (conversationId: number, content: string, onChunk?: (type: string, text: string) => void) => Promise<ChatMessageResult>;
313
- }
314
- declare function createChatService(options: ChatServiceOptions): ChatService;
315
-
316
273
  /**
317
274
  * useSearchSuggestions — Phase 0 lift-and-shift of
318
275
  * `header-mfe/src/components/search/auto-complete/hooks/useAutoComplete.js`
@@ -344,64 +301,6 @@ declare function createChatService(options: ChatServiceOptions): ChatService;
344
301
  */
345
302
  declare function useSearchSuggestions({ service, enabled, }: UseSearchSuggestionsOptions): UseSearchSuggestionsResult;
346
303
 
347
- interface SearchHistoryEntry {
348
- query: string;
349
- isAI: boolean;
350
- /** Serialized AI result payload; consumers cast to their concrete result type */
351
- aiResult?: unknown;
352
- timestamp: number;
353
- }
354
- interface SearchHistoryState {
355
- entries: SearchHistoryEntry[];
356
- /** Save a full entry (AI or non-AI); deduplicates by query (case-insensitive). */
357
- addEntry: (entry: Omit<SearchHistoryEntry, 'timestamp'>) => void;
358
- /** Convenience alias — saves a plain non-AI query entry. */
359
- addQuery: (query: string) => void;
360
- removeEntry: (query: string) => void;
361
- clearAll: () => void;
362
- }
363
- declare const useSearchHistoryStore: zustand.UseBoundStore<Omit<zustand.StoreApi<SearchHistoryState>, "persist"> & {
364
- persist: {
365
- setOptions: (options: Partial<zustand_middleware.PersistOptions<SearchHistoryState, unknown>>) => void;
366
- clearStorage: () => void;
367
- rehydrate: () => Promise<void> | void;
368
- hasHydrated: () => boolean;
369
- onHydrate: (fn: (state: SearchHistoryState) => void) => () => void;
370
- onFinishHydration: (fn: (state: SearchHistoryState) => void) => () => void;
371
- getOptions: () => Partial<zustand_middleware.PersistOptions<SearchHistoryState, unknown>>;
372
- };
373
- }>;
374
-
375
- interface UseSearchHistoryResult {
376
- entries: SearchHistoryEntry[];
377
- /** Derived string list of queries (backward compat) */
378
- queries: string[];
379
- addEntry: (entry: Omit<SearchHistoryEntry, 'timestamp'>) => void;
380
- /** @deprecated prefer addEntry */
381
- addQuery: (query: string) => void;
382
- removeEntry: (query: string) => void;
383
- clearAll: () => void;
384
- }
385
- /** React hook that subscribes to the tab-scoped search history store. */
386
- declare function useSearchHistory(): UseSearchHistoryResult;
387
-
388
- interface SpeechRecognitionErrorEventLike {
389
- error: string;
390
- }
391
- interface UseVoiceSearchOptions {
392
- lang?: string;
393
- onResult: (transcript: string) => void;
394
- onInterimResult?: (transcript: string) => void;
395
- onError?: (error: SpeechRecognitionErrorEventLike) => void;
396
- }
397
- interface UseVoiceSearchResult {
398
- listening: boolean;
399
- supported: boolean;
400
- start: () => void;
401
- stop: () => void;
402
- }
403
- declare function useVoiceSearch({ lang, onInterimResult, onResult, onError, }: UseVoiceSearchOptions): UseVoiceSearchResult;
404
-
405
304
  /**
406
305
  * Catalog-product normalisation helpers — Phase 0 lift-and-shift of the
407
306
  * relevant portions of
@@ -491,7 +390,7 @@ type SearchGtmSearchType = 'semantic search' | 'AI agent';
491
390
  * (matches the placeholder shape the data team provided in COMX-3277).
492
391
  */
493
392
  interface SearchGtmContext {
494
- /** Conversation id from ChatService.createConversation, coerced to string. */
393
+ /** NUi conversation id (the widget's `onConversationChange`), coerced to string. */
495
394
  chatId?: string | number;
496
395
  /** Page type where the chat was initiated (e.g. `plp`, `pdp`, `home`). */
497
396
  pageType?: string;
@@ -534,6 +433,122 @@ interface ErrorPopupEventInput extends SearchGtmContext {
534
433
  searchType?: SearchGtmSearchType;
535
434
  }
536
435
 
436
+ /** How the host's NUi app is reached and presented. */
437
+ interface NuiAgentConfig {
438
+ /** The NUi app key for the search surface. */
439
+ appKey: string;
440
+ /** Origin of the NUi API. */
441
+ apiBaseUrl: string;
442
+ /** UI language, e.g. `en-US`. */
443
+ language?: string;
444
+ /** Widget theme. Defaults to `nui-light`. */
445
+ theme?: SalesAIChatWidgetProps['theme'];
446
+ /** Context sent with every turn of the conversation, under the handoff context. */
447
+ context?: Record<string, unknown>;
448
+ /** Lets the host draw the agent's product recommendations. */
449
+ renderProduct?: SalesAIChatWidgetProps['renderProduct'];
450
+ }
451
+
452
+ /** How long a submit waits for the typeahead of that exact term before it stays on search. */
453
+ declare const SUBMIT_WAIT_TIMEOUT_MS = 2000;
454
+ interface ProductSearchProps {
455
+ isOpen: boolean;
456
+ onClose: () => void;
457
+ /** From `createSearchService`. Keep its identity stable (memoize it). */
458
+ service: SearchService;
459
+ /** Takes the shopper to a path, using the host's router. The panel closes afterwards. */
460
+ navigate: (path: string) => void;
461
+ /** Whether the visitor is signed in (the storefront's `customerData.isAuthenticated`). */
462
+ isAuthenticated: boolean;
463
+ /** The NUi app the agent runs on. */
464
+ agent: NuiAgentConfig;
465
+ logo?: ReactNode;
466
+ cancelLabel?: string;
467
+ fallbackImageUrl?: string;
468
+ copy?: SearchModalCopy;
469
+ /** Host-known values for the search analytics events. */
470
+ gtmContext?: Omit<SearchGtmContext, 'chatId'>;
471
+ }
472
+ /**
473
+ * Product search with an agent fallback.
474
+ *
475
+ * Shows Solr typeahead and recent searches in the library's `SearchModal`. When a signed-in
476
+ * shopper submits a term whose typeahead shows no products, the NUi agent takes over in the same
477
+ * panel with that term as the first message. Everything else behaves as plain search.
478
+ */
479
+ declare function ProductSearch({ isOpen, onClose, service, navigate, isAuthenticated, agent, logo, cancelLabel, fallbackImageUrl, copy, gtmContext, }: Readonly<ProductSearchProps>): ReactElement;
480
+
481
+ /**
482
+ * The enable gate for NUi search.
483
+ *
484
+ * NUi search is on only for signed-in visitors who have switched it on with
485
+ * the `enableNuiSearch` flag, set by a cookie or by a URL query parameter
486
+ * (`?enableNuiSearch=true` or `=false`). The query parameter also writes the
487
+ * cookie, so the choice sticks while the visitor navigates. Off by default.
488
+ *
489
+ * The flag is a client-side feature toggle, not a security control. Real
490
+ * per-market and per-locale switching is a later step.
491
+ */
492
+ /** Name shared by the cookie and the URL query parameter. */
493
+ declare const NUI_SEARCH_FLAG = "enableNuiSearch";
494
+ /** Where the flag is read from and written to. Defaults to the browser. */
495
+ interface NuiSearchFlagEnv {
496
+ /** Query string, with or without the leading `?`. */
497
+ search?: string;
498
+ /** Cookie string, as in `document.cookie`. */
499
+ cookie?: string;
500
+ /** Called with a cookie string to store when the query parameter is set. */
501
+ setCookie?: (cookie: string) => void;
502
+ }
503
+ /**
504
+ * Resolves the flag. A valid query parameter wins over the cookie and is
505
+ * written back to the cookie; otherwise the cookie decides. `undefined` means
506
+ * it was never set. Safe to call during server rendering.
507
+ */
508
+ declare function resolveNuiSearchFlag(env?: NuiSearchFlagEnv): boolean | undefined;
509
+ interface IsNuiSearchEnabledInput {
510
+ /** From the host (the storefront's `customerData.isAuthenticated`). */
511
+ isAuthenticated: boolean | undefined;
512
+ /** The resolved flag; see `resolveNuiSearchFlag`. */
513
+ flag: boolean | undefined;
514
+ }
515
+ /** On only when the visitor is signed in and the flag is explicitly on. */
516
+ declare function isNuiSearchEnabled({ isAuthenticated, flag, }: IsNuiSearchEnabledInput): boolean;
517
+
518
+ /**
519
+ * Routing between Solr search and the NUi agent.
520
+ *
521
+ * Rule: when the shopper submits a search and the typeahead response for that
522
+ * exact term shows no products, the NUi widget takes over in the same panel.
523
+ * Anything else stays plain search. A pure function, so a smarter intent
524
+ * heuristic can replace it later without touching any UI.
525
+ */
526
+
527
+ /**
528
+ * - `search` — stay on Solr search (submit behaves as it does today).
529
+ * - `handoff` — hand the query to the NUi agent.
530
+ * - `wait` — the typeahead for this term has not settled yet; ask again when
531
+ * the snapshot changes. Callers should apply their own timeout and
532
+ * fall back to `search`, so a slow request never blocks a submit.
533
+ */
534
+ type SearchRoute = 'search' | 'handoff' | 'wait';
535
+ /** The slice of `useSearchSuggestions` state the rule reads. */
536
+ type RoutingSnapshot = Pick<UseSearchSuggestionsResult, 'query' | 'loading' | 'settledQuery' | 'suggestions' | 'suggestionFetchFailed'>;
537
+ interface DecideSearchRouteInput {
538
+ /** Whether NUi search is on for this visitor (see `isNuiSearchEnabled`). */
539
+ enabled: boolean;
540
+ snapshot: RoutingSnapshot;
541
+ }
542
+ /**
543
+ * Decides what a submitted search does.
544
+ *
545
+ * "No products" means the hook has nothing to show for the term. A response
546
+ * that only offers "did you mean" terms counts as no products: the hook hides
547
+ * the products in that case, so the shopper sees none either. A failed request
548
+ * is not "no products": an outage must not send every shopper to the agent.
549
+ */
550
+ declare function decideSearchRoute({ enabled, snapshot, }: DecideSearchRouteInput): SearchRoute;
551
+
537
552
  /**
538
553
  * Extracts `{ countryCode, language }` from a storefront-shaped path
539
554
  * (`/{country}/{language}/...`, e.g. `/us/en/product/x`).
@@ -633,15 +648,5 @@ declare const MAX_VISIBLE_SUGGESTIONS = 6;
633
648
  declare const SEARCH_PLACEHOLDER = "Search by product name, ingredient, benefits, or SKU";
634
649
  /** Fallback placeholder shown on the collapsed search trigger. */
635
650
  declare const TRIGGER_PLACEHOLDER = "Product Search";
636
- /**
637
- * Full path segment for the AI commerce chat SSE endpoint.
638
- * Appended to the consumer-provided `chatBaseUrl`.
639
- * Source: https://admin.tst.nuskin.ai/api/analytics/v1/embed
640
- */
641
- declare const DEFAULT_AI_CHAT_BASE_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed";
642
- /** Default POST endpoint for creating an AI chat conversation. */
643
- declare const DEFAULT_AI_CHAT_CONVERSATIONS_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed/conversations";
644
- /** Default POST endpoint for sending AI chat messages. */
645
- declare const DEFAULT_AI_CHAT_MESSAGES_URL = "https://admin.tst.nuskin.ai/api/analytics/v1/embed/messages";
646
651
 
647
- export { CATALOG_SUGGESTIONS_PATH, type CatalogSuggestionItem, type ChatMessageResult, type ChatProduct, type ChatService, type ChatServiceOptions, 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, type DidYouMeanItem, type ErrorPopupEventInput, type GetMarketConfig, type LocaleParts, type LocationLike, MAX_VISIBLE_SUGGESTIONS, type MarketConfig, type ProductSearchEventInput, type ProductType, RECENT_SEARCHES_DEFAULT_LIMIT, RECENT_SEARCHES_PREFIX, SEARCH_GTM_EVENTS, SEARCH_GTM_SEARCH_TYPES, SEARCH_GTM_USER_STATUS, SEARCH_PLACEHOLDER, STOREFRONT_SHOPPING_CONTEXT, type SearchChromeCopy, type SearchGtmContext, type SearchGtmSearchType, type SearchGtmUserStatus, type SearchHistoryEntry, type SearchHistoryState, type SearchHistoryEntry as SearchHistoryStoreEntry, type SearchService, type SearchServiceOptions, type SearchTypeEventInput, type SuggestionOption, type SuggestionsResponse, TRIGGER_PLACEHOLDER, type UseSearchHistoryResult, type UseSearchSuggestionsOptions, type UseSearchSuggestionsResult, type UseVoiceSearchOptions, type UseVoiceSearchResult, createChatService, createSearchService, getImageFromProperty, getIngredientTerms, getLocaleFromUrl, getSeeMoreResultsPayload, getVisibleSuggestions, isCartActionDisabled, isSellableProductStatus, isSuggestionDisabled, mapCatalogProductToSuggestion, normalizeDidYouMean, pushGtmEvent, resolveApiGatewayPath, resolveSuggestionImageSrc, trackChatEntryPoint, trackErrorPopup, trackProductSearch, trackSearchType, useSearchHistory, useSearchHistoryStore, useSearchSuggestions, useVoiceSearch };
652
+ export { CATALOG_SUGGESTIONS_PATH, type CatalogSuggestionItem, DEBOUNCE_MS, DEFAULT_SEARCH_PATH, DEFAULT_SUGGESTION_PAGE, DEFAULT_SUGGESTION_SIZE, type DecideSearchRouteInput, type DidYouMeanItem, type ErrorPopupEventInput, type GetMarketConfig, type IsNuiSearchEnabledInput, type LocaleParts, type LocationLike, MAX_VISIBLE_SUGGESTIONS, type MarketConfig, NUI_SEARCH_FLAG, type NuiAgentConfig, type NuiSearchFlagEnv, ProductSearch, type ProductSearchEventInput, type ProductSearchProps, type ProductType, RECENT_SEARCHES_DEFAULT_LIMIT, RECENT_SEARCHES_PREFIX, type RoutingSnapshot, SEARCH_GTM_EVENTS, SEARCH_GTM_SEARCH_TYPES, SEARCH_GTM_USER_STATUS, SEARCH_PLACEHOLDER, STOREFRONT_SHOPPING_CONTEXT, SUBMIT_WAIT_TIMEOUT_MS, type SearchChromeCopy, type SearchGtmContext, type SearchGtmSearchType, type SearchGtmUserStatus, type SearchRoute, type SearchService, type SearchServiceOptions, type SearchTypeEventInput, type SuggestionOption, type SuggestionsResponse, TRIGGER_PLACEHOLDER, type UseSearchSuggestionsOptions, type UseSearchSuggestionsResult, createSearchService, decideSearchRoute, getImageFromProperty, getIngredientTerms, getLocaleFromUrl, getSeeMoreResultsPayload, getVisibleSuggestions, isCartActionDisabled, isNuiSearchEnabled, isSellableProductStatus, isSuggestionDisabled, mapCatalogProductToSuggestion, normalizeDidYouMean, pushGtmEvent, resolveApiGatewayPath, resolveNuiSearchFlag, resolveSuggestionImageSrc, trackChatEntryPoint, trackErrorPopup, trackProductSearch, trackSearchType, useSearchSuggestions };