Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ module.exports = {
enableConversationContext: 'false',
conversationContext: [],
enableAgentFromHeader: 'false',
enableAgentFromFloatingButton: 'false'
enableAgentFromFloatingButton: 'false',
enableAgentFromSearchSuggestions: 'false'
},
// Customize how your 'site' and 'locale' are displayed in the url.
url: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ module.exports = {
enableConversationContext: 'false',
conversationContext: [],
enableAgentFromHeader: 'false',
enableAgentFromFloatingButton: 'false'
enableAgentFromFloatingButton: 'false',
enableAgentFromSearchSuggestions: 'false'
},
// Customize settings for your url
url: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useEffect, useMemo, useRef, useState} from 'react'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {useSearchSuggestions} from '@salesforce/commerce-sdk-react'
import {
Input,
Expand Down Expand Up @@ -140,10 +140,19 @@ const Search = (props) => {
const appOrigin = useAppOrigin()
const sfLanguage = normalizeLocaleToSalesforce(locale.id)

const askAgentOnSearchEnabled = useMemo(() => {
const {enabled, askAgentOnSearch} = getCommerceAgentConfig()
return isAskAgentOnSearchEnabled(enabled, askAgentOnSearch)
}, [config.app.commerceAgent])
const commerceAgentConfig = useMemo(
() => getCommerceAgentConfig(),
[config.app?.commerceAgent, config.app?.commerceAgentSettings]
)
const askAgentOnSearchEnabled = useMemo(
() =>
isAskAgentOnSearchEnabled(
commerceAgentConfig?.enabled,
commerceAgentConfig?.askAgentOnSearch
),
[commerceAgentConfig]
)
const enableAgentFromSearchSuggestions = commerceAgentConfig?.enableAgentFromSearchSuggestions

const [isOpen, setIsOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
Expand Down Expand Up @@ -216,7 +225,7 @@ const Search = (props) => {
}

const clearInput = () => {
searchInputRef.current.blur()
searchInputRef.current?.blur()
setIsOpen(false)
}

Expand Down Expand Up @@ -331,6 +340,11 @@ const Search = (props) => {
}
}

const onAskAssistantClick = useCallback(() => {
launchChat()
clearInput()
}, [launchChat, clearInput])

const shouldOpenPopover = () => {
// As per design we only want to show the popover if the input is focused and we have recent searches saved
// or we have search suggestions available and have inputed some text (empty text in this scenario should show recent searches)
Expand Down Expand Up @@ -402,6 +416,8 @@ const Search = (props) => {
closeAndNavigate={closeAndNavigate}
recentSearches={recentSearches}
searchSuggestions={searchSuggestions}
enableAgentFromSearchSuggestions={enableAgentFromSearchSuggestions}
onAskAssistantClick={onAskAssistantClick}
/>
</PopoverContent>
</HideOnMobile>
Expand Down Expand Up @@ -430,6 +446,8 @@ const Search = (props) => {
closeAndNavigate={closeAndNavigate}
recentSearches={recentSearches}
searchSuggestions={searchSuggestions}
enableAgentFromSearchSuggestions={enableAgentFromSearchSuggestions}
onAskAssistantClick={onAskAssistantClick}
/>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import PropTypes from 'prop-types'
import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
import {FormattedMessage} from 'react-intl'
import {SparkleIcon, ChevronRightIcon} from '@salesforce/retail-react-app/app/components/icons'

const AskAssistantBanner = ({onClick, styles}) => {
return (
<Box
{...styles.askAssistantBanner}
as="button"
type="button"
width="full"
textAlign="left"
onClick={onClick}
aria-label="Ask Assistant - Discover, compare and shop smarter with your personal shopping assistant"
>
<Box {...styles.askAssistantBannerContent}>
<Box {...styles.askAssistantBannerIcon} as={SparkleIcon} boxSize={6} />
<Box>
<Text {...styles.askAssistantBannerTitle}>
<FormattedMessage
defaultMessage="Ask Shopping Agent"
id="search.suggestions.askAssistant.title"
/>
</Text>
<Text {...styles.askAssistantBannerDescription}>
<FormattedMessage
defaultMessage="Discover, compare, and shop smarter with your personal Shopping Agent."
id="search.suggestions.askAssistant.description"
/>
</Text>
</Box>
</Box>
<Box {...styles.askAssistantBannerArrow} as={ChevronRightIcon} boxSize={5} />
</Box>
)
}

AskAssistantBanner.propTypes = {
onClick: PropTypes.func.isRequired,
styles: PropTypes.object.isRequired
}

export default AskAssistantBanner
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
import AskAssistantBanner from '@salesforce/retail-react-app/app/components/search/partials/ask-assistant-banner'

const baseStyles = {
askAssistantBanner: {},
askAssistantBannerContent: {},
askAssistantBannerIcon: {},
askAssistantBannerTitle: {},
askAssistantBannerDescription: {},
askAssistantBannerArrow: {}
}

test('renders Ask Assistant banner with title and description', () => {
renderWithProviders(<AskAssistantBanner onClick={jest.fn()} styles={baseStyles} />)

expect(
screen.getByRole('button', {
name: /ask assistant.*discover, compare and shop smarter/i
})
).toBeInTheDocument()
expect(screen.getByText('Ask Shopping Agent')).toBeInTheDocument()
expect(
screen.getByText(/Discover, compare, and shop smarter with your personal Shopping Agent/i)
).toBeInTheDocument()
})

test('calls onClick when banner is clicked', async () => {
const user = userEvent.setup()
const onClick = jest.fn()

renderWithProviders(<AskAssistantBanner onClick={onClick} styles={baseStyles} />)

const button = screen.getByRole('button', {
name: /ask assistant.*discover, compare and shop smarter/i
})
await user.click(button)

expect(onClick).toHaveBeenCalledTimes(1)
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,30 @@ import PropTypes from 'prop-types'
import {Box} from '@salesforce/retail-react-app/app/components/shared/ui'
import Suggestions from '@salesforce/retail-react-app/app/components/search/partials/suggestions'
import HorizontalSuggestions from '@salesforce/retail-react-app/app/components/search/partials/horizontal-suggestions'
import AskAssistantBanner from '@salesforce/retail-react-app/app/components/search/partials/ask-assistant-banner'
import {FormattedMessage} from 'react-intl'
import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
import Link from '@salesforce/retail-react-app/app/components/link'
import {searchUrlBuilder} from '@salesforce/retail-react-app/app/utils/url'

const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => {
const SuggestionSection = ({
searchSuggestions,
closeAndNavigate,
styles,
showAskAssistantBanner,
onAskAssistantClick
}) => {
const hasCategories = searchSuggestions?.categorySuggestions?.length
const hasProducts = searchSuggestions?.productSuggestions?.length
const hasPhraseSuggestions = searchSuggestions?.phraseSuggestions?.length
const hasPopularSearches = searchSuggestions?.popularSearchSuggestions?.length
const hasRecentSearches = searchSuggestions?.recentSearchSuggestions?.length

const askAssistantBanner =
showAskAssistantBanner && onAskAssistantClick ? (
<AskAssistantBanner onClick={onAskAssistantClick} styles={styles} />
) : null

return (
<Fragment>
{/* Mobile - Vertical alignment */}
Expand Down Expand Up @@ -95,6 +107,7 @@ const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => {
/>
</Fragment>
)}
{askAssistantBanner}
</HideOnDesktop>
{/* Desktop - Vertical and Horizontal alignment */}
<HideOnMobile>
Expand Down Expand Up @@ -189,6 +202,7 @@ const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => {
)}
</Box>
</Box>
{askAssistantBanner}
</HideOnMobile>
</Fragment>
)
Expand All @@ -197,7 +211,9 @@ const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => {
SuggestionSection.propTypes = {
searchSuggestions: PropTypes.object.isRequired,
closeAndNavigate: PropTypes.func.isRequired,
styles: PropTypes.object.isRequired
styles: PropTypes.object.isRequired,
showAskAssistantBanner: PropTypes.bool,
onAskAssistantClick: PropTypes.func
}

export default SuggestionSection
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ jest.mock('@salesforce/retail-react-app/app/components/dynamic-image', () => {
const baseStyles = {
textContainer: {},
sectionHeader: {},
phraseContainer: {}
phraseContainer: {},
askAssistantBanner: {},
askAssistantBannerContent: {},
askAssistantBannerIcon: {},
askAssistantBannerTitle: {},
askAssistantBannerDescription: {},
askAssistantBannerArrow: {}
}

const makeSearchSuggestions = (overrides = {}) => ({
Expand Down Expand Up @@ -137,3 +143,94 @@ test('renders nothing when there are no categories, products, or phrase suggesti
expect(screen.queryByText('Products')).not.toBeInTheDocument()
expect(screen.queryByTestId('sf-horizontal-product-suggestions')).not.toBeInTheDocument()
})

describe('Ask Assistant banner', () => {
test('renders Ask Assistant banner when showAskAssistantBanner and onAskAssistantClick are provided', () => {
const searchSuggestions = makeSearchSuggestions({
categorySuggestions: [{type: 'category', name: 'Women', link: '/women'}]
})

renderWithProviders(
<SuggestionSection
searchSuggestions={searchSuggestions}
closeAndNavigate={jest.fn()}
styles={baseStyles}
showAskAssistantBanner={true}
onAskAssistantClick={jest.fn()}
/>
)

const banners = screen.getAllByRole('button', {
name: /ask assistant.*discover, compare and shop smarter/i
})
expect(banners.length).toBeGreaterThanOrEqual(1)
})

test('does not render Ask Assistant banner when showAskAssistantBanner is false', () => {
const searchSuggestions = makeSearchSuggestions({
categorySuggestions: [{type: 'category', name: 'Women', link: '/women'}]
})

renderWithProviders(
<SuggestionSection
searchSuggestions={searchSuggestions}
closeAndNavigate={jest.fn()}
styles={baseStyles}
showAskAssistantBanner={false}
onAskAssistantClick={jest.fn()}
/>
)

expect(
screen.queryByRole('button', {
name: /ask assistant.*discover, compare and shop smarter/i
})
).not.toBeInTheDocument()
})

test('does not render Ask Assistant banner when onAskAssistantClick is not provided', () => {
const searchSuggestions = makeSearchSuggestions({
categorySuggestions: [{type: 'category', name: 'Women', link: '/women'}]
})

renderWithProviders(
<SuggestionSection
searchSuggestions={searchSuggestions}
closeAndNavigate={jest.fn()}
styles={baseStyles}
showAskAssistantBanner={true}
/>
)

expect(
screen.queryByRole('button', {
name: /ask assistant.*discover, compare and shop smarter/i
})
).not.toBeInTheDocument()
})

test('clicking Ask Assistant banner calls onAskAssistantClick', async () => {
const user = userEvent.setup()
const onAskAssistantClick = jest.fn()
const searchSuggestions = makeSearchSuggestions({
recentSearchSuggestions: [{type: 'recent', name: 'shoes', link: '/search?q=shoes'}]
})

renderWithProviders(
<SuggestionSection
searchSuggestions={searchSuggestions}
closeAndNavigate={jest.fn()}
styles={baseStyles}
showAskAssistantBanner={true}
onAskAssistantClick={onAskAssistantClick}
/>
)

const banner = screen.getAllByRole('button', {
name: /ask assistant.*discover, compare and shop smarter/i
})[0]
await user.click(banner)

expect(onAskAssistantClick).toHaveBeenCalledTimes(1)
})
})
Loading
Loading