From 0280c0eeab50f46a4adec7f980f8428b2d447214 Mon Sep 17 00:00:00 2001 From: lasoh Date: Fri, 31 Jul 2026 14:56:44 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(CXCDS-18834):=20AI=20Search=20POC=20?= =?UTF-8?q?=E2=80=94=20mock=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds AI Search feature behind useAiSearch feature toggle (mock data, no backend dependency). Includes: - AI mode toggle in search box (single pill button, Figma-aligned) - AI search panel with suggestions outlet - Product criteria badge on PLP items (mock: rotating 3/3, 2/3, 1/3 matches) - Product criteria on PDP via CurrentProductService - AI answer banner outlet on product list - Navigation to search results on AI query - Feature toggle: useAiSearch (default false, enabled in storefrontapp) - Translations for AI search UI (aiToggle, aiCriteria, aiPanel) - Styles: searchbox toggle, criteria badge, product list/grid items --- .../assets/src/translations/en/common.json | 23 +- .../feature-toggles/config/feature-toggles.ts | 2 + .../search-box-component.service.ts | 87 ++++++ .../search-box/search-box-outlets.model.ts | 2 + .../search-box/search-box.component.html | 87 +++++- .../search-box/search-box.component.ts | 68 ++++- .../container/product-list.component.html | 1 + .../product-grid-item.component.html | 2 + .../product-list-item.component.html | 2 + .../product/product-outlets.model.ts | 3 + .../product-summary.component.html | 5 + .../product/list/_product-grid-item.scss | 98 ++++++ .../product/list/_product-list-item.scss | 95 ++++++ .../product/list/_product-list.scss | 24 ++ .../components/product/search/_searchbox.scss | 286 +++++++++++++++++- integration-libs/cds/public_api.ts | 4 + .../ai-search/ai-answer-banner.component.ts | 84 +++++ .../ai-product-criteria-pdp.component.ts | 54 ++++ .../ai-product-criteria.component.html | 51 ++++ .../ai-product-criteria.component.ts | 58 ++++ .../ai-search/ai-product-criteria.service.ts | 63 ++++ .../ai-search/ai-search-backend.service.ts | 180 +++++++++++ .../ai-search-suggestions.component.html | 76 +++++ .../ai-search-suggestions.component.ts | 111 +++++++ .../ai-search/ai-search-suggestions.module.ts | 62 ++++ .../ai-search-suggestions.service.ts | 59 ++++ .../features/cds/cds-feature.module.ts | 43 ++- .../spartacus/spartacus-features.module.ts | 1 + 28 files changed, 1607 insertions(+), 24 deletions(-) create mode 100644 integration-libs/cds/src/ai-search/ai-answer-banner.component.ts create mode 100644 integration-libs/cds/src/ai-search/ai-product-criteria-pdp.component.ts create mode 100644 integration-libs/cds/src/ai-search/ai-product-criteria.component.html create mode 100644 integration-libs/cds/src/ai-search/ai-product-criteria.component.ts create mode 100644 integration-libs/cds/src/ai-search/ai-product-criteria.service.ts create mode 100644 integration-libs/cds/src/ai-search/ai-search-backend.service.ts create mode 100644 integration-libs/cds/src/ai-search/ai-search-suggestions.component.html create mode 100644 integration-libs/cds/src/ai-search/ai-search-suggestions.component.ts create mode 100644 integration-libs/cds/src/ai-search/ai-search-suggestions.module.ts create mode 100644 integration-libs/cds/src/ai-search/ai-search-suggestions.service.ts diff --git a/core-libs/assets/src/translations/en/common.json b/core-libs/assets/src/translations/en/common.json index 1784dbd32c8..f0565390acf 100644 --- a/core-libs/assets/src/translations/en/common.json +++ b/core-libs/assets/src/translations/en/common.json @@ -96,7 +96,28 @@ "empty": "Ask us anything" }, "closeSearchPanel": "Close", - "queryError": "Your search query is incorrectly formatted. Please remove special characters like \":\" and try again." + "queryError": "Your search query is incorrectly formatted. Please remove special characters like \":\" and try again.", + "aiPlaceholder": "Describe what you're looking for in natural language. AI search may take a bit longer.", + "aiToggle": { + "groupLabel": "Search mode", + "regular": "Switch to standard search", + "regularLabel": "Search", + "regularTooltip": "Search", + "ai": "Switch to AI search", + "aiLabel": "AI Mode", + "aiTooltip": "AI Search" + }, + "aiPanel": { + "examples": "AI search inspiration", + "results": "AI search suggestions" + }, + "aiCriteria": { + "fullMatch": "Fully matches your search criteria", + "partialMatch": "Matches {{matched}} out of {{total}} search criteria", + "tooltipTitle": "Matches {{matched}} of {{total}} search criteria:", + "matched": "Matched", + "notMatched": "Not Matched" + } }, "sorting": { "date": "Date", diff --git a/core-libs/core/src/features-config/feature-toggles/config/feature-toggles.ts b/core-libs/core/src/features-config/feature-toggles/config/feature-toggles.ts index b5995d92aa8..66fd6fd598a 100644 --- a/core-libs/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/core-libs/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -572,6 +572,7 @@ export interface FeatureTogglesInterface { * Login Page form submission. */ siteIsolationForCustomLoginPage?: boolean; + useAiSearch?: boolean; /** * When enabled, the navigation menu buttons (e.g. "My Account") and dropdown @@ -788,4 +789,5 @@ export const defaultFeatureToggles: Required = { a11yDisabledButtonContrast: false, a11yAddressFormInitialFocus: false, a11yFocusBreadcrumbOnNavigation: false, + useAiSearch: false, }; diff --git a/core-libs/storefront/cms-components/navigation/search-box/search-box-component.service.ts b/core-libs/storefront/cms-components/navigation/search-box/search-box-component.service.ts index f7f6bbcb0e7..cc99b95a39c 100644 --- a/core-libs/storefront/cms-components/navigation/search-box/search-box-component.service.ts +++ b/core-libs/storefront/cms-components/navigation/search-box/search-box-component.service.ts @@ -38,6 +38,17 @@ export class SearchBoxComponentService { chosenWord = new ReplaySubject(); sharedEvent = new ReplaySubject(); searchCompleted = new BehaviorSubject(false); + private readonly _currentQuery$ = new BehaviorSubject(''); + readonly currentQuery$: Observable = this._currentQuery$.asObservable(); + + private readonly _isAiModeActive$ = new BehaviorSubject(false); + readonly isAiModeActive$: Observable = this._isAiModeActive$.asObservable(); + + private readonly _lastSearchWasAi$ = new BehaviorSubject(false); + readonly lastSearchWasAi$: Observable = this._lastSearchWasAi$.asObservable(); + + private readonly _lastAiQuery$ = new BehaviorSubject(''); + readonly lastAiQuery$: Observable = this._lastAiQuery$.asObservable(); protected enableRecentSearches: boolean = false; protected enableTrendingSearches: boolean = false; @@ -60,6 +71,7 @@ export class SearchBoxComponentService { this.hasKeywordRedirect = false; this.currentQueryLength = query ? query.length : 0; this.searchCompleted.next(false); + this._currentQuery$.next(query ?? ''); if ( !this.enableRecentSearches && @@ -161,6 +173,7 @@ export class SearchBoxComponentService { clearResults() { this.searchService.clearResults(); this.toggleBodyClass(HAS_SEARCH_RESULT_CLASS, false); + this._currentQuery$.next(''); // Reset search completion state this.hasKeywordRedirect = false; @@ -360,4 +373,78 @@ export class SearchBoxComponentService { setRecentSearches(enabled: boolean = false) { this.enableRecentSearches = enabled; } + + setAiMode(active: boolean): void { + this._isAiModeActive$.next(active); + this.persistAiModePreference(active); + } + + /** + * Persists the user's search-mode toggle choice (regular vs AI) so it is + * restored on the next visit. Uses localStorage because this is a lasting UI + * preference, unlike the per-search AI context which lives in sessionStorage. + */ + private persistAiModePreference(active: boolean): void { + try { + this.winRef.localStorage?.setItem( + 'cx_ai_mode_preference', + active ? '1' : '0' + ); + } catch { + // localStorage may be unavailable (SSR, private mode, quota); ignore. + } + } + + getAiModePreference(): boolean { + try { + return this.winRef.localStorage?.getItem('cx_ai_mode_preference') === '1'; + } catch { + // localStorage may be unavailable (SSR, private mode); default to regular. + return false; + } + } + + markAiSearchLaunched(isAiMode: boolean): void { + this._lastSearchWasAi$.next(isAiMode); + if (!isAiMode) { + this.clearAiContext(); + } + } + + setAiQuery(query: string): void { + this._lastAiQuery$.next(query); + this.persistAiContext(query); + } + + private persistAiContext(query: string): void { + try { + const storage = this.winRef.sessionStorage; + if (storage) { + storage.setItem('cx_ai_context', JSON.stringify({ query, ts: Date.now() })); + } + } catch {} + } + + restoreAiContextFromStorage(): void { + try { + const storage = this.winRef.sessionStorage; + const raw = storage?.getItem('cx_ai_context'); + if (!raw) return; + const { query, ts } = JSON.parse(raw) as { query: string; ts: number }; + // Ignore if older than 30 minutes + if (Date.now() - ts > 30 * 60 * 1000) { + storage?.removeItem('cx_ai_context'); + return; + } + this._lastSearchWasAi$.next(true); + this._lastAiQuery$.next(query); + } catch {} + } + + private clearAiContext(): void { + this._lastAiQuery$.next(''); + try { + this.winRef.sessionStorage?.removeItem('cx_ai_context'); + } catch {} + } } diff --git a/core-libs/storefront/cms-components/navigation/search-box/search-box-outlets.model.ts b/core-libs/storefront/cms-components/navigation/search-box/search-box-outlets.model.ts index f04e8087c3b..d9072419a6e 100644 --- a/core-libs/storefront/cms-components/navigation/search-box/search-box-outlets.model.ts +++ b/core-libs/storefront/cms-components/navigation/search-box/search-box-outlets.model.ts @@ -8,4 +8,6 @@ export enum SearchBoxOutlets { RECENT_SEARCHES = 'SearchBoxOutlets.RECENT_SEARCHES', RECENT_SEARCHES_HEADER = 'SearchBoxOutlets.RECENT_SEARCHES_HEADER', TRENDING_SEARCHES = 'SearchBoxOutlets.TRENDING_SEARCHES', + AI_TOGGLE = 'SearchBoxOutlets.AI_TOGGLE', + AI_SEARCH_PANEL = 'SearchBoxOutlets.AI_SEARCH_PANEL', } diff --git a/core-libs/storefront/cms-components/navigation/search-box/search-box.component.html b/core-libs/storefront/cms-components/navigation/search-box/search-box.component.html index 4b15a327c26..ed89e894b4a 100644 --- a/core-libs/storefront/cms-components/navigation/search-box/search-box.component.html +++ b/core-libs/storefront/cms-components/navigation/search-box/search-box.component.html @@ -2,16 +2,17 @@ [attr.aria-label]="'searchBox.productSearch' | cxTranslate" role="search" class="cx-searchbox-container" + [class.cx-searchbox--ai-mode]="isAiSearchEnabled && isAiModeActive" >