Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -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 @@ -331,6 +340,11 @@ const Search = (props) => {
}
}

const onAskAssistantClick = () => {
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
}

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,12 +9,19 @@ 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
Expand Down Expand Up @@ -95,6 +102,9 @@ const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => {
/>
</Fragment>
)}
{showAskAssistantBanner && onAskAssistantClick && (
<AskAssistantBanner onClick={onAskAssistantClick} styles={styles} />
)}
</HideOnDesktop>
{/* Desktop - Vertical and Horizontal alignment */}
<HideOnMobile>
Expand Down Expand Up @@ -189,6 +199,9 @@ const SuggestionSection = ({searchSuggestions, closeAndNavigate, styles}) => {
)}
</Box>
</Box>
{showAskAssistantBanner && onAskAssistantClick && (
<AskAssistantBanner onClick={onAskAssistantClick} styles={styles} />
)}
</HideOnMobile>
</Fragment>
)
Expand All @@ -197,7 +210,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)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import PropTypes from 'prop-types'
import {Stack, useMultiStyleConfig} from '@salesforce/retail-react-app/app/components/shared/ui'
import RecentSearches from '@salesforce/retail-react-app/app/components/search/partials/recent-searches'
import SuggestionSection from '@salesforce/retail-react-app/app/components/search/partials/search-suggestions-section'
import AskAssistantBanner from '@salesforce/retail-react-app/app/components/search/partials/ask-assistant-banner'

const SearchSuggestions = ({recentSearches, searchSuggestions, closeAndNavigate}) => {
const SearchSuggestions = ({
recentSearches,
searchSuggestions,
closeAndNavigate,
enableAgentFromSearchSuggestions,
onAskAssistantClick
}) => {
const styles = useMultiStyleConfig('SearchSuggestions')
const hasCategories = searchSuggestions?.categorySuggestions?.length
const hasProducts = searchSuggestions?.productSuggestions?.length
Expand All @@ -19,6 +26,8 @@ const SearchSuggestions = ({recentSearches, searchSuggestions, closeAndNavigate}
const hasRecentSearches = searchSuggestions?.recentSearchSuggestions?.length
const hasSuggestions =
hasCategories || hasProducts || hasBrands || hasPopularSearches || hasRecentSearches
const showAskAssistantBanner =
enableAgentFromSearchSuggestions === 'true' || enableAgentFromSearchSuggestions === true

return (
<Stack {...styles.container}>
Expand All @@ -27,12 +36,19 @@ const SearchSuggestions = ({recentSearches, searchSuggestions, closeAndNavigate}
searchSuggestions={searchSuggestions}
closeAndNavigate={closeAndNavigate}
styles={styles}
showAskAssistantBanner={showAskAssistantBanner}
onAskAssistantClick={onAskAssistantClick}
/>
) : (
<RecentSearches
recentSearches={recentSearches}
closeAndNavigate={closeAndNavigate}
/>
<>
<RecentSearches
recentSearches={recentSearches}
closeAndNavigate={closeAndNavigate}
/>
{showAskAssistantBanner && onAskAssistantClick && (
<AskAssistantBanner onClick={onAskAssistantClick} styles={styles} />
)}
</>
)}
</Stack>
)
Expand All @@ -41,7 +57,9 @@ const SearchSuggestions = ({recentSearches, searchSuggestions, closeAndNavigate}
SearchSuggestions.propTypes = {
recentSearches: PropTypes.array,
searchSuggestions: PropTypes.object,
closeAndNavigate: PropTypes.func
closeAndNavigate: PropTypes.func,
enableAgentFromSearchSuggestions: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onAskAssistantClick: PropTypes.func
}

export default SearchSuggestions
Loading
Loading