@commerce-ai-tool/core 2.4.0

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +11 -0
  4. package/dist/ai/factory.d.ts +5 -0
  5. package/dist/ai/factory.d.ts.map +1 -0
  6. package/dist/ai/index.d.ts +3 -0
  7. package/dist/ai/index.d.ts.map +1 -0
  8. package/dist/ai/json-schema-errors.d.ts +4 -0
  9. package/dist/ai/json-schema-errors.d.ts.map +1 -0
  10. package/dist/ai/providers/bedrock.d.ts +19 -0
  11. package/dist/ai/providers/bedrock.d.ts.map +1 -0
  12. package/dist/ai/providers/openrouter.d.ts +27 -0
  13. package/dist/ai/providers/openrouter.d.ts.map +1 -0
  14. package/dist/ai/types.d.ts +23 -0
  15. package/dist/ai/types.d.ts.map +1 -0
  16. package/dist/cache/search-cache.d.ts +19 -0
  17. package/dist/cache/search-cache.d.ts.map +1 -0
  18. package/dist/client.cjs +423 -0
  19. package/dist/client.cjs.map +1 -0
  20. package/dist/client.d.cts +12 -0
  21. package/dist/client.d.ts +12 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +388 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/commercetools/build-search-keywords.d.ts +37 -0
  26. package/dist/commercetools/build-search-keywords.d.ts.map +1 -0
  27. package/dist/commercetools/cart.d.ts +70 -0
  28. package/dist/commercetools/cart.d.ts.map +1 -0
  29. package/dist/commercetools/checkout.d.ts +33 -0
  30. package/dist/commercetools/checkout.d.ts.map +1 -0
  31. package/dist/commercetools/client.d.ts +43 -0
  32. package/dist/commercetools/client.d.ts.map +1 -0
  33. package/dist/commercetools/facet-color.d.ts +12 -0
  34. package/dist/commercetools/facet-color.d.ts.map +1 -0
  35. package/dist/commercetools/facets.d.ts +31 -0
  36. package/dist/commercetools/facets.d.ts.map +1 -0
  37. package/dist/commercetools/graphql-product-cards.d.ts +94 -0
  38. package/dist/commercetools/graphql-product-cards.d.ts.map +1 -0
  39. package/dist/commercetools/order.d.ts +27 -0
  40. package/dist/commercetools/order.d.ts.map +1 -0
  41. package/dist/commercetools/payment-map.d.ts +18 -0
  42. package/dist/commercetools/payment-map.d.ts.map +1 -0
  43. package/dist/commercetools/payment.d.ts +28 -0
  44. package/dist/commercetools/payment.d.ts.map +1 -0
  45. package/dist/commercetools/product-card-hydrate.d.ts +25 -0
  46. package/dist/commercetools/product-card-hydrate.d.ts.map +1 -0
  47. package/dist/commercetools/product-types.d.ts +34 -0
  48. package/dist/commercetools/product-types.d.ts.map +1 -0
  49. package/dist/commercetools/query-builder.d.ts +41 -0
  50. package/dist/commercetools/query-builder.d.ts.map +1 -0
  51. package/dist/commercetools/search-helpers.d.ts +16 -0
  52. package/dist/commercetools/search-helpers.d.ts.map +1 -0
  53. package/dist/commercetools/suggestion-quality.d.ts +13 -0
  54. package/dist/commercetools/suggestion-quality.d.ts.map +1 -0
  55. package/dist/commercetools/suggestions.d.ts +3 -0
  56. package/dist/commercetools/suggestions.d.ts.map +1 -0
  57. package/dist/index.cjs +6066 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.cts +42 -0
  60. package/dist/index.d.ts +42 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +5939 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/locale/resolve.d.ts +16 -0
  65. package/dist/locale/resolve.d.ts.map +1 -0
  66. package/dist/messages/index.d.ts +110 -0
  67. package/dist/messages/index.d.ts.map +1 -0
  68. package/dist/observability/enabled.d.ts +15 -0
  69. package/dist/observability/enabled.d.ts.map +1 -0
  70. package/dist/observability/index.d.ts +8 -0
  71. package/dist/observability/index.d.ts.map +1 -0
  72. package/dist/observability/redact.d.ts +9 -0
  73. package/dist/observability/redact.d.ts.map +1 -0
  74. package/dist/observability/spans.d.ts +20 -0
  75. package/dist/observability/spans.d.ts.map +1 -0
  76. package/dist/observability/wrap-ai-provider.d.ts +14 -0
  77. package/dist/observability/wrap-ai-provider.d.ts.map +1 -0
  78. package/dist/payments/types.d.ts +35 -0
  79. package/dist/payments/types.d.ts.map +1 -0
  80. package/dist/prompts/catalog.d.ts +20 -0
  81. package/dist/prompts/catalog.d.ts.map +1 -0
  82. package/dist/prompts/fragments.d.ts +9 -0
  83. package/dist/prompts/fragments.d.ts.map +1 -0
  84. package/dist/prompts/index.d.ts +35 -0
  85. package/dist/prompts/index.d.ts.map +1 -0
  86. package/dist/prompts/resolve.d.ts +35 -0
  87. package/dist/prompts/resolve.d.ts.map +1 -0
  88. package/dist/prompts/schemas.d.ts +206 -0
  89. package/dist/prompts/schemas.d.ts.map +1 -0
  90. package/dist/search/compound-shopping-list.d.ts +6 -0
  91. package/dist/search/compound-shopping-list.d.ts.map +1 -0
  92. package/dist/search/mission.d.ts +14 -0
  93. package/dist/search/mission.d.ts.map +1 -0
  94. package/dist/search/orchestrator.d.ts +29 -0
  95. package/dist/search/orchestrator.d.ts.map +1 -0
  96. package/dist/search/query-passthrough.d.ts +12 -0
  97. package/dist/search/query-passthrough.d.ts.map +1 -0
  98. package/dist/search/relax-search.d.ts +8 -0
  99. package/dist/search/relax-search.d.ts.map +1 -0
  100. package/dist/search/suggestions-input.d.ts +35 -0
  101. package/dist/search/suggestions-input.d.ts.map +1 -0
  102. package/dist/search/voice-tts.d.ts +6 -0
  103. package/dist/search/voice-tts.d.ts.map +1 -0
  104. package/dist/types/index.d.ts +510 -0
  105. package/dist/types/index.d.ts.map +1 -0
  106. package/dist/utils/audio.d.ts +4 -0
  107. package/dist/utils/audio.d.ts.map +1 -0
  108. package/dist/utils/dev-trace.d.ts +2 -0
  109. package/dist/utils/dev-trace.d.ts.map +1 -0
  110. package/dist/utils/hash.d.ts +2 -0
  111. package/dist/utils/hash.d.ts.map +1 -0
  112. package/dist/utils/model-json.d.ts +4 -0
  113. package/dist/utils/model-json.d.ts.map +1 -0
  114. package/dist/utils/search-timer.d.ts +11 -0
  115. package/dist/utils/search-timer.d.ts.map +1 -0
  116. package/dist/utils/with-timeout.d.ts +6 -0
  117. package/dist/utils/with-timeout.d.ts.map +1 -0
  118. package/package.json +86 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/types/index.ts","../src/messages/index.ts","../src/search/compound-shopping-list.ts","../src/commercetools/facet-color.ts","../src/commercetools/facets.ts"],"sourcesContent":["/**\n * Browser-safe core surface for the React and Angular widgets.\n * Do not re-export AI, commercetools, or observability from this entry.\n */\nexport type * from \"./types/index.js\";\nexport { CART_SESSION_HEADER, MAX_LINE_ITEM_QUANTITY } from \"./types/index.js\";\nexport {\n DEFAULT_COMMERCE_AI_SEARCH_MESSAGES,\n resolveCommerceAISearchMessages,\n} from \"./messages/index.js\";\nexport type { CommerceAISearchMessages } from \"./messages/index.js\";\nexport { looksLikeCompoundShoppingList } from \"./search/compound-shopping-list.js\";\nexport {\n isFacetFilterSelected,\n toggleFacetFilter,\n} from \"./commercetools/facets.js\";\nexport {\n hexColorSwatchValue,\n isColorLikeFacetName,\n} from \"./commercetools/facet-color.js\";\n","import type { PaymentProvider } from \"../payments/types.js\";\n\nexport type { PaymentMethodOption } from \"../payments/types.js\";\n\nexport type ThemeMode = \"auto\" | \"light\" | \"dark\";\n\nexport type AIProviderName = \"openrouter\" | \"bedrock\";\n\nexport type VoiceMode = \"openrouter-audio\" | \"elevenlabs-stt\";\n\nexport interface SearchCacheConfig {\n ttlMs?: number;\n maxEntries?: number;\n}\n\nexport interface FacetConfig {\n enabled?: boolean;\n schemaTtlMs?: number;\n include?: string[];\n exclude?: string[];\n maxAttributes?: number;\n}\n\nexport interface SearchTimeoutsConfig {\n aiTextMs?: number;\n aiVoiceAudioMs?: number;\n aiImageMs?: number;\n commercetoolsMs?: number;\n commercetoolsSuggestMs?: number;\n elevenLabsTtsMs?: number;\n}\n\nexport const SUGGESTIONS_MIN_PREFIX_LENGTH = 2;\nexport const SUGGESTIONS_DEFAULT_LIMIT = 8;\nexport const SUGGESTIONS_MAX_LIMIT = 20;\n\nexport interface CommercetoolsConfig {\n projectKey: string;\n clientId: string;\n clientSecret: string;\n region: string;\n scopes?: string[];\n}\n\nexport interface OpenRouterConfig {\n apiKey: string;\n model?: string;\n visionModel?: string;\n /** Audio-capable model for direct voice search (OpenRouter input_audio). */\n voiceModel?: string;\n /**\n * Use OpenRouter `json_schema` (strict) for structured interpretation.\n * Falls back to `json_object` when the model rejects json_schema.\n * Default: true.\n */\n jsonSchema?: boolean;\n}\n\nexport interface BedrockConfig {\n region: string;\n modelId?: string;\n visionModelId?: string;\n}\n\nexport interface AIConfig {\n provider: AIProviderName;\n openrouter?: OpenRouterConfig;\n bedrock?: BedrockConfig;\n}\n\nexport interface ElevenLabsConfig {\n apiKey: string;\n sttModel?: string;\n ttsVoiceId?: string;\n ttsModel?: string;\n}\n\nexport interface CommerceAIDefaults {\n /** Language products are indexed in within commercetools */\n catalogLocale?: string;\n /** @deprecated Use catalogLocale */\n locale?: string;\n storeKey?: string;\n limit?: number;\n currency?: string;\n /** ISO country code for cart and product-card price selection (optional). */\n country?: string;\n}\n\nexport interface PaymentConfig {\n provider?: PaymentProvider;\n /** Block order creation without a successful authorization. Defaults to true when a provider is set. */\n requiredForOrder?: boolean;\n}\n\nexport interface LangfuseConfig {\n /** Derived from LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY when loading from env. */\n enabled?: boolean;\n /**\n * Fetch system prompts from Langfuse at runtime.\n * Applied via `configureLangfusePrompts` when creating the search orchestrator\n * (or set `LANGFUSE_PROMPTS=true` when config omits this field).\n * Local catalog remains the fallback and the source of truth for evals.\n */\n promptsEnabled?: boolean;\n /** Langfuse prompt label (default `production`). From LANGFUSE_PROMPT_LABEL or this field. */\n promptLabel?: string;\n /** Client-side prompt cache TTL in seconds (default 60). From LANGFUSE_PROMPT_CACHE_TTL_SECONDS or this field. */\n promptCacheTtlSeconds?: number;\n}\n\nexport interface MissionsConfig {\n /** Opt-in. When true, every fresh text search also runs mission decomposition. */\n enabled?: boolean;\n /** Maximum product intents kept from a decomposed mission. Default 5. */\n maxIntents?: number;\n /** Product Search limit per intent. Default 4. */\n perIntentLimit?: number;\n /** Minimum AI confidence (0–1) required to keep a mission. Default 0.6. */\n minConfidence?: number;\n}\n\nexport interface CommerceAIConfig {\n commercetools: CommercetoolsConfig;\n ai: AIConfig;\n elevenlabs?: ElevenLabsConfig;\n defaults?: CommerceAIDefaults;\n voiceMode?: VoiceMode;\n cache?: SearchCacheConfig;\n timeouts?: SearchTimeoutsConfig;\n facets?: FacetConfig;\n langfuse?: LangfuseConfig;\n payments?: PaymentConfig;\n missions?: MissionsConfig;\n}\n\nexport interface ProductCard {\n id: string;\n key?: string;\n name: string;\n description?: string;\n imageUrl?: string;\n /** SKU of the master variant — used for add-to-cart. */\n sku?: string;\n /** Master variant id — fallback when `sku` is missing. */\n variantId?: number;\n price?: {\n amount: number;\n currency: string;\n formatted: string;\n };\n slug?: string;\n}\n\nexport interface MoneyAmount {\n amount: number;\n currency: string;\n formatted: string;\n}\n\nexport interface CartLineItemSnapshot {\n id: string;\n name: string;\n sku?: string;\n productId: string;\n quantity: number;\n imageUrl?: string;\n price?: MoneyAmount;\n totalPrice?: MoneyAmount;\n}\n\nexport interface CheckoutAddress {\n firstName: string;\n lastName: string;\n streetName: string;\n additionalStreetInfo?: string;\n postalCode: string;\n city: string;\n region?: string;\n country: string;\n email?: string;\n phone?: string;\n}\n\nexport interface ShippingMethodSnapshot {\n id: string;\n name: string;\n description?: string;\n price?: MoneyAmount;\n}\n\nexport interface PaymentSnapshot {\n id: string;\n key?: string;\n interfaceId?: string;\n paymentInterface: string;\n method: string;\n status: \"authorized\" | \"pending\" | \"failed\";\n amount: MoneyAmount;\n clientData?: Record<string, string>;\n}\n\nexport interface DeliverySnapshot {\n id: string;\n trackingId?: string;\n carrier?: string;\n provider?: string;\n}\n\n/** Client-safe cart shape — never the raw commercetools Cart object. */\nexport interface CartSnapshot {\n id: string;\n version: number;\n anonymousId?: string;\n customerId?: string;\n lineItems: CartLineItemSnapshot[];\n totalPrice: MoneyAmount;\n totalQuantity: number;\n shippingAddress?: CheckoutAddress;\n billingAddress?: CheckoutAddress;\n shippingMethod?: ShippingMethodSnapshot;\n payments?: PaymentSnapshot[];\n}\n\nexport interface OrderSnapshot {\n id: string;\n orderNumber?: string;\n orderState: string;\n createdAt?: string;\n paymentState?: string;\n shipmentState?: string;\n customerEmail?: string;\n totalPrice: MoneyAmount;\n lineItems: CartLineItemSnapshot[];\n shippingAddress?: CheckoutAddress;\n billingAddress?: CheckoutAddress;\n shippingMethod?: ShippingMethodSnapshot;\n payments?: PaymentSnapshot[];\n deliveries?: DeliverySnapshot[];\n}\n\n/** Public customer identity — never includes credentials. */\nexport interface CustomerSnapshot {\n id: string;\n email: string;\n}\n\n/** HMAC cart session header for GET /cart — never send the token in the query string. */\nexport const CART_SESSION_HEADER = \"x-commerce-ai-cart-session\";\n\n/** Upper bound for a single add-to-cart line quantity (mission add-all and batch add). */\nexport const MAX_LINE_ITEM_QUANTITY = 99;\n\nexport interface CartLoginRequest {\n email: string;\n password: string;\n anonymousId?: string;\n cartId?: string;\n catalogLocale?: string;\n}\n\nexport interface CartLoginResult {\n cart: CartSnapshot | null;\n customer: CustomerSnapshot;\n}\n\nexport interface AddToCartLineItem {\n sku?: string;\n productId?: string;\n variantId?: number;\n quantity?: number;\n}\n\nexport interface AddToCartRequest {\n anonymousId?: string;\n customerId?: string;\n sku?: string;\n productId?: string;\n variantId?: number;\n quantity?: number;\n currency?: string;\n country?: string;\n catalogLocale?: string;\n /** When set, add to this cart instead of looking up by anonymousId / customerId. */\n cartId?: string;\n sessionToken?: string;\n}\n\nexport interface AddItemsToCartRequest {\n anonymousId?: string;\n customerId?: string;\n items: AddToCartLineItem[];\n currency?: string;\n country?: string;\n catalogLocale?: string;\n cartId?: string;\n sessionToken?: string;\n}\n\nexport interface CartMutationRequest {\n anonymousId?: string;\n customerId?: string;\n lineItemId: string;\n cartId?: string;\n cartVersion?: number;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface UpdateCartQuantityRequest extends CartMutationRequest {\n quantity: number;\n}\n\nexport interface GetCartRequest {\n anonymousId?: string;\n customerId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface CheckoutRequest {\n anonymousId?: string;\n customerId?: string;\n cartId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface SetCartAddressesRequest extends CheckoutRequest {\n shippingAddress: CheckoutAddress;\n billingAddress?: CheckoutAddress;\n}\n\nexport interface SetShippingMethodRequest extends CheckoutRequest {\n shippingMethodId: string;\n}\n\nexport interface CreateOrderRequest extends CheckoutRequest {\n orderNumber?: string;\n}\n\nexport interface AuthorizePaymentRequest extends CheckoutRequest {\n method: string;\n orderNumber?: string;\n}\n\nexport interface GetOrderRequest {\n orderNumber?: string;\n anonymousId?: string;\n customerId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface ListOrdersRequest {\n anonymousId?: string;\n customerId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n limit?: number;\n}\n\nexport type SearchRelaxationLevel = \"none\" | \"drop_soft_filters\" | \"primary_any_match\";\n\nexport interface SearchMeta {\n total: number;\n limit: number;\n offset: number;\n /** @deprecated Use catalogLocale */\n locale: string;\n catalogLocale: string;\n queryLocale: string;\n queryInterpretation?: string;\n searchTerms?: string[];\n /** Catalog-language phrase closest to the user's intent, when the model supplied one. */\n primaryTerm?: string;\n appliedFilters?: InterpretedSearchFilters;\n /**\n * How far Product Search was relaxed after an empty first result.\n * Omitted when no relaxation ran (`none`).\n */\n relaxation?: SearchRelaxationLevel;\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n schemaEtag?: string;\n /** Per-step durations in milliseconds (dev / CAT_DEBUG only) */\n timings?: Record<string, number>;\n /** Total pipeline duration in milliseconds (dev / CAT_DEBUG only) */\n totalMs?: number;\n /**\n * Active OpenTelemetry / Langfuse trace id when Langfuse is enabled or CAT_DEBUG=true.\n * Non-stable for client contracts — intended for local/dev linking.\n */\n traceId?: string;\n}\n\nexport interface SearchResult {\n products: ProductCard[];\n meta: SearchMeta;\n facets?: SearchFacetGroup[];\n suggestedFacets?: SuggestedFacet[];\n facetSchema?: FacetAttributeDefinition[];\n /** Present when a compound shopping mission was executed. */\n mission?: MissionSearchResult;\n}\n\nexport interface SuggestionsRequest extends SearchLocaleOptions {\n query: string;\n limit?: number;\n}\n\nexport interface SuggestionsResult {\n suggestions: string[];\n /**\n * True when this request invoked AI `suggestSearchTerms` (not a cache hit).\n * Used by the server to flush Langfuse on serverless hosts.\n */\n aiFallbackUsed?: boolean;\n}\n\nexport interface TextSearchRequest {\n query: string;\n /** Language of the user query (AI input) */\n queryLocale?: string;\n /** Language products are indexed in commercetools */\n catalogLocale?: string;\n /** @deprecated Use queryLocale */\n locale?: string;\n limit?: number;\n offset?: number;\n filters?: InterpretedSearchFilters;\n searchTerms?: string[];\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n refineQuery?: string;\n includeFacets?: boolean;\n /** AI-suggested facets from the active search session (chip refine). */\n suggestedFacets?: SuggestedFacet[];\n /** Per-request override of `CommerceAIConfig.missions.enabled`. */\n enableMissions?: boolean;\n}\n\nexport interface SearchLocaleOptions {\n queryLocale?: string;\n catalogLocale?: string;\n /** @deprecated Use queryLocale */\n locale?: string;\n}\n\nexport interface SearchLocaleContext {\n queryLocale: string;\n catalogLocale: string;\n}\n\nexport interface VoiceSearchResult extends SearchResult {\n transcript: string;\n enhancedQuery?: string;\n}\n\nexport interface ImageSearchResult extends SearchResult {\n interpretation: string;\n}\n\n/** Standard filter keys returned by AI interpretation (all optional). */\nexport interface InterpretedSearchFilters {\n color?: string;\n brand?: string;\n category?: string;\n priceMin?: string;\n priceMax?: string;\n [key: string]: string | undefined;\n}\n\nexport interface InterpretedSearchQuery {\n /**\n * Catalog-language phrase closest to the user's intent.\n * Product Search boosts this phrase above the other `searchTerms`.\n * Kept separate from `searchTerms[0]` because same-language query passthrough\n * prepends the typed query without changing intent priority.\n */\n primaryTerm?: string;\n /**\n * Catalog-language search phrases. Product Search matches any phrase (OR).\n * Each element is a complete phrase (e.g. `\"red shoes\"`), not a split word.\n * Broad intents may include 3–5 synonym or hyponym phrases.\n */\n searchTerms: string[];\n filters?: InterpretedSearchFilters;\n suggestedFacets?: SuggestedFacet[];\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n interpretation: string;\n}\n\nexport interface ShoppingIntent {\n /** Stable id for UI keys and selection (`intent-0`, `intent-1`, …). */\n id: string;\n /** Short catalog-language label, e.g. \"tennis racket\". */\n label: string;\n /** Requested quantity, always >= 1. */\n quantity: number;\n /** Catalog-language phrase closest to this intent, boosted above alternates. */\n primaryTerm?: string;\n searchTerms: string[];\n filters?: InterpretedSearchFilters;\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n}\n\nexport interface DecomposedShoppingMission {\n isMission: boolean;\n /** 0–1. Missions below the configured minConfidence fall back to standard search. */\n confidence: number;\n intents: ShoppingIntent[];\n interpretation: string;\n}\n\nexport interface MissionIntentGroup {\n intent: ShoppingIntent;\n products: ProductCard[];\n total: number;\n /** True when this intent's Product Search failed; other groups still render. */\n failed?: boolean;\n}\n\nexport interface MissionSearchResult {\n intents: MissionIntentGroup[];\n interpretation: string;\n}\n\nexport type FacetAttributeType = \"enum\" | \"lenum\" | \"boolean\" | \"text\" | \"number\";\n\nexport interface FacetAttributeDefinition {\n name: string;\n label: string;\n kind: \"distinct\" | \"range\";\n attributeType: FacetAttributeType;\n field: string;\n fieldType?: string;\n}\n\nexport interface ResolvedFacetSchema {\n attributes: FacetAttributeDefinition[];\n systemFacets: Array<\"categories\" | \"price\">;\n etag: string;\n resolvedAt: string;\n}\n\nexport interface SuggestedFacet {\n name: string;\n reason?: string;\n}\n\nexport interface SearchFacetBucket {\n key: string;\n label: string;\n count: number;\n}\n\nexport interface SearchFacetGroup {\n id: string;\n label: string;\n type: \"distinct\" | \"range\";\n buckets: SearchFacetBucket[];\n selectedKey?: string;\n}\n\nexport interface SearchSessionState {\n query: string;\n searchTerms: string[];\n appliedFilters: InterpretedSearchFilters;\n suggestedFacets: SuggestedFacet[];\n facetSchema?: FacetAttributeDefinition[];\n schemaEtag?: string;\n}\n\nexport interface VoiceAudioInterpretation extends InterpretedSearchQuery {\n transcript: string;\n enhancedQuery: string;\n}\n\n/** @deprecated Use ProductSearchRequest from query-builder / platform-sdk */\nexport type { ProductSearchRequest as ProductSearchQueryBody } from \"@commercetools/platform-sdk\";\n","export interface CommerceAISearchMessages {\n placeholder: string;\n searchAriaLabel: string;\n productSearchAriaLabel: string;\n searchResultsAriaLabel: string;\n searching: string;\n noProductsFound: string;\n searchedFor: string;\n dropImageToSearch: string;\n voiceSearch: string;\n stopRecording: string;\n searchByCamera: string;\n searchByImage: string;\n replayVoiceSummary: string;\n listening: string;\n tapMicToStop: string;\n understandingQuery: string;\n preparingAudioSummary: string;\n cameraCapture: string;\n cameraPreview: string;\n capturePhoto: string;\n cancel: string;\n dismiss: string;\n searchFailed: string;\n imageSearchFailed: string;\n couldNotCapturePhoto: string;\n suggestionsAriaLabel: string;\n loadingSuggestions: string;\n noSuggestions: string;\n filtersAriaLabel: string;\n clearFilters: string;\n narrowResults: string;\n newSearch: string;\n addToCart: string;\n cart: string;\n emptyCart: string;\n removeItem: string;\n total: string;\n each: string;\n cartAriaLabel: string;\n itemAdded: string;\n increaseQuantity: string;\n decreaseQuantity: string;\n closeCart: string;\n unableToAddToCart: string;\n missionTitle: string;\n missionQuantity: string;\n /** Adds the first (recommended) product from each filled lane. Use `{count}` for the number of picks. */\n missionAddAll: string;\n missionItemsAdded: string;\n missionSelectProduct: string;\n missionIntentEmpty: string;\n missionIntentFailed: string;\n signIn: string;\n signInToSyncCart: string;\n signOut: string;\n email: string;\n password: string;\n signedInAs: string;\n invalidCredentials: string;\n signInFailed: string;\n checkout: string;\n checkoutTitle: string;\n orderSummary: string;\n shippingAddress: string;\n billingAddress: string;\n billingSameAsShipping: string;\n continueToDelivery: string;\n shippingMethod: string;\n selectShippingMethod: string;\n noShippingMethods: string;\n completeAddressToContinue: string;\n selectDeliveryToContinue: string;\n selectPaymentToContinue: string;\n checkoutStepAddress: string;\n checkoutStepDelivery: string;\n checkoutStepPayment: string;\n paymentMethod: string;\n selectPaymentMethod: string;\n paymentAuthorized: string;\n paymentDeclined: string;\n continueToPayment: string;\n placeOrder: string;\n placingOrder: string;\n orderPlaced: string;\n viewOrderStatus: string;\n continueShopping: string;\n checkoutFailed: string;\n orderStatus: string;\n orderPlacedAt: string;\n paymentStateLabel: string;\n shipmentStateLabel: string;\n trackingNumber: string;\n carrier: string;\n noTrackingYet: string;\n orderNotFound: string;\n noOrdersYet: string;\n yourOrders: string;\n firstName: string;\n lastName: string;\n streetName: string;\n additionalAddress: string;\n postalCode: string;\n city: string;\n region: string;\n country: string;\n}\n\nexport const DEFAULT_COMMERCE_AI_SEARCH_MESSAGES: CommerceAISearchMessages = {\n placeholder: \"What are you looking for?\",\n searchAriaLabel: \"Search query\",\n productSearchAriaLabel: \"Product search\",\n searchResultsAriaLabel: \"Search results\",\n searching: \"Searching...\",\n noProductsFound: \"No products found\",\n searchedFor: \"Searched for:\",\n dropImageToSearch: \"Drop image to search\",\n voiceSearch: \"Voice search\",\n stopRecording: \"Stop recording\",\n searchByCamera: \"Search by camera\",\n searchByImage: \"Search by image\",\n replayVoiceSummary: \"Replay voice result summary\",\n listening: \"Listening…\",\n tapMicToStop: \"Tap mic to stop\",\n understandingQuery: \"Understanding your query…\",\n preparingAudioSummary: \"Preparing audio summary…\",\n cameraCapture: \"Camera capture\",\n cameraPreview: \"Camera preview\",\n capturePhoto: \"Capture\",\n cancel: \"Cancel\",\n dismiss: \"Dismiss\",\n searchFailed: \"Search failed\",\n imageSearchFailed: \"Image search failed\",\n couldNotCapturePhoto: \"Could not capture photo\",\n suggestionsAriaLabel: \"Search suggestions\",\n loadingSuggestions: \"Loading suggestions...\",\n noSuggestions: \"No suggestions\",\n filtersAriaLabel: \"Search filters\",\n clearFilters: \"Clear filters\",\n narrowResults: \"Narrow your results\",\n newSearch: \"New search\",\n addToCart: \"Add to cart\",\n cart: \"Cart\",\n emptyCart: \"Your cart is empty\",\n removeItem: \"Remove item\",\n total: \"Total\",\n each: \"each\",\n cartAriaLabel: \"Shopping cart\",\n itemAdded: \"Added to cart\",\n increaseQuantity: \"Increase quantity\",\n decreaseQuantity: \"Decrease quantity\",\n closeCart: \"Close cart\",\n unableToAddToCart: \"This product cannot be added to the cart\",\n missionTitle: \"Shopping list\",\n missionQuantity: \"Looking for\",\n missionAddAll: \"Add {count} top picks to cart\",\n missionItemsAdded: \"Added to cart\",\n missionSelectProduct: \"Select this product\",\n missionIntentEmpty: \"No matching products\",\n missionIntentFailed: \"Couldn't search this item\",\n signIn: \"Sign in\",\n signInToSyncCart: \"Sign in to sync your cart\",\n signOut: \"Sign out\",\n email: \"Email\",\n password: \"Password\",\n signedInAs: \"Signed in as\",\n invalidCredentials: \"Invalid email or password\",\n signInFailed: \"Sign in failed\",\n checkout: \"Checkout\",\n checkoutTitle: \"Complete your order\",\n orderSummary: \"Order summary\",\n shippingAddress: \"Shipping address\",\n billingAddress: \"Billing address\",\n billingSameAsShipping: \"Billing address is the same as shipping\",\n continueToDelivery: \"Continue to delivery\",\n shippingMethod: \"Delivery method\",\n selectShippingMethod: \"Select a delivery method\",\n noShippingMethods: \"No delivery methods are available for this address\",\n completeAddressToContinue: \"Add a shipping address to continue\",\n selectDeliveryToContinue: \"Select a delivery method to continue\",\n selectPaymentToContinue: \"Select a payment method to continue\",\n checkoutStepAddress: \"Address\",\n checkoutStepDelivery: \"Delivery\",\n checkoutStepPayment: \"Payment\",\n paymentMethod: \"Payment method\",\n selectPaymentMethod: \"Select a payment method\",\n paymentAuthorized: \"Payment authorized\",\n paymentDeclined: \"Payment was declined\",\n continueToPayment: \"Continue to payment\",\n placeOrder: \"Place order\",\n placingOrder: \"Placing order…\",\n orderPlaced: \"Order placed\",\n viewOrderStatus: \"View order status\",\n continueShopping: \"Continue shopping\",\n checkoutFailed: \"Checkout failed\",\n orderStatus: \"Order status\",\n orderPlacedAt: \"Placed\",\n paymentStateLabel: \"Payment\",\n shipmentStateLabel: \"Shipment\",\n trackingNumber: \"Tracking number\",\n carrier: \"Carrier\",\n noTrackingYet: \"Tracking details will appear when the order ships\",\n orderNotFound: \"Order not found\",\n noOrdersYet: \"No orders yet\",\n yourOrders: \"Your orders\",\n firstName: \"First name\",\n lastName: \"Last name\",\n streetName: \"Street address\",\n additionalAddress: \"Apartment, suite, etc. (optional)\",\n postalCode: \"Postal code\",\n city: \"City\",\n region: \"State or region (optional)\",\n country: \"Country\",\n};\n\nexport function resolveCommerceAISearchMessages(\n overrides?: Partial<CommerceAISearchMessages>,\n): CommerceAISearchMessages {\n return {\n ...DEFAULT_COMMERCE_AI_SEARCH_MESSAGES,\n ...overrides,\n };\n}\n","const COMPOUND_SHOPPING_LIST_PATTERN = /\\band\\b|,|\\bplus\\b|\\boraz\\b/i;\n\n/**\n * Heuristic for the search box: a new multi-item query should not be treated as\n * a facet NL refine of the previous session.\n */\nexport function looksLikeCompoundShoppingList(query: string): boolean {\n return COMPOUND_SHOPPING_LIST_PATTERN.test(query.trim());\n}\n","/** CSS Color Module named colors used to label hex facet values. */\nconst CSS_NAMED_COLOR_TABLE = `\nAlice Blue:f0f8ff\nAntique White:faebd7\nAqua:00ffff\nAquamarine:7fffd4\nAzure:f0ffff\nBeige:f5f5dc\nBisque:ffe4c4\nBlack:000000\nBlanched Almond:ffebcd\nBlue:0000ff\nBlue Violet:8a2be2\nBrown:a52a2a\nBurlywood:deb887\nCadet Blue:5f9ea0\nChartreuse:7fff00\nChocolate:d2691e\nCoral:ff7f50\nCornflower Blue:6495ed\nCornsilk:fff8dc\nCrimson:dc143c\nDark Blue:00008b\nDark Cyan:008b8b\nDark Goldenrod:b8860b\nDark Gray:a9a9a9\nDark Green:006400\nDark Khaki:bdb76b\nDark Magenta:8b008b\nDark Olive Green:556b2f\nDark Orange:ff8c00\nDark Orchid:9932cc\nDark Red:8b0000\nDark Salmon:e9967a\nDark Sea Green:8fbc8f\nDark Slate Blue:483d8b\nDark Slate Gray:2f4f4f\nDark Turquoise:00ced1\nDark Violet:9400d3\nDeep Pink:ff1493\nDeep Sky Blue:00bfff\nDim Gray:696969\nDodger Blue:1e90ff\nFirebrick:b22222\nFloral White:fffaf0\nForest Green:228b22\nGainsboro:dcdcdc\nGhost White:f8f8ff\nGold:ffd700\nGoldenrod:daa520\nGray:808080\nGreen:008000\nGreen Yellow:adff2f\nHoneydew:f0fff0\nHot Pink:ff69b4\nIndian Red:cd5c5c\nIndigo:4b0082\nIvory:fffff0\nKhaki:f0e68c\nLavender:e6e6fa\nLavender Blush:fff0f5\nLawn Green:7cfc00\nLemon Chiffon:fffacd\nLight Blue:add8e6\nLight Coral:f08080\nLight Cyan:e0ffff\nLight Goldenrod Yellow:fafad2\nLight Gray:d3d3d3\nLight Green:90ee90\nLight Pink:ffb6c1\nLight Salmon:ffa07a\nLight Sea Green:20b2aa\nLight Sky Blue:87cefa\nLight Slate Gray:778899\nLight Steel Blue:b0c4de\nLight Yellow:ffffe0\nLime:00ff00\nLime Green:32cd32\nLinen:faf0e6\nMagenta:ff00ff\nMaroon:800000\nMedium Aquamarine:66cdaa\nMedium Blue:0000cd\nMedium Orchid:ba55d3\nMedium Purple:9370db\nMedium Sea Green:3cb371\nMedium Slate Blue:7b68ee\nMedium Spring Green:00fa9a\nMedium Turquoise:48d1cc\nMedium Violet Red:c71585\nMidnight Blue:191970\nMint Cream:f5fffa\nMisty Rose:ffe4e1\nMoccasin:ffe4b5\nNavajo White:ffdead\nNavy:000080\nOld Lace:fdf5e6\nOlive:808000\nOlive Drab:6b8e23\nOrange:ffa500\nOrange Red:ff4500\nOrchid:da70d6\nPale Goldenrod:eee8aa\nPale Green:98fb98\nPale Turquoise:afeeee\nPale Violet Red:db7093\nPapaya Whip:ffefd5\nPeach Puff:ffdab9\nPeru:cd853f\nPink:ffc0cb\nPlum:dda0dd\nPowder Blue:b0e0e6\nPurple:800080\nRebecca Purple:663399\nRed:ff0000\nRosy Brown:bc8f8f\nRoyal Blue:4169e1\nSaddle Brown:8b4513\nSalmon:fa8072\nSandy Brown:f4a460\nSea Green:2e8b57\nSeashell:fff5ee\nSienna:a0522d\nSilver:c0c0c0\nSky Blue:87ceeb\nSlate Blue:6a5acd\nSlate Gray:708090\nSnow:fffafa\nSpring Green:00ff7f\nSteel Blue:4682b4\nTan:d2b48c\nTeal:008080\nThistle:d8bfd8\nTomato:ff6347\nTurquoise:40e0d0\nViolet:ee82ee\nWheat:f5deb3\nWhite:ffffff\nWhite Smoke:f5f5f5\nYellow:ffff00\nYellow Green:9acd32\n`;\n\nconst HEX_COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\ntype NamedColor = { name: string; r: number; g: number; b: number };\n\nconst NAMED_COLORS: NamedColor[] = CSS_NAMED_COLOR_TABLE.trim()\n .split(\"\\n\")\n .flatMap((line) => {\n const separator = line.indexOf(\":\");\n if (separator <= 0) {\n return [];\n }\n const name = line.slice(0, separator);\n const hex = line.slice(separator + 1);\n const n = Number.parseInt(hex, 16);\n if (!name || !Number.isFinite(n)) {\n return [];\n }\n return [{ name, r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }];\n });\n\nfunction parseHexRgb(value: string): [number, number, number] | null {\n const match = HEX_COLOR_RE.exec(value.trim());\n const captured = match?.[1];\n if (!captured) {\n return null;\n }\n const hex =\n captured.length === 3\n ? `${captured[0]}${captured[0]}${captured[1]}${captured[1]}${captured[2]}${captured[2]}`\n : captured;\n const n = Number.parseInt(hex, 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction nearestCssColor(rgb: [number, number, number]): { name: string; dist: number } | null {\n const first = NAMED_COLORS[0];\n if (!first) {\n return null;\n }\n let bestName = first.name;\n let bestDist = Number.POSITIVE_INFINITY;\n for (const color of NAMED_COLORS) {\n const dist =\n (rgb[0] - color.r) ** 2 + (rgb[1] - color.g) ** 2 + (rgb[2] - color.b) ** 2;\n if (dist < bestDist) {\n bestDist = dist;\n bestName = color.name;\n if (dist === 0) {\n return { name: bestName, dist };\n }\n }\n }\n return { name: bestName, dist: bestDist };\n}\n\n/** Max squared RGB distance to accept a CSS name instead of the raw hex. */\nconst MAX_NAMED_COLOR_DISTANCE_SQ = 40 * 40;\n\nconst COLOR_LIKE_FACET_NAME = /(?:^|[-_])colou?r(?:[-_]|$)|(?:^|[-_])finish(?:[-_]|$)/i;\n\n/** True when a facet id looks like a color/finish attribute (`color-code`, `finish-code`). */\nexport function isColorLikeFacetName(name: string): boolean {\n return COLOR_LIKE_FACET_NAME.test(name.trim());\n}\n\n/** True when a facet bucket key is a CSS hex color (`#RGB` or `#RRGGBB`). */\nexport function isHexColorFacetKey(value: string): boolean {\n return HEX_COLOR_RE.test(value.trim());\n}\n\n/** Normalized `#rrggbb` for a hex facet key, or `undefined` when not a color. */\nexport function hexColorSwatchValue(value: string): string | undefined {\n const rgb = parseHexRgb(value);\n if (!rgb) {\n return undefined;\n }\n return `#${rgb.map((channel) => channel.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/**\n * Human-readable facet chip label. Hex keys on color/finish facets become the\n * nearest CSS color name when close enough; other keys are unchanged.\n */\nexport function formatFacetBucketLabel(key: string, facetName?: string): string {\n if (facetName !== undefined && !isColorLikeFacetName(facetName)) {\n return key;\n }\n\n const rgb = parseHexRgb(key);\n if (!rgb) {\n return key;\n }\n\n const nearest = nearestCssColor(rgb);\n if (!nearest || nearest.dist > MAX_NAMED_COLOR_DISTANCE_SQ) {\n return key;\n }\n\n return nearest.name;\n}\n","import type {\n FacetAttributeDefinition,\n InterpretedSearchFilters,\n ResolvedFacetSchema,\n SearchFacetGroup,\n SuggestedFacet,\n} from \"../types/index.js\";\nimport { formatFacetBucketLabel } from \"./facet-color.js\";\n\nexport const PRICE_FACET_ID = \"price\";\nexport const CATEGORIES_FACET_ID = \"categories\";\n\nexport const DEFAULT_PRICE_RANGES = [\n { key: \"under-50\", to: 5_000 },\n { key: \"50-to-100\", from: 5_000, to: 10_000 },\n { key: \"100-plus\", from: 10_000 },\n] as const;\n\nexport type PriceRangeDefinition = (typeof DEFAULT_PRICE_RANGES)[number];\n\n/** Major currency units (e.g. EUR) corresponding to a price range bucket. */\nexport function priceRangeToFilterValues(\n range: PriceRangeDefinition,\n): Pick<InterpretedSearchFilters, \"priceMin\" | \"priceMax\"> {\n const filters: Pick<InterpretedSearchFilters, \"priceMin\" | \"priceMax\"> = {};\n if (\"from\" in range && range.from !== undefined) {\n filters.priceMin = String(range.from / 100);\n }\n if (\"to\" in range && range.to !== undefined) {\n filters.priceMax = String(range.to / 100);\n }\n return filters;\n}\n\nexport function priceBucketKeyFromFilters(\n filters: InterpretedSearchFilters,\n): string | undefined {\n const minCents = parseMajorToCents(filters.priceMin);\n const maxCents = parseMajorToCents(filters.priceMax);\n\n for (const range of DEFAULT_PRICE_RANGES) {\n const from = \"from\" in range ? range.from : undefined;\n const to = \"to\" in range ? range.to : undefined;\n if (minCents === from && maxCents === to) {\n return range.key;\n }\n }\n return undefined;\n}\n\nexport function isFacetFilterSelected(\n filters: InterpretedSearchFilters,\n facetId: string,\n bucketKey: string,\n): boolean {\n if (facetId === CATEGORIES_FACET_ID) {\n return filters.category === bucketKey;\n }\n if (facetId === PRICE_FACET_ID) {\n return priceBucketKeyFromFilters(filters) === bucketKey;\n }\n return filters[facetId] === bucketKey;\n}\n\n/**\n * Toggle a facet chip into filter state. Price buckets map to priceMin/priceMax;\n * categories map to `category`.\n */\nexport function toggleFacetFilter(\n filters: InterpretedSearchFilters,\n facetId: string,\n bucketKey: string,\n): InterpretedSearchFilters {\n const next = { ...filters };\n\n if (facetId === PRICE_FACET_ID) {\n if (priceBucketKeyFromFilters(next) === bucketKey) {\n delete next.priceMin;\n delete next.priceMax;\n return next;\n }\n delete next.priceMin;\n delete next.priceMax;\n const range = DEFAULT_PRICE_RANGES.find((candidate) => candidate.key === bucketKey);\n if (!range) {\n return next;\n }\n return { ...next, ...priceRangeToFilterValues(range) };\n }\n\n const filterKey = facetId === CATEGORIES_FACET_ID ? \"category\" : facetId;\n if (next[filterKey] === bucketKey) {\n delete next[filterKey];\n } else {\n next[filterKey] = bucketKey;\n }\n return next;\n}\n\nexport function buildProductSearchFacets(\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[] = [],\n): unknown[] {\n const names = requestedFacetNames(schema, suggestedFacets);\n const facets: unknown[] = [];\n for (const name of names) {\n if (name === CATEGORIES_FACET_ID) {\n facets.push({ distinct: { name, field: \"categories\", limit: 12, level: \"products\" } });\n continue;\n }\n if (name === PRICE_FACET_ID) {\n facets.push({\n ranges: {\n name,\n field: \"variants.prices.centAmount\",\n ranges: DEFAULT_PRICE_RANGES.map((range) => ({ ...range })),\n level: \"products\",\n },\n });\n continue;\n }\n\n const attribute = schema.attributes.find((candidate) => candidate.name === name);\n if (!attribute || attribute.kind !== \"distinct\") {\n continue;\n }\n facets.push({\n distinct: {\n name: attribute.name,\n field: attribute.field,\n fieldType: attribute.fieldType,\n limit: 12,\n level: \"products\",\n },\n });\n }\n return facets;\n}\n\n/** Product Projection Search `facet` query parameter expressions. */\nexport function buildProjectionFacetParams(\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[] = [],\n): string[] {\n const names = requestedFacetNames(schema, suggestedFacets);\n const facets: string[] = [];\n\n for (const name of names) {\n if (name === CATEGORIES_FACET_ID) {\n facets.push(`categories.id as ${CATEGORIES_FACET_ID}`);\n continue;\n }\n if (name === PRICE_FACET_ID) {\n const ranges = DEFAULT_PRICE_RANGES.map((range) => {\n const from = \"from\" in range && range.from !== undefined ? String(range.from) : \"*\";\n const to = \"to\" in range && range.to !== undefined ? String(range.to) : \"*\";\n return `(${from} to ${to})`;\n }).join(\", \");\n facets.push(`variants.prices.centAmount:range ${ranges} as ${PRICE_FACET_ID}`);\n continue;\n }\n\n const attribute = schema.attributes.find((candidate) => candidate.name === name);\n if (!attribute || attribute.kind !== \"distinct\") {\n continue;\n }\n facets.push(`${attribute.field} as ${attribute.name}`);\n }\n\n return facets;\n}\n\nexport function normalizeProductSearchFacets(\n rawFacets: unknown,\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[] = [],\n filters: InterpretedSearchFilters = {},\n): SearchFacetGroup[] {\n const raw = Array.isArray(rawFacets)\n ? rawFacets\n : rawFacets && typeof rawFacets === \"object\"\n ? Object.entries(rawFacets as Record<string, unknown>).map(([name, facet]) => ({\n name,\n ...(facet && typeof facet === \"object\" ? facet : {}),\n }))\n : [];\n const labels = new Map(schema.attributes.map((attribute) => [attribute.name, attribute.label]));\n\n const facets: SearchFacetGroup[] = [];\n for (const facet of raw) {\n if (!facet || typeof facet !== \"object\") {\n continue;\n }\n const record = facet as {\n name?: unknown;\n buckets?: unknown;\n terms?: unknown;\n ranges?: unknown;\n };\n if (typeof record.name !== \"string\") {\n continue;\n }\n\n const buckets = normalizeFacetBuckets({\n name: record.name,\n buckets: record.buckets,\n terms: record.terms,\n ranges: record.ranges,\n });\n if (!buckets.length) {\n continue;\n }\n\n facets.push({\n id: record.name,\n label:\n labels.get(record.name) ??\n (record.name === CATEGORIES_FACET_ID\n ? \"Categories\"\n : record.name === PRICE_FACET_ID\n ? \"Price\"\n : record.name),\n type: record.name === PRICE_FACET_ID ? \"range\" : \"distinct\",\n buckets,\n ...(selectedFacetValue(record.name, filters)\n ? { selectedKey: selectedFacetValue(record.name, filters) }\n : {}),\n });\n }\n const requested = requestedFacetNames(schema, suggestedFacets);\n return facets.sort((left, right) => requested.indexOf(left.id) - requested.indexOf(right.id));\n}\n\nexport function filterFacetSuggestions(\n suggestedFacets: SuggestedFacet[] | undefined,\n schema: ResolvedFacetSchema,\n): SuggestedFacet[] {\n const known = new Set([...schema.attributes.map((attribute) => attribute.name), ...schema.systemFacets]);\n return (suggestedFacets ?? []).filter((facet) => known.has(facet.name)).slice(0, 5);\n}\n\nexport function getFacetAttribute(\n schema: ResolvedFacetSchema,\n name: string,\n): FacetAttributeDefinition | undefined {\n return schema.attributes.find((attribute) => attribute.name === name);\n}\n\nfunction requestedFacetNames(\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[],\n): string[] {\n const suggested = filterFacetSuggestions(suggestedFacets, schema).map((facet) => facet.name);\n return suggested.length ? suggested : schema.attributes.slice(0, 3).map((attribute) => attribute.name);\n}\n\nfunction normalizeFacetBuckets(record: {\n buckets?: unknown;\n terms?: unknown;\n ranges?: unknown;\n name?: string;\n}): Array<{ key: string; label: string; count: number }> {\n if (Array.isArray(record.buckets)) {\n return record.buckets.flatMap((bucket) => normalizeBucket(bucket, record.name));\n }\n\n if (Array.isArray(record.terms)) {\n return record.terms.flatMap((term) => normalizeTermBucket(term, record.name));\n }\n\n if (Array.isArray(record.ranges)) {\n return record.ranges.flatMap((range, index) => normalizeRangeBucket(range, index));\n }\n\n return [];\n}\n\nfunction normalizeBucket(bucket: unknown, facetName?: string) {\n if (!bucket || typeof bucket !== \"object\") {\n return [];\n }\n const record = bucket as { key?: unknown; count?: unknown };\n if (typeof record.key !== \"string\" || typeof record.count !== \"number\") {\n return [];\n }\n return [{ key: record.key, label: formatFacetBucketLabel(record.key, facetName), count: record.count }];\n}\n\nfunction normalizeTermBucket(bucket: unknown, facetName?: string) {\n if (!bucket || typeof bucket !== \"object\") {\n return [];\n }\n const record = bucket as { term?: unknown; count?: unknown };\n if (typeof record.term !== \"string\" || typeof record.count !== \"number\") {\n return [];\n }\n return [{ key: record.term, label: formatFacetBucketLabel(record.term, facetName), count: record.count }];\n}\n\nfunction normalizeRangeBucket(bucket: unknown, index: number) {\n if (!bucket || typeof bucket !== \"object\") {\n return [];\n }\n const record = bucket as { from?: unknown; to?: unknown; count?: unknown; key?: unknown };\n if (typeof record.count !== \"number\") {\n return [];\n }\n\n if (typeof record.key === \"string\") {\n return [{ key: record.key, label: record.key, count: record.count }];\n }\n\n const from = typeof record.from === \"number\" ? record.from : undefined;\n const to = typeof record.to === \"number\" ? record.to : undefined;\n const matched = DEFAULT_PRICE_RANGES.find((range) => {\n const rangeFrom = \"from\" in range ? range.from : undefined;\n const rangeTo = \"to\" in range ? range.to : undefined;\n return rangeFrom === from && rangeTo === to;\n });\n const key = matched?.key ?? `${from ?? \"*\"}-${to ?? \"*\"}-${index}`;\n return [{ key, label: key, count: record.count }];\n}\n\nfunction selectedFacetValue(name: string, filters: InterpretedSearchFilters): string | undefined {\n if (name === CATEGORIES_FACET_ID) {\n return filters.category;\n }\n if (name === PRICE_FACET_ID) {\n return priceBucketKeyFromFilters(filters);\n }\n return filters[name];\n}\n\nfunction parseMajorToCents(value?: string): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return undefined;\n }\n return Math.round(parsed * 100);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Browser-safe core surface for the React and Angular widgets.
3
+ * Do not re-export AI, commercetools, or observability from this entry.
4
+ */
5
+ export type * from "./types/index.js";
6
+ export { CART_SESSION_HEADER, MAX_LINE_ITEM_QUANTITY } from "./types/index.js";
7
+ export { DEFAULT_COMMERCE_AI_SEARCH_MESSAGES, resolveCommerceAISearchMessages, } from "./messages/index.js";
8
+ export type { CommerceAISearchMessages } from "./messages/index.js";
9
+ export { looksLikeCompoundShoppingList } from "./search/compound-shopping-list.js";
10
+ export { isFacetFilterSelected, toggleFacetFilter, } from "./commercetools/facets.js";
11
+ export { hexColorSwatchValue, isColorLikeFacetName, } from "./commercetools/facet-color.js";
12
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Browser-safe core surface for the React and Angular widgets.
3
+ * Do not re-export AI, commercetools, or observability from this entry.
4
+ */
5
+ export type * from "./types/index.js";
6
+ export { CART_SESSION_HEADER, MAX_LINE_ITEM_QUANTITY } from "./types/index.js";
7
+ export { DEFAULT_COMMERCE_AI_SEARCH_MESSAGES, resolveCommerceAISearchMessages, } from "./messages/index.js";
8
+ export type { CommerceAISearchMessages } from "./messages/index.js";
9
+ export { looksLikeCompoundShoppingList } from "./search/compound-shopping-list.js";
10
+ export { isFacetFilterSelected, toggleFacetFilter, } from "./commercetools/facets.js";
11
+ export { hexColorSwatchValue, isColorLikeFacetName, } from "./commercetools/facet-color.js";
12
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,mBAAmB,kBAAkB,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EACL,mCAAmC,EACnC,+BAA+B,GAChC,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AACnF,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,388 @@
1
+ // src/types/index.ts
2
+ var CART_SESSION_HEADER = "x-commerce-ai-cart-session";
3
+ var MAX_LINE_ITEM_QUANTITY = 99;
4
+
5
+ // src/messages/index.ts
6
+ var DEFAULT_COMMERCE_AI_SEARCH_MESSAGES = {
7
+ placeholder: "What are you looking for?",
8
+ searchAriaLabel: "Search query",
9
+ productSearchAriaLabel: "Product search",
10
+ searchResultsAriaLabel: "Search results",
11
+ searching: "Searching...",
12
+ noProductsFound: "No products found",
13
+ searchedFor: "Searched for:",
14
+ dropImageToSearch: "Drop image to search",
15
+ voiceSearch: "Voice search",
16
+ stopRecording: "Stop recording",
17
+ searchByCamera: "Search by camera",
18
+ searchByImage: "Search by image",
19
+ replayVoiceSummary: "Replay voice result summary",
20
+ listening: "Listening\u2026",
21
+ tapMicToStop: "Tap mic to stop",
22
+ understandingQuery: "Understanding your query\u2026",
23
+ preparingAudioSummary: "Preparing audio summary\u2026",
24
+ cameraCapture: "Camera capture",
25
+ cameraPreview: "Camera preview",
26
+ capturePhoto: "Capture",
27
+ cancel: "Cancel",
28
+ dismiss: "Dismiss",
29
+ searchFailed: "Search failed",
30
+ imageSearchFailed: "Image search failed",
31
+ couldNotCapturePhoto: "Could not capture photo",
32
+ suggestionsAriaLabel: "Search suggestions",
33
+ loadingSuggestions: "Loading suggestions...",
34
+ noSuggestions: "No suggestions",
35
+ filtersAriaLabel: "Search filters",
36
+ clearFilters: "Clear filters",
37
+ narrowResults: "Narrow your results",
38
+ newSearch: "New search",
39
+ addToCart: "Add to cart",
40
+ cart: "Cart",
41
+ emptyCart: "Your cart is empty",
42
+ removeItem: "Remove item",
43
+ total: "Total",
44
+ each: "each",
45
+ cartAriaLabel: "Shopping cart",
46
+ itemAdded: "Added to cart",
47
+ increaseQuantity: "Increase quantity",
48
+ decreaseQuantity: "Decrease quantity",
49
+ closeCart: "Close cart",
50
+ unableToAddToCart: "This product cannot be added to the cart",
51
+ missionTitle: "Shopping list",
52
+ missionQuantity: "Looking for",
53
+ missionAddAll: "Add {count} top picks to cart",
54
+ missionItemsAdded: "Added to cart",
55
+ missionSelectProduct: "Select this product",
56
+ missionIntentEmpty: "No matching products",
57
+ missionIntentFailed: "Couldn't search this item",
58
+ signIn: "Sign in",
59
+ signInToSyncCart: "Sign in to sync your cart",
60
+ signOut: "Sign out",
61
+ email: "Email",
62
+ password: "Password",
63
+ signedInAs: "Signed in as",
64
+ invalidCredentials: "Invalid email or password",
65
+ signInFailed: "Sign in failed",
66
+ checkout: "Checkout",
67
+ checkoutTitle: "Complete your order",
68
+ orderSummary: "Order summary",
69
+ shippingAddress: "Shipping address",
70
+ billingAddress: "Billing address",
71
+ billingSameAsShipping: "Billing address is the same as shipping",
72
+ continueToDelivery: "Continue to delivery",
73
+ shippingMethod: "Delivery method",
74
+ selectShippingMethod: "Select a delivery method",
75
+ noShippingMethods: "No delivery methods are available for this address",
76
+ completeAddressToContinue: "Add a shipping address to continue",
77
+ selectDeliveryToContinue: "Select a delivery method to continue",
78
+ selectPaymentToContinue: "Select a payment method to continue",
79
+ checkoutStepAddress: "Address",
80
+ checkoutStepDelivery: "Delivery",
81
+ checkoutStepPayment: "Payment",
82
+ paymentMethod: "Payment method",
83
+ selectPaymentMethod: "Select a payment method",
84
+ paymentAuthorized: "Payment authorized",
85
+ paymentDeclined: "Payment was declined",
86
+ continueToPayment: "Continue to payment",
87
+ placeOrder: "Place order",
88
+ placingOrder: "Placing order\u2026",
89
+ orderPlaced: "Order placed",
90
+ viewOrderStatus: "View order status",
91
+ continueShopping: "Continue shopping",
92
+ checkoutFailed: "Checkout failed",
93
+ orderStatus: "Order status",
94
+ orderPlacedAt: "Placed",
95
+ paymentStateLabel: "Payment",
96
+ shipmentStateLabel: "Shipment",
97
+ trackingNumber: "Tracking number",
98
+ carrier: "Carrier",
99
+ noTrackingYet: "Tracking details will appear when the order ships",
100
+ orderNotFound: "Order not found",
101
+ noOrdersYet: "No orders yet",
102
+ yourOrders: "Your orders",
103
+ firstName: "First name",
104
+ lastName: "Last name",
105
+ streetName: "Street address",
106
+ additionalAddress: "Apartment, suite, etc. (optional)",
107
+ postalCode: "Postal code",
108
+ city: "City",
109
+ region: "State or region (optional)",
110
+ country: "Country"
111
+ };
112
+ function resolveCommerceAISearchMessages(overrides) {
113
+ return {
114
+ ...DEFAULT_COMMERCE_AI_SEARCH_MESSAGES,
115
+ ...overrides
116
+ };
117
+ }
118
+
119
+ // src/search/compound-shopping-list.ts
120
+ var COMPOUND_SHOPPING_LIST_PATTERN = /\band\b|,|\bplus\b|\boraz\b/i;
121
+ function looksLikeCompoundShoppingList(query) {
122
+ return COMPOUND_SHOPPING_LIST_PATTERN.test(query.trim());
123
+ }
124
+
125
+ // src/commercetools/facet-color.ts
126
+ var CSS_NAMED_COLOR_TABLE = `
127
+ Alice Blue:f0f8ff
128
+ Antique White:faebd7
129
+ Aqua:00ffff
130
+ Aquamarine:7fffd4
131
+ Azure:f0ffff
132
+ Beige:f5f5dc
133
+ Bisque:ffe4c4
134
+ Black:000000
135
+ Blanched Almond:ffebcd
136
+ Blue:0000ff
137
+ Blue Violet:8a2be2
138
+ Brown:a52a2a
139
+ Burlywood:deb887
140
+ Cadet Blue:5f9ea0
141
+ Chartreuse:7fff00
142
+ Chocolate:d2691e
143
+ Coral:ff7f50
144
+ Cornflower Blue:6495ed
145
+ Cornsilk:fff8dc
146
+ Crimson:dc143c
147
+ Dark Blue:00008b
148
+ Dark Cyan:008b8b
149
+ Dark Goldenrod:b8860b
150
+ Dark Gray:a9a9a9
151
+ Dark Green:006400
152
+ Dark Khaki:bdb76b
153
+ Dark Magenta:8b008b
154
+ Dark Olive Green:556b2f
155
+ Dark Orange:ff8c00
156
+ Dark Orchid:9932cc
157
+ Dark Red:8b0000
158
+ Dark Salmon:e9967a
159
+ Dark Sea Green:8fbc8f
160
+ Dark Slate Blue:483d8b
161
+ Dark Slate Gray:2f4f4f
162
+ Dark Turquoise:00ced1
163
+ Dark Violet:9400d3
164
+ Deep Pink:ff1493
165
+ Deep Sky Blue:00bfff
166
+ Dim Gray:696969
167
+ Dodger Blue:1e90ff
168
+ Firebrick:b22222
169
+ Floral White:fffaf0
170
+ Forest Green:228b22
171
+ Gainsboro:dcdcdc
172
+ Ghost White:f8f8ff
173
+ Gold:ffd700
174
+ Goldenrod:daa520
175
+ Gray:808080
176
+ Green:008000
177
+ Green Yellow:adff2f
178
+ Honeydew:f0fff0
179
+ Hot Pink:ff69b4
180
+ Indian Red:cd5c5c
181
+ Indigo:4b0082
182
+ Ivory:fffff0
183
+ Khaki:f0e68c
184
+ Lavender:e6e6fa
185
+ Lavender Blush:fff0f5
186
+ Lawn Green:7cfc00
187
+ Lemon Chiffon:fffacd
188
+ Light Blue:add8e6
189
+ Light Coral:f08080
190
+ Light Cyan:e0ffff
191
+ Light Goldenrod Yellow:fafad2
192
+ Light Gray:d3d3d3
193
+ Light Green:90ee90
194
+ Light Pink:ffb6c1
195
+ Light Salmon:ffa07a
196
+ Light Sea Green:20b2aa
197
+ Light Sky Blue:87cefa
198
+ Light Slate Gray:778899
199
+ Light Steel Blue:b0c4de
200
+ Light Yellow:ffffe0
201
+ Lime:00ff00
202
+ Lime Green:32cd32
203
+ Linen:faf0e6
204
+ Magenta:ff00ff
205
+ Maroon:800000
206
+ Medium Aquamarine:66cdaa
207
+ Medium Blue:0000cd
208
+ Medium Orchid:ba55d3
209
+ Medium Purple:9370db
210
+ Medium Sea Green:3cb371
211
+ Medium Slate Blue:7b68ee
212
+ Medium Spring Green:00fa9a
213
+ Medium Turquoise:48d1cc
214
+ Medium Violet Red:c71585
215
+ Midnight Blue:191970
216
+ Mint Cream:f5fffa
217
+ Misty Rose:ffe4e1
218
+ Moccasin:ffe4b5
219
+ Navajo White:ffdead
220
+ Navy:000080
221
+ Old Lace:fdf5e6
222
+ Olive:808000
223
+ Olive Drab:6b8e23
224
+ Orange:ffa500
225
+ Orange Red:ff4500
226
+ Orchid:da70d6
227
+ Pale Goldenrod:eee8aa
228
+ Pale Green:98fb98
229
+ Pale Turquoise:afeeee
230
+ Pale Violet Red:db7093
231
+ Papaya Whip:ffefd5
232
+ Peach Puff:ffdab9
233
+ Peru:cd853f
234
+ Pink:ffc0cb
235
+ Plum:dda0dd
236
+ Powder Blue:b0e0e6
237
+ Purple:800080
238
+ Rebecca Purple:663399
239
+ Red:ff0000
240
+ Rosy Brown:bc8f8f
241
+ Royal Blue:4169e1
242
+ Saddle Brown:8b4513
243
+ Salmon:fa8072
244
+ Sandy Brown:f4a460
245
+ Sea Green:2e8b57
246
+ Seashell:fff5ee
247
+ Sienna:a0522d
248
+ Silver:c0c0c0
249
+ Sky Blue:87ceeb
250
+ Slate Blue:6a5acd
251
+ Slate Gray:708090
252
+ Snow:fffafa
253
+ Spring Green:00ff7f
254
+ Steel Blue:4682b4
255
+ Tan:d2b48c
256
+ Teal:008080
257
+ Thistle:d8bfd8
258
+ Tomato:ff6347
259
+ Turquoise:40e0d0
260
+ Violet:ee82ee
261
+ Wheat:f5deb3
262
+ White:ffffff
263
+ White Smoke:f5f5f5
264
+ Yellow:ffff00
265
+ Yellow Green:9acd32
266
+ `;
267
+ var HEX_COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
268
+ var NAMED_COLORS = CSS_NAMED_COLOR_TABLE.trim().split("\n").flatMap((line) => {
269
+ const separator = line.indexOf(":");
270
+ if (separator <= 0) {
271
+ return [];
272
+ }
273
+ const name = line.slice(0, separator);
274
+ const hex = line.slice(separator + 1);
275
+ const n = Number.parseInt(hex, 16);
276
+ if (!name || !Number.isFinite(n)) {
277
+ return [];
278
+ }
279
+ return [{ name, r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 }];
280
+ });
281
+ function parseHexRgb(value) {
282
+ const match = HEX_COLOR_RE.exec(value.trim());
283
+ const captured = match?.[1];
284
+ if (!captured) {
285
+ return null;
286
+ }
287
+ const hex = captured.length === 3 ? `${captured[0]}${captured[0]}${captured[1]}${captured[1]}${captured[2]}${captured[2]}` : captured;
288
+ const n = Number.parseInt(hex, 16);
289
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
290
+ }
291
+ var MAX_NAMED_COLOR_DISTANCE_SQ = 40 * 40;
292
+ var COLOR_LIKE_FACET_NAME = /(?:^|[-_])colou?r(?:[-_]|$)|(?:^|[-_])finish(?:[-_]|$)/i;
293
+ function isColorLikeFacetName(name) {
294
+ return COLOR_LIKE_FACET_NAME.test(name.trim());
295
+ }
296
+ function hexColorSwatchValue(value) {
297
+ const rgb = parseHexRgb(value);
298
+ if (!rgb) {
299
+ return void 0;
300
+ }
301
+ return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
302
+ }
303
+
304
+ // src/commercetools/facets.ts
305
+ var PRICE_FACET_ID = "price";
306
+ var CATEGORIES_FACET_ID = "categories";
307
+ var DEFAULT_PRICE_RANGES = [
308
+ { key: "under-50", to: 5e3 },
309
+ { key: "50-to-100", from: 5e3, to: 1e4 },
310
+ { key: "100-plus", from: 1e4 }
311
+ ];
312
+ function priceRangeToFilterValues(range) {
313
+ const filters = {};
314
+ if ("from" in range && range.from !== void 0) {
315
+ filters.priceMin = String(range.from / 100);
316
+ }
317
+ if ("to" in range && range.to !== void 0) {
318
+ filters.priceMax = String(range.to / 100);
319
+ }
320
+ return filters;
321
+ }
322
+ function priceBucketKeyFromFilters(filters) {
323
+ const minCents = parseMajorToCents(filters.priceMin);
324
+ const maxCents = parseMajorToCents(filters.priceMax);
325
+ for (const range of DEFAULT_PRICE_RANGES) {
326
+ const from = "from" in range ? range.from : void 0;
327
+ const to = "to" in range ? range.to : void 0;
328
+ if (minCents === from && maxCents === to) {
329
+ return range.key;
330
+ }
331
+ }
332
+ return void 0;
333
+ }
334
+ function isFacetFilterSelected(filters, facetId, bucketKey) {
335
+ if (facetId === CATEGORIES_FACET_ID) {
336
+ return filters.category === bucketKey;
337
+ }
338
+ if (facetId === PRICE_FACET_ID) {
339
+ return priceBucketKeyFromFilters(filters) === bucketKey;
340
+ }
341
+ return filters[facetId] === bucketKey;
342
+ }
343
+ function toggleFacetFilter(filters, facetId, bucketKey) {
344
+ const next = { ...filters };
345
+ if (facetId === PRICE_FACET_ID) {
346
+ if (priceBucketKeyFromFilters(next) === bucketKey) {
347
+ delete next.priceMin;
348
+ delete next.priceMax;
349
+ return next;
350
+ }
351
+ delete next.priceMin;
352
+ delete next.priceMax;
353
+ const range = DEFAULT_PRICE_RANGES.find((candidate) => candidate.key === bucketKey);
354
+ if (!range) {
355
+ return next;
356
+ }
357
+ return { ...next, ...priceRangeToFilterValues(range) };
358
+ }
359
+ const filterKey = facetId === CATEGORIES_FACET_ID ? "category" : facetId;
360
+ if (next[filterKey] === bucketKey) {
361
+ delete next[filterKey];
362
+ } else {
363
+ next[filterKey] = bucketKey;
364
+ }
365
+ return next;
366
+ }
367
+ function parseMajorToCents(value) {
368
+ if (!value) {
369
+ return void 0;
370
+ }
371
+ const parsed = Number(value);
372
+ if (!Number.isFinite(parsed) || parsed < 0) {
373
+ return void 0;
374
+ }
375
+ return Math.round(parsed * 100);
376
+ }
377
+ export {
378
+ CART_SESSION_HEADER,
379
+ DEFAULT_COMMERCE_AI_SEARCH_MESSAGES,
380
+ MAX_LINE_ITEM_QUANTITY,
381
+ hexColorSwatchValue,
382
+ isColorLikeFacetName,
383
+ isFacetFilterSelected,
384
+ looksLikeCompoundShoppingList,
385
+ resolveCommerceAISearchMessages,
386
+ toggleFacetFilter
387
+ };
388
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/index.ts","../src/messages/index.ts","../src/search/compound-shopping-list.ts","../src/commercetools/facet-color.ts","../src/commercetools/facets.ts"],"sourcesContent":["import type { PaymentProvider } from \"../payments/types.js\";\n\nexport type { PaymentMethodOption } from \"../payments/types.js\";\n\nexport type ThemeMode = \"auto\" | \"light\" | \"dark\";\n\nexport type AIProviderName = \"openrouter\" | \"bedrock\";\n\nexport type VoiceMode = \"openrouter-audio\" | \"elevenlabs-stt\";\n\nexport interface SearchCacheConfig {\n ttlMs?: number;\n maxEntries?: number;\n}\n\nexport interface FacetConfig {\n enabled?: boolean;\n schemaTtlMs?: number;\n include?: string[];\n exclude?: string[];\n maxAttributes?: number;\n}\n\nexport interface SearchTimeoutsConfig {\n aiTextMs?: number;\n aiVoiceAudioMs?: number;\n aiImageMs?: number;\n commercetoolsMs?: number;\n commercetoolsSuggestMs?: number;\n elevenLabsTtsMs?: number;\n}\n\nexport const SUGGESTIONS_MIN_PREFIX_LENGTH = 2;\nexport const SUGGESTIONS_DEFAULT_LIMIT = 8;\nexport const SUGGESTIONS_MAX_LIMIT = 20;\n\nexport interface CommercetoolsConfig {\n projectKey: string;\n clientId: string;\n clientSecret: string;\n region: string;\n scopes?: string[];\n}\n\nexport interface OpenRouterConfig {\n apiKey: string;\n model?: string;\n visionModel?: string;\n /** Audio-capable model for direct voice search (OpenRouter input_audio). */\n voiceModel?: string;\n /**\n * Use OpenRouter `json_schema` (strict) for structured interpretation.\n * Falls back to `json_object` when the model rejects json_schema.\n * Default: true.\n */\n jsonSchema?: boolean;\n}\n\nexport interface BedrockConfig {\n region: string;\n modelId?: string;\n visionModelId?: string;\n}\n\nexport interface AIConfig {\n provider: AIProviderName;\n openrouter?: OpenRouterConfig;\n bedrock?: BedrockConfig;\n}\n\nexport interface ElevenLabsConfig {\n apiKey: string;\n sttModel?: string;\n ttsVoiceId?: string;\n ttsModel?: string;\n}\n\nexport interface CommerceAIDefaults {\n /** Language products are indexed in within commercetools */\n catalogLocale?: string;\n /** @deprecated Use catalogLocale */\n locale?: string;\n storeKey?: string;\n limit?: number;\n currency?: string;\n /** ISO country code for cart and product-card price selection (optional). */\n country?: string;\n}\n\nexport interface PaymentConfig {\n provider?: PaymentProvider;\n /** Block order creation without a successful authorization. Defaults to true when a provider is set. */\n requiredForOrder?: boolean;\n}\n\nexport interface LangfuseConfig {\n /** Derived from LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY when loading from env. */\n enabled?: boolean;\n /**\n * Fetch system prompts from Langfuse at runtime.\n * Applied via `configureLangfusePrompts` when creating the search orchestrator\n * (or set `LANGFUSE_PROMPTS=true` when config omits this field).\n * Local catalog remains the fallback and the source of truth for evals.\n */\n promptsEnabled?: boolean;\n /** Langfuse prompt label (default `production`). From LANGFUSE_PROMPT_LABEL or this field. */\n promptLabel?: string;\n /** Client-side prompt cache TTL in seconds (default 60). From LANGFUSE_PROMPT_CACHE_TTL_SECONDS or this field. */\n promptCacheTtlSeconds?: number;\n}\n\nexport interface MissionsConfig {\n /** Opt-in. When true, every fresh text search also runs mission decomposition. */\n enabled?: boolean;\n /** Maximum product intents kept from a decomposed mission. Default 5. */\n maxIntents?: number;\n /** Product Search limit per intent. Default 4. */\n perIntentLimit?: number;\n /** Minimum AI confidence (0–1) required to keep a mission. Default 0.6. */\n minConfidence?: number;\n}\n\nexport interface CommerceAIConfig {\n commercetools: CommercetoolsConfig;\n ai: AIConfig;\n elevenlabs?: ElevenLabsConfig;\n defaults?: CommerceAIDefaults;\n voiceMode?: VoiceMode;\n cache?: SearchCacheConfig;\n timeouts?: SearchTimeoutsConfig;\n facets?: FacetConfig;\n langfuse?: LangfuseConfig;\n payments?: PaymentConfig;\n missions?: MissionsConfig;\n}\n\nexport interface ProductCard {\n id: string;\n key?: string;\n name: string;\n description?: string;\n imageUrl?: string;\n /** SKU of the master variant — used for add-to-cart. */\n sku?: string;\n /** Master variant id — fallback when `sku` is missing. */\n variantId?: number;\n price?: {\n amount: number;\n currency: string;\n formatted: string;\n };\n slug?: string;\n}\n\nexport interface MoneyAmount {\n amount: number;\n currency: string;\n formatted: string;\n}\n\nexport interface CartLineItemSnapshot {\n id: string;\n name: string;\n sku?: string;\n productId: string;\n quantity: number;\n imageUrl?: string;\n price?: MoneyAmount;\n totalPrice?: MoneyAmount;\n}\n\nexport interface CheckoutAddress {\n firstName: string;\n lastName: string;\n streetName: string;\n additionalStreetInfo?: string;\n postalCode: string;\n city: string;\n region?: string;\n country: string;\n email?: string;\n phone?: string;\n}\n\nexport interface ShippingMethodSnapshot {\n id: string;\n name: string;\n description?: string;\n price?: MoneyAmount;\n}\n\nexport interface PaymentSnapshot {\n id: string;\n key?: string;\n interfaceId?: string;\n paymentInterface: string;\n method: string;\n status: \"authorized\" | \"pending\" | \"failed\";\n amount: MoneyAmount;\n clientData?: Record<string, string>;\n}\n\nexport interface DeliverySnapshot {\n id: string;\n trackingId?: string;\n carrier?: string;\n provider?: string;\n}\n\n/** Client-safe cart shape — never the raw commercetools Cart object. */\nexport interface CartSnapshot {\n id: string;\n version: number;\n anonymousId?: string;\n customerId?: string;\n lineItems: CartLineItemSnapshot[];\n totalPrice: MoneyAmount;\n totalQuantity: number;\n shippingAddress?: CheckoutAddress;\n billingAddress?: CheckoutAddress;\n shippingMethod?: ShippingMethodSnapshot;\n payments?: PaymentSnapshot[];\n}\n\nexport interface OrderSnapshot {\n id: string;\n orderNumber?: string;\n orderState: string;\n createdAt?: string;\n paymentState?: string;\n shipmentState?: string;\n customerEmail?: string;\n totalPrice: MoneyAmount;\n lineItems: CartLineItemSnapshot[];\n shippingAddress?: CheckoutAddress;\n billingAddress?: CheckoutAddress;\n shippingMethod?: ShippingMethodSnapshot;\n payments?: PaymentSnapshot[];\n deliveries?: DeliverySnapshot[];\n}\n\n/** Public customer identity — never includes credentials. */\nexport interface CustomerSnapshot {\n id: string;\n email: string;\n}\n\n/** HMAC cart session header for GET /cart — never send the token in the query string. */\nexport const CART_SESSION_HEADER = \"x-commerce-ai-cart-session\";\n\n/** Upper bound for a single add-to-cart line quantity (mission add-all and batch add). */\nexport const MAX_LINE_ITEM_QUANTITY = 99;\n\nexport interface CartLoginRequest {\n email: string;\n password: string;\n anonymousId?: string;\n cartId?: string;\n catalogLocale?: string;\n}\n\nexport interface CartLoginResult {\n cart: CartSnapshot | null;\n customer: CustomerSnapshot;\n}\n\nexport interface AddToCartLineItem {\n sku?: string;\n productId?: string;\n variantId?: number;\n quantity?: number;\n}\n\nexport interface AddToCartRequest {\n anonymousId?: string;\n customerId?: string;\n sku?: string;\n productId?: string;\n variantId?: number;\n quantity?: number;\n currency?: string;\n country?: string;\n catalogLocale?: string;\n /** When set, add to this cart instead of looking up by anonymousId / customerId. */\n cartId?: string;\n sessionToken?: string;\n}\n\nexport interface AddItemsToCartRequest {\n anonymousId?: string;\n customerId?: string;\n items: AddToCartLineItem[];\n currency?: string;\n country?: string;\n catalogLocale?: string;\n cartId?: string;\n sessionToken?: string;\n}\n\nexport interface CartMutationRequest {\n anonymousId?: string;\n customerId?: string;\n lineItemId: string;\n cartId?: string;\n cartVersion?: number;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface UpdateCartQuantityRequest extends CartMutationRequest {\n quantity: number;\n}\n\nexport interface GetCartRequest {\n anonymousId?: string;\n customerId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface CheckoutRequest {\n anonymousId?: string;\n customerId?: string;\n cartId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface SetCartAddressesRequest extends CheckoutRequest {\n shippingAddress: CheckoutAddress;\n billingAddress?: CheckoutAddress;\n}\n\nexport interface SetShippingMethodRequest extends CheckoutRequest {\n shippingMethodId: string;\n}\n\nexport interface CreateOrderRequest extends CheckoutRequest {\n orderNumber?: string;\n}\n\nexport interface AuthorizePaymentRequest extends CheckoutRequest {\n method: string;\n orderNumber?: string;\n}\n\nexport interface GetOrderRequest {\n orderNumber?: string;\n anonymousId?: string;\n customerId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n}\n\nexport interface ListOrdersRequest {\n anonymousId?: string;\n customerId?: string;\n catalogLocale?: string;\n sessionToken?: string;\n limit?: number;\n}\n\nexport type SearchRelaxationLevel = \"none\" | \"drop_soft_filters\" | \"primary_any_match\";\n\nexport interface SearchMeta {\n total: number;\n limit: number;\n offset: number;\n /** @deprecated Use catalogLocale */\n locale: string;\n catalogLocale: string;\n queryLocale: string;\n queryInterpretation?: string;\n searchTerms?: string[];\n /** Catalog-language phrase closest to the user's intent, when the model supplied one. */\n primaryTerm?: string;\n appliedFilters?: InterpretedSearchFilters;\n /**\n * How far Product Search was relaxed after an empty first result.\n * Omitted when no relaxation ran (`none`).\n */\n relaxation?: SearchRelaxationLevel;\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n schemaEtag?: string;\n /** Per-step durations in milliseconds (dev / CAT_DEBUG only) */\n timings?: Record<string, number>;\n /** Total pipeline duration in milliseconds (dev / CAT_DEBUG only) */\n totalMs?: number;\n /**\n * Active OpenTelemetry / Langfuse trace id when Langfuse is enabled or CAT_DEBUG=true.\n * Non-stable for client contracts — intended for local/dev linking.\n */\n traceId?: string;\n}\n\nexport interface SearchResult {\n products: ProductCard[];\n meta: SearchMeta;\n facets?: SearchFacetGroup[];\n suggestedFacets?: SuggestedFacet[];\n facetSchema?: FacetAttributeDefinition[];\n /** Present when a compound shopping mission was executed. */\n mission?: MissionSearchResult;\n}\n\nexport interface SuggestionsRequest extends SearchLocaleOptions {\n query: string;\n limit?: number;\n}\n\nexport interface SuggestionsResult {\n suggestions: string[];\n /**\n * True when this request invoked AI `suggestSearchTerms` (not a cache hit).\n * Used by the server to flush Langfuse on serverless hosts.\n */\n aiFallbackUsed?: boolean;\n}\n\nexport interface TextSearchRequest {\n query: string;\n /** Language of the user query (AI input) */\n queryLocale?: string;\n /** Language products are indexed in commercetools */\n catalogLocale?: string;\n /** @deprecated Use queryLocale */\n locale?: string;\n limit?: number;\n offset?: number;\n filters?: InterpretedSearchFilters;\n searchTerms?: string[];\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n refineQuery?: string;\n includeFacets?: boolean;\n /** AI-suggested facets from the active search session (chip refine). */\n suggestedFacets?: SuggestedFacet[];\n /** Per-request override of `CommerceAIConfig.missions.enabled`. */\n enableMissions?: boolean;\n}\n\nexport interface SearchLocaleOptions {\n queryLocale?: string;\n catalogLocale?: string;\n /** @deprecated Use queryLocale */\n locale?: string;\n}\n\nexport interface SearchLocaleContext {\n queryLocale: string;\n catalogLocale: string;\n}\n\nexport interface VoiceSearchResult extends SearchResult {\n transcript: string;\n enhancedQuery?: string;\n}\n\nexport interface ImageSearchResult extends SearchResult {\n interpretation: string;\n}\n\n/** Standard filter keys returned by AI interpretation (all optional). */\nexport interface InterpretedSearchFilters {\n color?: string;\n brand?: string;\n category?: string;\n priceMin?: string;\n priceMax?: string;\n [key: string]: string | undefined;\n}\n\nexport interface InterpretedSearchQuery {\n /**\n * Catalog-language phrase closest to the user's intent.\n * Product Search boosts this phrase above the other `searchTerms`.\n * Kept separate from `searchTerms[0]` because same-language query passthrough\n * prepends the typed query without changing intent priority.\n */\n primaryTerm?: string;\n /**\n * Catalog-language search phrases. Product Search matches any phrase (OR).\n * Each element is a complete phrase (e.g. `\"red shoes\"`), not a split word.\n * Broad intents may include 3–5 synonym or hyponym phrases.\n */\n searchTerms: string[];\n filters?: InterpretedSearchFilters;\n suggestedFacets?: SuggestedFacet[];\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n interpretation: string;\n}\n\nexport interface ShoppingIntent {\n /** Stable id for UI keys and selection (`intent-0`, `intent-1`, …). */\n id: string;\n /** Short catalog-language label, e.g. \"tennis racket\". */\n label: string;\n /** Requested quantity, always >= 1. */\n quantity: number;\n /** Catalog-language phrase closest to this intent, boosted above alternates. */\n primaryTerm?: string;\n searchTerms: string[];\n filters?: InterpretedSearchFilters;\n sort?: \"relevance\" | \"price_asc\" | \"price_desc\";\n}\n\nexport interface DecomposedShoppingMission {\n isMission: boolean;\n /** 0–1. Missions below the configured minConfidence fall back to standard search. */\n confidence: number;\n intents: ShoppingIntent[];\n interpretation: string;\n}\n\nexport interface MissionIntentGroup {\n intent: ShoppingIntent;\n products: ProductCard[];\n total: number;\n /** True when this intent's Product Search failed; other groups still render. */\n failed?: boolean;\n}\n\nexport interface MissionSearchResult {\n intents: MissionIntentGroup[];\n interpretation: string;\n}\n\nexport type FacetAttributeType = \"enum\" | \"lenum\" | \"boolean\" | \"text\" | \"number\";\n\nexport interface FacetAttributeDefinition {\n name: string;\n label: string;\n kind: \"distinct\" | \"range\";\n attributeType: FacetAttributeType;\n field: string;\n fieldType?: string;\n}\n\nexport interface ResolvedFacetSchema {\n attributes: FacetAttributeDefinition[];\n systemFacets: Array<\"categories\" | \"price\">;\n etag: string;\n resolvedAt: string;\n}\n\nexport interface SuggestedFacet {\n name: string;\n reason?: string;\n}\n\nexport interface SearchFacetBucket {\n key: string;\n label: string;\n count: number;\n}\n\nexport interface SearchFacetGroup {\n id: string;\n label: string;\n type: \"distinct\" | \"range\";\n buckets: SearchFacetBucket[];\n selectedKey?: string;\n}\n\nexport interface SearchSessionState {\n query: string;\n searchTerms: string[];\n appliedFilters: InterpretedSearchFilters;\n suggestedFacets: SuggestedFacet[];\n facetSchema?: FacetAttributeDefinition[];\n schemaEtag?: string;\n}\n\nexport interface VoiceAudioInterpretation extends InterpretedSearchQuery {\n transcript: string;\n enhancedQuery: string;\n}\n\n/** @deprecated Use ProductSearchRequest from query-builder / platform-sdk */\nexport type { ProductSearchRequest as ProductSearchQueryBody } from \"@commercetools/platform-sdk\";\n","export interface CommerceAISearchMessages {\n placeholder: string;\n searchAriaLabel: string;\n productSearchAriaLabel: string;\n searchResultsAriaLabel: string;\n searching: string;\n noProductsFound: string;\n searchedFor: string;\n dropImageToSearch: string;\n voiceSearch: string;\n stopRecording: string;\n searchByCamera: string;\n searchByImage: string;\n replayVoiceSummary: string;\n listening: string;\n tapMicToStop: string;\n understandingQuery: string;\n preparingAudioSummary: string;\n cameraCapture: string;\n cameraPreview: string;\n capturePhoto: string;\n cancel: string;\n dismiss: string;\n searchFailed: string;\n imageSearchFailed: string;\n couldNotCapturePhoto: string;\n suggestionsAriaLabel: string;\n loadingSuggestions: string;\n noSuggestions: string;\n filtersAriaLabel: string;\n clearFilters: string;\n narrowResults: string;\n newSearch: string;\n addToCart: string;\n cart: string;\n emptyCart: string;\n removeItem: string;\n total: string;\n each: string;\n cartAriaLabel: string;\n itemAdded: string;\n increaseQuantity: string;\n decreaseQuantity: string;\n closeCart: string;\n unableToAddToCart: string;\n missionTitle: string;\n missionQuantity: string;\n /** Adds the first (recommended) product from each filled lane. Use `{count}` for the number of picks. */\n missionAddAll: string;\n missionItemsAdded: string;\n missionSelectProduct: string;\n missionIntentEmpty: string;\n missionIntentFailed: string;\n signIn: string;\n signInToSyncCart: string;\n signOut: string;\n email: string;\n password: string;\n signedInAs: string;\n invalidCredentials: string;\n signInFailed: string;\n checkout: string;\n checkoutTitle: string;\n orderSummary: string;\n shippingAddress: string;\n billingAddress: string;\n billingSameAsShipping: string;\n continueToDelivery: string;\n shippingMethod: string;\n selectShippingMethod: string;\n noShippingMethods: string;\n completeAddressToContinue: string;\n selectDeliveryToContinue: string;\n selectPaymentToContinue: string;\n checkoutStepAddress: string;\n checkoutStepDelivery: string;\n checkoutStepPayment: string;\n paymentMethod: string;\n selectPaymentMethod: string;\n paymentAuthorized: string;\n paymentDeclined: string;\n continueToPayment: string;\n placeOrder: string;\n placingOrder: string;\n orderPlaced: string;\n viewOrderStatus: string;\n continueShopping: string;\n checkoutFailed: string;\n orderStatus: string;\n orderPlacedAt: string;\n paymentStateLabel: string;\n shipmentStateLabel: string;\n trackingNumber: string;\n carrier: string;\n noTrackingYet: string;\n orderNotFound: string;\n noOrdersYet: string;\n yourOrders: string;\n firstName: string;\n lastName: string;\n streetName: string;\n additionalAddress: string;\n postalCode: string;\n city: string;\n region: string;\n country: string;\n}\n\nexport const DEFAULT_COMMERCE_AI_SEARCH_MESSAGES: CommerceAISearchMessages = {\n placeholder: \"What are you looking for?\",\n searchAriaLabel: \"Search query\",\n productSearchAriaLabel: \"Product search\",\n searchResultsAriaLabel: \"Search results\",\n searching: \"Searching...\",\n noProductsFound: \"No products found\",\n searchedFor: \"Searched for:\",\n dropImageToSearch: \"Drop image to search\",\n voiceSearch: \"Voice search\",\n stopRecording: \"Stop recording\",\n searchByCamera: \"Search by camera\",\n searchByImage: \"Search by image\",\n replayVoiceSummary: \"Replay voice result summary\",\n listening: \"Listening…\",\n tapMicToStop: \"Tap mic to stop\",\n understandingQuery: \"Understanding your query…\",\n preparingAudioSummary: \"Preparing audio summary…\",\n cameraCapture: \"Camera capture\",\n cameraPreview: \"Camera preview\",\n capturePhoto: \"Capture\",\n cancel: \"Cancel\",\n dismiss: \"Dismiss\",\n searchFailed: \"Search failed\",\n imageSearchFailed: \"Image search failed\",\n couldNotCapturePhoto: \"Could not capture photo\",\n suggestionsAriaLabel: \"Search suggestions\",\n loadingSuggestions: \"Loading suggestions...\",\n noSuggestions: \"No suggestions\",\n filtersAriaLabel: \"Search filters\",\n clearFilters: \"Clear filters\",\n narrowResults: \"Narrow your results\",\n newSearch: \"New search\",\n addToCart: \"Add to cart\",\n cart: \"Cart\",\n emptyCart: \"Your cart is empty\",\n removeItem: \"Remove item\",\n total: \"Total\",\n each: \"each\",\n cartAriaLabel: \"Shopping cart\",\n itemAdded: \"Added to cart\",\n increaseQuantity: \"Increase quantity\",\n decreaseQuantity: \"Decrease quantity\",\n closeCart: \"Close cart\",\n unableToAddToCart: \"This product cannot be added to the cart\",\n missionTitle: \"Shopping list\",\n missionQuantity: \"Looking for\",\n missionAddAll: \"Add {count} top picks to cart\",\n missionItemsAdded: \"Added to cart\",\n missionSelectProduct: \"Select this product\",\n missionIntentEmpty: \"No matching products\",\n missionIntentFailed: \"Couldn't search this item\",\n signIn: \"Sign in\",\n signInToSyncCart: \"Sign in to sync your cart\",\n signOut: \"Sign out\",\n email: \"Email\",\n password: \"Password\",\n signedInAs: \"Signed in as\",\n invalidCredentials: \"Invalid email or password\",\n signInFailed: \"Sign in failed\",\n checkout: \"Checkout\",\n checkoutTitle: \"Complete your order\",\n orderSummary: \"Order summary\",\n shippingAddress: \"Shipping address\",\n billingAddress: \"Billing address\",\n billingSameAsShipping: \"Billing address is the same as shipping\",\n continueToDelivery: \"Continue to delivery\",\n shippingMethod: \"Delivery method\",\n selectShippingMethod: \"Select a delivery method\",\n noShippingMethods: \"No delivery methods are available for this address\",\n completeAddressToContinue: \"Add a shipping address to continue\",\n selectDeliveryToContinue: \"Select a delivery method to continue\",\n selectPaymentToContinue: \"Select a payment method to continue\",\n checkoutStepAddress: \"Address\",\n checkoutStepDelivery: \"Delivery\",\n checkoutStepPayment: \"Payment\",\n paymentMethod: \"Payment method\",\n selectPaymentMethod: \"Select a payment method\",\n paymentAuthorized: \"Payment authorized\",\n paymentDeclined: \"Payment was declined\",\n continueToPayment: \"Continue to payment\",\n placeOrder: \"Place order\",\n placingOrder: \"Placing order…\",\n orderPlaced: \"Order placed\",\n viewOrderStatus: \"View order status\",\n continueShopping: \"Continue shopping\",\n checkoutFailed: \"Checkout failed\",\n orderStatus: \"Order status\",\n orderPlacedAt: \"Placed\",\n paymentStateLabel: \"Payment\",\n shipmentStateLabel: \"Shipment\",\n trackingNumber: \"Tracking number\",\n carrier: \"Carrier\",\n noTrackingYet: \"Tracking details will appear when the order ships\",\n orderNotFound: \"Order not found\",\n noOrdersYet: \"No orders yet\",\n yourOrders: \"Your orders\",\n firstName: \"First name\",\n lastName: \"Last name\",\n streetName: \"Street address\",\n additionalAddress: \"Apartment, suite, etc. (optional)\",\n postalCode: \"Postal code\",\n city: \"City\",\n region: \"State or region (optional)\",\n country: \"Country\",\n};\n\nexport function resolveCommerceAISearchMessages(\n overrides?: Partial<CommerceAISearchMessages>,\n): CommerceAISearchMessages {\n return {\n ...DEFAULT_COMMERCE_AI_SEARCH_MESSAGES,\n ...overrides,\n };\n}\n","const COMPOUND_SHOPPING_LIST_PATTERN = /\\band\\b|,|\\bplus\\b|\\boraz\\b/i;\n\n/**\n * Heuristic for the search box: a new multi-item query should not be treated as\n * a facet NL refine of the previous session.\n */\nexport function looksLikeCompoundShoppingList(query: string): boolean {\n return COMPOUND_SHOPPING_LIST_PATTERN.test(query.trim());\n}\n","/** CSS Color Module named colors used to label hex facet values. */\nconst CSS_NAMED_COLOR_TABLE = `\nAlice Blue:f0f8ff\nAntique White:faebd7\nAqua:00ffff\nAquamarine:7fffd4\nAzure:f0ffff\nBeige:f5f5dc\nBisque:ffe4c4\nBlack:000000\nBlanched Almond:ffebcd\nBlue:0000ff\nBlue Violet:8a2be2\nBrown:a52a2a\nBurlywood:deb887\nCadet Blue:5f9ea0\nChartreuse:7fff00\nChocolate:d2691e\nCoral:ff7f50\nCornflower Blue:6495ed\nCornsilk:fff8dc\nCrimson:dc143c\nDark Blue:00008b\nDark Cyan:008b8b\nDark Goldenrod:b8860b\nDark Gray:a9a9a9\nDark Green:006400\nDark Khaki:bdb76b\nDark Magenta:8b008b\nDark Olive Green:556b2f\nDark Orange:ff8c00\nDark Orchid:9932cc\nDark Red:8b0000\nDark Salmon:e9967a\nDark Sea Green:8fbc8f\nDark Slate Blue:483d8b\nDark Slate Gray:2f4f4f\nDark Turquoise:00ced1\nDark Violet:9400d3\nDeep Pink:ff1493\nDeep Sky Blue:00bfff\nDim Gray:696969\nDodger Blue:1e90ff\nFirebrick:b22222\nFloral White:fffaf0\nForest Green:228b22\nGainsboro:dcdcdc\nGhost White:f8f8ff\nGold:ffd700\nGoldenrod:daa520\nGray:808080\nGreen:008000\nGreen Yellow:adff2f\nHoneydew:f0fff0\nHot Pink:ff69b4\nIndian Red:cd5c5c\nIndigo:4b0082\nIvory:fffff0\nKhaki:f0e68c\nLavender:e6e6fa\nLavender Blush:fff0f5\nLawn Green:7cfc00\nLemon Chiffon:fffacd\nLight Blue:add8e6\nLight Coral:f08080\nLight Cyan:e0ffff\nLight Goldenrod Yellow:fafad2\nLight Gray:d3d3d3\nLight Green:90ee90\nLight Pink:ffb6c1\nLight Salmon:ffa07a\nLight Sea Green:20b2aa\nLight Sky Blue:87cefa\nLight Slate Gray:778899\nLight Steel Blue:b0c4de\nLight Yellow:ffffe0\nLime:00ff00\nLime Green:32cd32\nLinen:faf0e6\nMagenta:ff00ff\nMaroon:800000\nMedium Aquamarine:66cdaa\nMedium Blue:0000cd\nMedium Orchid:ba55d3\nMedium Purple:9370db\nMedium Sea Green:3cb371\nMedium Slate Blue:7b68ee\nMedium Spring Green:00fa9a\nMedium Turquoise:48d1cc\nMedium Violet Red:c71585\nMidnight Blue:191970\nMint Cream:f5fffa\nMisty Rose:ffe4e1\nMoccasin:ffe4b5\nNavajo White:ffdead\nNavy:000080\nOld Lace:fdf5e6\nOlive:808000\nOlive Drab:6b8e23\nOrange:ffa500\nOrange Red:ff4500\nOrchid:da70d6\nPale Goldenrod:eee8aa\nPale Green:98fb98\nPale Turquoise:afeeee\nPale Violet Red:db7093\nPapaya Whip:ffefd5\nPeach Puff:ffdab9\nPeru:cd853f\nPink:ffc0cb\nPlum:dda0dd\nPowder Blue:b0e0e6\nPurple:800080\nRebecca Purple:663399\nRed:ff0000\nRosy Brown:bc8f8f\nRoyal Blue:4169e1\nSaddle Brown:8b4513\nSalmon:fa8072\nSandy Brown:f4a460\nSea Green:2e8b57\nSeashell:fff5ee\nSienna:a0522d\nSilver:c0c0c0\nSky Blue:87ceeb\nSlate Blue:6a5acd\nSlate Gray:708090\nSnow:fffafa\nSpring Green:00ff7f\nSteel Blue:4682b4\nTan:d2b48c\nTeal:008080\nThistle:d8bfd8\nTomato:ff6347\nTurquoise:40e0d0\nViolet:ee82ee\nWheat:f5deb3\nWhite:ffffff\nWhite Smoke:f5f5f5\nYellow:ffff00\nYellow Green:9acd32\n`;\n\nconst HEX_COLOR_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\ntype NamedColor = { name: string; r: number; g: number; b: number };\n\nconst NAMED_COLORS: NamedColor[] = CSS_NAMED_COLOR_TABLE.trim()\n .split(\"\\n\")\n .flatMap((line) => {\n const separator = line.indexOf(\":\");\n if (separator <= 0) {\n return [];\n }\n const name = line.slice(0, separator);\n const hex = line.slice(separator + 1);\n const n = Number.parseInt(hex, 16);\n if (!name || !Number.isFinite(n)) {\n return [];\n }\n return [{ name, r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }];\n });\n\nfunction parseHexRgb(value: string): [number, number, number] | null {\n const match = HEX_COLOR_RE.exec(value.trim());\n const captured = match?.[1];\n if (!captured) {\n return null;\n }\n const hex =\n captured.length === 3\n ? `${captured[0]}${captured[0]}${captured[1]}${captured[1]}${captured[2]}${captured[2]}`\n : captured;\n const n = Number.parseInt(hex, 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction nearestCssColor(rgb: [number, number, number]): { name: string; dist: number } | null {\n const first = NAMED_COLORS[0];\n if (!first) {\n return null;\n }\n let bestName = first.name;\n let bestDist = Number.POSITIVE_INFINITY;\n for (const color of NAMED_COLORS) {\n const dist =\n (rgb[0] - color.r) ** 2 + (rgb[1] - color.g) ** 2 + (rgb[2] - color.b) ** 2;\n if (dist < bestDist) {\n bestDist = dist;\n bestName = color.name;\n if (dist === 0) {\n return { name: bestName, dist };\n }\n }\n }\n return { name: bestName, dist: bestDist };\n}\n\n/** Max squared RGB distance to accept a CSS name instead of the raw hex. */\nconst MAX_NAMED_COLOR_DISTANCE_SQ = 40 * 40;\n\nconst COLOR_LIKE_FACET_NAME = /(?:^|[-_])colou?r(?:[-_]|$)|(?:^|[-_])finish(?:[-_]|$)/i;\n\n/** True when a facet id looks like a color/finish attribute (`color-code`, `finish-code`). */\nexport function isColorLikeFacetName(name: string): boolean {\n return COLOR_LIKE_FACET_NAME.test(name.trim());\n}\n\n/** True when a facet bucket key is a CSS hex color (`#RGB` or `#RRGGBB`). */\nexport function isHexColorFacetKey(value: string): boolean {\n return HEX_COLOR_RE.test(value.trim());\n}\n\n/** Normalized `#rrggbb` for a hex facet key, or `undefined` when not a color. */\nexport function hexColorSwatchValue(value: string): string | undefined {\n const rgb = parseHexRgb(value);\n if (!rgb) {\n return undefined;\n }\n return `#${rgb.map((channel) => channel.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/**\n * Human-readable facet chip label. Hex keys on color/finish facets become the\n * nearest CSS color name when close enough; other keys are unchanged.\n */\nexport function formatFacetBucketLabel(key: string, facetName?: string): string {\n if (facetName !== undefined && !isColorLikeFacetName(facetName)) {\n return key;\n }\n\n const rgb = parseHexRgb(key);\n if (!rgb) {\n return key;\n }\n\n const nearest = nearestCssColor(rgb);\n if (!nearest || nearest.dist > MAX_NAMED_COLOR_DISTANCE_SQ) {\n return key;\n }\n\n return nearest.name;\n}\n","import type {\n FacetAttributeDefinition,\n InterpretedSearchFilters,\n ResolvedFacetSchema,\n SearchFacetGroup,\n SuggestedFacet,\n} from \"../types/index.js\";\nimport { formatFacetBucketLabel } from \"./facet-color.js\";\n\nexport const PRICE_FACET_ID = \"price\";\nexport const CATEGORIES_FACET_ID = \"categories\";\n\nexport const DEFAULT_PRICE_RANGES = [\n { key: \"under-50\", to: 5_000 },\n { key: \"50-to-100\", from: 5_000, to: 10_000 },\n { key: \"100-plus\", from: 10_000 },\n] as const;\n\nexport type PriceRangeDefinition = (typeof DEFAULT_PRICE_RANGES)[number];\n\n/** Major currency units (e.g. EUR) corresponding to a price range bucket. */\nexport function priceRangeToFilterValues(\n range: PriceRangeDefinition,\n): Pick<InterpretedSearchFilters, \"priceMin\" | \"priceMax\"> {\n const filters: Pick<InterpretedSearchFilters, \"priceMin\" | \"priceMax\"> = {};\n if (\"from\" in range && range.from !== undefined) {\n filters.priceMin = String(range.from / 100);\n }\n if (\"to\" in range && range.to !== undefined) {\n filters.priceMax = String(range.to / 100);\n }\n return filters;\n}\n\nexport function priceBucketKeyFromFilters(\n filters: InterpretedSearchFilters,\n): string | undefined {\n const minCents = parseMajorToCents(filters.priceMin);\n const maxCents = parseMajorToCents(filters.priceMax);\n\n for (const range of DEFAULT_PRICE_RANGES) {\n const from = \"from\" in range ? range.from : undefined;\n const to = \"to\" in range ? range.to : undefined;\n if (minCents === from && maxCents === to) {\n return range.key;\n }\n }\n return undefined;\n}\n\nexport function isFacetFilterSelected(\n filters: InterpretedSearchFilters,\n facetId: string,\n bucketKey: string,\n): boolean {\n if (facetId === CATEGORIES_FACET_ID) {\n return filters.category === bucketKey;\n }\n if (facetId === PRICE_FACET_ID) {\n return priceBucketKeyFromFilters(filters) === bucketKey;\n }\n return filters[facetId] === bucketKey;\n}\n\n/**\n * Toggle a facet chip into filter state. Price buckets map to priceMin/priceMax;\n * categories map to `category`.\n */\nexport function toggleFacetFilter(\n filters: InterpretedSearchFilters,\n facetId: string,\n bucketKey: string,\n): InterpretedSearchFilters {\n const next = { ...filters };\n\n if (facetId === PRICE_FACET_ID) {\n if (priceBucketKeyFromFilters(next) === bucketKey) {\n delete next.priceMin;\n delete next.priceMax;\n return next;\n }\n delete next.priceMin;\n delete next.priceMax;\n const range = DEFAULT_PRICE_RANGES.find((candidate) => candidate.key === bucketKey);\n if (!range) {\n return next;\n }\n return { ...next, ...priceRangeToFilterValues(range) };\n }\n\n const filterKey = facetId === CATEGORIES_FACET_ID ? \"category\" : facetId;\n if (next[filterKey] === bucketKey) {\n delete next[filterKey];\n } else {\n next[filterKey] = bucketKey;\n }\n return next;\n}\n\nexport function buildProductSearchFacets(\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[] = [],\n): unknown[] {\n const names = requestedFacetNames(schema, suggestedFacets);\n const facets: unknown[] = [];\n for (const name of names) {\n if (name === CATEGORIES_FACET_ID) {\n facets.push({ distinct: { name, field: \"categories\", limit: 12, level: \"products\" } });\n continue;\n }\n if (name === PRICE_FACET_ID) {\n facets.push({\n ranges: {\n name,\n field: \"variants.prices.centAmount\",\n ranges: DEFAULT_PRICE_RANGES.map((range) => ({ ...range })),\n level: \"products\",\n },\n });\n continue;\n }\n\n const attribute = schema.attributes.find((candidate) => candidate.name === name);\n if (!attribute || attribute.kind !== \"distinct\") {\n continue;\n }\n facets.push({\n distinct: {\n name: attribute.name,\n field: attribute.field,\n fieldType: attribute.fieldType,\n limit: 12,\n level: \"products\",\n },\n });\n }\n return facets;\n}\n\n/** Product Projection Search `facet` query parameter expressions. */\nexport function buildProjectionFacetParams(\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[] = [],\n): string[] {\n const names = requestedFacetNames(schema, suggestedFacets);\n const facets: string[] = [];\n\n for (const name of names) {\n if (name === CATEGORIES_FACET_ID) {\n facets.push(`categories.id as ${CATEGORIES_FACET_ID}`);\n continue;\n }\n if (name === PRICE_FACET_ID) {\n const ranges = DEFAULT_PRICE_RANGES.map((range) => {\n const from = \"from\" in range && range.from !== undefined ? String(range.from) : \"*\";\n const to = \"to\" in range && range.to !== undefined ? String(range.to) : \"*\";\n return `(${from} to ${to})`;\n }).join(\", \");\n facets.push(`variants.prices.centAmount:range ${ranges} as ${PRICE_FACET_ID}`);\n continue;\n }\n\n const attribute = schema.attributes.find((candidate) => candidate.name === name);\n if (!attribute || attribute.kind !== \"distinct\") {\n continue;\n }\n facets.push(`${attribute.field} as ${attribute.name}`);\n }\n\n return facets;\n}\n\nexport function normalizeProductSearchFacets(\n rawFacets: unknown,\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[] = [],\n filters: InterpretedSearchFilters = {},\n): SearchFacetGroup[] {\n const raw = Array.isArray(rawFacets)\n ? rawFacets\n : rawFacets && typeof rawFacets === \"object\"\n ? Object.entries(rawFacets as Record<string, unknown>).map(([name, facet]) => ({\n name,\n ...(facet && typeof facet === \"object\" ? facet : {}),\n }))\n : [];\n const labels = new Map(schema.attributes.map((attribute) => [attribute.name, attribute.label]));\n\n const facets: SearchFacetGroup[] = [];\n for (const facet of raw) {\n if (!facet || typeof facet !== \"object\") {\n continue;\n }\n const record = facet as {\n name?: unknown;\n buckets?: unknown;\n terms?: unknown;\n ranges?: unknown;\n };\n if (typeof record.name !== \"string\") {\n continue;\n }\n\n const buckets = normalizeFacetBuckets({\n name: record.name,\n buckets: record.buckets,\n terms: record.terms,\n ranges: record.ranges,\n });\n if (!buckets.length) {\n continue;\n }\n\n facets.push({\n id: record.name,\n label:\n labels.get(record.name) ??\n (record.name === CATEGORIES_FACET_ID\n ? \"Categories\"\n : record.name === PRICE_FACET_ID\n ? \"Price\"\n : record.name),\n type: record.name === PRICE_FACET_ID ? \"range\" : \"distinct\",\n buckets,\n ...(selectedFacetValue(record.name, filters)\n ? { selectedKey: selectedFacetValue(record.name, filters) }\n : {}),\n });\n }\n const requested = requestedFacetNames(schema, suggestedFacets);\n return facets.sort((left, right) => requested.indexOf(left.id) - requested.indexOf(right.id));\n}\n\nexport function filterFacetSuggestions(\n suggestedFacets: SuggestedFacet[] | undefined,\n schema: ResolvedFacetSchema,\n): SuggestedFacet[] {\n const known = new Set([...schema.attributes.map((attribute) => attribute.name), ...schema.systemFacets]);\n return (suggestedFacets ?? []).filter((facet) => known.has(facet.name)).slice(0, 5);\n}\n\nexport function getFacetAttribute(\n schema: ResolvedFacetSchema,\n name: string,\n): FacetAttributeDefinition | undefined {\n return schema.attributes.find((attribute) => attribute.name === name);\n}\n\nfunction requestedFacetNames(\n schema: ResolvedFacetSchema,\n suggestedFacets: SuggestedFacet[],\n): string[] {\n const suggested = filterFacetSuggestions(suggestedFacets, schema).map((facet) => facet.name);\n return suggested.length ? suggested : schema.attributes.slice(0, 3).map((attribute) => attribute.name);\n}\n\nfunction normalizeFacetBuckets(record: {\n buckets?: unknown;\n terms?: unknown;\n ranges?: unknown;\n name?: string;\n}): Array<{ key: string; label: string; count: number }> {\n if (Array.isArray(record.buckets)) {\n return record.buckets.flatMap((bucket) => normalizeBucket(bucket, record.name));\n }\n\n if (Array.isArray(record.terms)) {\n return record.terms.flatMap((term) => normalizeTermBucket(term, record.name));\n }\n\n if (Array.isArray(record.ranges)) {\n return record.ranges.flatMap((range, index) => normalizeRangeBucket(range, index));\n }\n\n return [];\n}\n\nfunction normalizeBucket(bucket: unknown, facetName?: string) {\n if (!bucket || typeof bucket !== \"object\") {\n return [];\n }\n const record = bucket as { key?: unknown; count?: unknown };\n if (typeof record.key !== \"string\" || typeof record.count !== \"number\") {\n return [];\n }\n return [{ key: record.key, label: formatFacetBucketLabel(record.key, facetName), count: record.count }];\n}\n\nfunction normalizeTermBucket(bucket: unknown, facetName?: string) {\n if (!bucket || typeof bucket !== \"object\") {\n return [];\n }\n const record = bucket as { term?: unknown; count?: unknown };\n if (typeof record.term !== \"string\" || typeof record.count !== \"number\") {\n return [];\n }\n return [{ key: record.term, label: formatFacetBucketLabel(record.term, facetName), count: record.count }];\n}\n\nfunction normalizeRangeBucket(bucket: unknown, index: number) {\n if (!bucket || typeof bucket !== \"object\") {\n return [];\n }\n const record = bucket as { from?: unknown; to?: unknown; count?: unknown; key?: unknown };\n if (typeof record.count !== \"number\") {\n return [];\n }\n\n if (typeof record.key === \"string\") {\n return [{ key: record.key, label: record.key, count: record.count }];\n }\n\n const from = typeof record.from === \"number\" ? record.from : undefined;\n const to = typeof record.to === \"number\" ? record.to : undefined;\n const matched = DEFAULT_PRICE_RANGES.find((range) => {\n const rangeFrom = \"from\" in range ? range.from : undefined;\n const rangeTo = \"to\" in range ? range.to : undefined;\n return rangeFrom === from && rangeTo === to;\n });\n const key = matched?.key ?? `${from ?? \"*\"}-${to ?? \"*\"}-${index}`;\n return [{ key, label: key, count: record.count }];\n}\n\nfunction selectedFacetValue(name: string, filters: InterpretedSearchFilters): string | undefined {\n if (name === CATEGORIES_FACET_ID) {\n return filters.category;\n }\n if (name === PRICE_FACET_ID) {\n return priceBucketKeyFromFilters(filters);\n }\n return filters[name];\n}\n\nfunction parseMajorToCents(value?: string): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return undefined;\n }\n return Math.round(parsed * 100);\n}\n"],"mappings":"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","names":[]}