improve the error state for AI Search (#55018)
This commit is contained in:
@@ -42,6 +42,7 @@ search:
|
|||||||
clear_search_query: Clear
|
clear_search_query: Clear
|
||||||
view_all_search_results: View more results
|
view_all_search_results: View more results
|
||||||
no_results_found: No results found
|
no_results_found: No results found
|
||||||
|
search_docs_with_query: Search docs for "{{query}}"
|
||||||
ai:
|
ai:
|
||||||
disclaimer: Copilot uses AI. Check for mistakes by reviewing the links in the response.
|
disclaimer: Copilot uses AI. Check for mistakes by reviewing the links in the response.
|
||||||
references: References from these articles
|
references: References from these articles
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ search:
|
|||||||
clear_search_query: Clear
|
clear_search_query: Clear
|
||||||
view_all_search_results: View more results
|
view_all_search_results: View more results
|
||||||
no_results_found: No results found
|
no_results_found: No results found
|
||||||
|
search_docs_with_query: Search docs for "{{query}}"
|
||||||
ai:
|
ai:
|
||||||
disclaimer: Copilot uses AI. Check for mistakes by reviewing the links in the response.
|
disclaimer: Copilot uses AI. Check for mistakes by reviewing the links in the response.
|
||||||
references: References from these articles
|
references: References from these articles
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export async function executeCombinedSearch(
|
|||||||
// Allow the caller to pass in an AbortSignal to cancel the request
|
// Allow the caller to pass in an AbortSignal to cancel the request
|
||||||
signal: abortSignal || undefined,
|
signal: abortSignal || undefined,
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response?.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch ai autocomplete search results.\nStatus ${response.status}\n${response.statusText}`,
|
`Failed to fetch ai autocomplete search results.\nStatus ${response.status}\n${response.statusText}`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,6 +89,17 @@ export function useCombinedSearchResults({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there is an existing search error, don't return any results
|
||||||
|
if (searchError) {
|
||||||
|
setSearchOptions({
|
||||||
|
aiAutocompleteOptions: [],
|
||||||
|
generalSearchResults: [],
|
||||||
|
totalGeneralSearchResults: 0,
|
||||||
|
})
|
||||||
|
setSearchLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new AbortController for the new request
|
// Create a new AbortController for the new request
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
abortControllerRef.current = controller
|
abortControllerRef.current = controller
|
||||||
@@ -120,6 +131,11 @@ export function useCombinedSearchResults({
|
|||||||
}
|
}
|
||||||
console.error(error)
|
console.error(error)
|
||||||
setSearchError(true)
|
setSearchError(true)
|
||||||
|
setSearchOptions({
|
||||||
|
aiAutocompleteOptions: [],
|
||||||
|
generalSearchResults: [],
|
||||||
|
totalGeneralSearchResults: 0,
|
||||||
|
})
|
||||||
setSearchLoading(false)
|
setSearchLoading(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ export function SearchOverlay({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: ReturnType<typeof setTimeout>
|
let timer: ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
|
if (autoCompleteSearchError) {
|
||||||
|
return setShowSpinner(false)
|
||||||
|
}
|
||||||
|
|
||||||
// If it's the initial fetch, show the spinner immediately
|
// If it's the initial fetch, show the spinner immediately
|
||||||
if (!aiAutocompleteOptions.length && !generalSearchResults.length) {
|
if (!aiAutocompleteOptions.length && !generalSearchResults.length) {
|
||||||
return setShowSpinner(true)
|
return setShowSpinner(true)
|
||||||
@@ -137,7 +141,12 @@ export function SearchOverlay({
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
}
|
}
|
||||||
}, [searchLoading, aiAutocompleteOptions.length, generalSearchResults.length])
|
}, [
|
||||||
|
searchLoading,
|
||||||
|
aiAutocompleteOptions.length,
|
||||||
|
generalSearchResults.length,
|
||||||
|
autoCompleteSearchError,
|
||||||
|
])
|
||||||
|
|
||||||
// Filter out any options that match the local query and replace them with a custom user query option that include isUserQuery: true
|
// Filter out any options that match the local query and replace them with a custom user query option that include isUserQuery: true
|
||||||
const filteredAIOptions = aiAutocompleteOptions.filter(
|
const filteredAIOptions = aiAutocompleteOptions.filter(
|
||||||
@@ -147,7 +156,14 @@ export function SearchOverlay({
|
|||||||
// Create new arrays that prepend the user input
|
// Create new arrays that prepend the user input
|
||||||
const userInputOptions =
|
const userInputOptions =
|
||||||
urlSearchInputQuery.trim() !== ''
|
urlSearchInputQuery.trim() !== ''
|
||||||
? [{ term: urlSearchInputQuery, highlights: [], isUserQuery: true }]
|
? [
|
||||||
|
{
|
||||||
|
term: urlSearchInputQuery,
|
||||||
|
title: urlSearchInputQuery,
|
||||||
|
highlights: [],
|
||||||
|
isUserQuery: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []
|
: []
|
||||||
|
|
||||||
// Combine options for key navigation
|
// Combine options for key navigation
|
||||||
@@ -165,6 +181,13 @@ export function SearchOverlay({
|
|||||||
title: t('search.overlay.view_all_search_results'),
|
title: t('search.overlay.view_all_search_results'),
|
||||||
isViewAllResults: true,
|
isViewAllResults: true,
|
||||||
} as any)
|
} as any)
|
||||||
|
} else if (autoCompleteSearchError) {
|
||||||
|
if (urlSearchInputQuery.trim() !== '') {
|
||||||
|
generalOptionsWithViewStatus.push({
|
||||||
|
...(userInputOptions[0] || {}),
|
||||||
|
isSearchDocsOption: true,
|
||||||
|
} as unknown as GeneralSearchHit)
|
||||||
|
}
|
||||||
} else if (urlSearchInputQuery.trim() !== '' && !searchLoading) {
|
} else if (urlSearchInputQuery.trim() !== '' && !searchLoading) {
|
||||||
generalOptionsWithViewStatus.push({
|
generalOptionsWithViewStatus.push({
|
||||||
title: t('search.overlay.no_results_found'),
|
title: t('search.overlay.no_results_found'),
|
||||||
@@ -205,6 +228,7 @@ export function SearchOverlay({
|
|||||||
aiSearchError,
|
aiSearchError,
|
||||||
aiReferences,
|
aiReferences,
|
||||||
isAskAIState,
|
isAskAIState,
|
||||||
|
autoCompleteSearchError,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Rather than use `initialFocusRef` to have our Primer <Overlay> component auto-focus our input
|
// Rather than use `initialFocusRef` to have our Primer <Overlay> component auto-focus our input
|
||||||
@@ -432,7 +456,10 @@ export function SearchOverlay({
|
|||||||
) {
|
) {
|
||||||
const selectedItem = combinedOptions[selectedIndex]
|
const selectedItem = combinedOptions[selectedIndex]
|
||||||
if (selectedItem.group === 'general') {
|
if (selectedItem.group === 'general') {
|
||||||
if ((selectedItem.option as GeneralSearchHitWithOptions).isViewAllResults) {
|
if (
|
||||||
|
(selectedItem.option as GeneralSearchHitWithOptions).isViewAllResults ||
|
||||||
|
(selectedItem.option as GeneralSearchHitWithOptions).isSearchDocsOption
|
||||||
|
) {
|
||||||
pressedOnContext = 'view-all'
|
pressedOnContext = 'view-all'
|
||||||
performGeneralSearch()
|
performGeneralSearch()
|
||||||
} else {
|
} else {
|
||||||
@@ -500,7 +527,11 @@ export function SearchOverlay({
|
|||||||
className={styles.suggestionsList}
|
className={styles.suggestionsList}
|
||||||
ref={suggestionsListHeightRef}
|
ref={suggestionsListHeightRef}
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: `${previousSuggestionsListHeight}px`,
|
// When there is an error and nothing is typed in by the user, show an empty list with no height
|
||||||
|
minHeight:
|
||||||
|
autoCompleteSearchError && !generalOptionsWithViewStatus.length
|
||||||
|
? '0'
|
||||||
|
: `${previousSuggestionsListHeight}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Always show the AI Search UI error message when it is needed */}
|
{/* Always show the AI Search UI error message when it is needed */}
|
||||||
@@ -533,27 +564,9 @@ export function SearchOverlay({
|
|||||||
<ActionList.Divider key="error-bottom-divider" />
|
<ActionList.Divider key="error-bottom-divider" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* Only show the autocomplete search UI error message in Dev */}
|
|
||||||
{process.env.NODE_ENV === 'development' && autoCompleteSearchError && !aiSearchError && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
padding: '0 16px 0 16px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Banner
|
|
||||||
tabIndex={0}
|
|
||||||
className={styles.errorBanner}
|
|
||||||
title={t('search.failure.general_title')}
|
|
||||||
description={t('search.failure.description')}
|
|
||||||
variant="info"
|
|
||||||
aria-live="assertive"
|
|
||||||
role="alert"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{renderSearchGroups(
|
{renderSearchGroups(
|
||||||
t,
|
t,
|
||||||
autoCompleteSearchError ? [] : generalOptionsWithViewStatus,
|
generalOptionsWithViewStatus,
|
||||||
aiSearchError ? [] : aiOptionsWithUserInput,
|
aiSearchError ? [] : aiOptionsWithUserInput,
|
||||||
generalSearchResultOnSelect,
|
generalSearchResultOnSelect,
|
||||||
aiSearchOptionOnSelect,
|
aiSearchOptionOnSelect,
|
||||||
@@ -713,6 +726,7 @@ interface AutocompleteSearchHitWithUserQuery extends AutocompleteSearchHit {
|
|||||||
interface GeneralSearchHitWithOptions extends GeneralSearchHit {
|
interface GeneralSearchHitWithOptions extends GeneralSearchHit {
|
||||||
isViewAllResults?: boolean
|
isViewAllResults?: boolean
|
||||||
isNoResultsFound?: boolean
|
isNoResultsFound?: boolean
|
||||||
|
isSearchDocsOption?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the autocomplete suggestions with AI suggestions first, headings, and a divider between the two
|
// Render the autocomplete suggestions with AI suggestions first, headings, and a divider between the two
|
||||||
@@ -824,6 +838,40 @@ function renderSearchGroups(
|
|||||||
)
|
)
|
||||||
// There should be no more items after the no results found item
|
// There should be no more items after the no results found item
|
||||||
break
|
break
|
||||||
|
// This is a special case where there is an error loading search results and we want to be able to search the docs using the user's query
|
||||||
|
} else if (option.isSearchDocsOption) {
|
||||||
|
const isActive = selectedIndex === index
|
||||||
|
items.push(
|
||||||
|
<ActionList.Item
|
||||||
|
key={`general-${index}`}
|
||||||
|
id={`search-option-general-${index}`}
|
||||||
|
role="option"
|
||||||
|
tabIndex={-1}
|
||||||
|
active={isActive}
|
||||||
|
onSelect={() => performGeneralSearch()}
|
||||||
|
aria-label={t('search.overlay.search_docs_with_query').replace('{query}', option.title)}
|
||||||
|
ref={(element) => {
|
||||||
|
if (listElementsRef.current) {
|
||||||
|
listElementsRef.current[index] = element
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionList.LeadingVisual aria-hidden>
|
||||||
|
<SearchIcon />
|
||||||
|
</ActionList.LeadingVisual>
|
||||||
|
{option.title}
|
||||||
|
<ActionList.TrailingVisual
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
// Hold the space even when not visible to prevent layout shift
|
||||||
|
visibility: isActive ? 'visible' : 'hidden',
|
||||||
|
width: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon />
|
||||||
|
</ActionList.TrailingVisual>
|
||||||
|
</ActionList.Item>,
|
||||||
|
)
|
||||||
} else if (option.title) {
|
} else if (option.title) {
|
||||||
const isActive = selectedIndex === index
|
const isActive = selectedIndex === index
|
||||||
items.push(
|
items.push(
|
||||||
@@ -877,13 +925,15 @@ function renderSearchGroups(
|
|||||||
// Don't show the bottom divider if:
|
// Don't show the bottom divider if:
|
||||||
// 1. We are in the AI could not answer state
|
// 1. We are in the AI could not answer state
|
||||||
// 2. We are in the AI Search error state
|
// 2. We are in the AI Search error state
|
||||||
|
// 3. There are no AI suggestions to show in suggestions state
|
||||||
if (
|
if (
|
||||||
!askAIState.aiCouldNotAnswer &&
|
!askAIState.aiCouldNotAnswer &&
|
||||||
!askAIState.aiSearchError &&
|
!askAIState.aiSearchError &&
|
||||||
(!askAIState.isAskAIState ||
|
(!askAIState.isAskAIState ||
|
||||||
generalSearchOptions.filter(
|
generalSearchOptions.filter(
|
||||||
(option) => !option.isViewAllResults && !option.isNoResultsFound,
|
(option) => !option.isViewAllResults && !option.isNoResultsFound,
|
||||||
).length)
|
).length) &&
|
||||||
|
aiOptionsWithUserInput.length
|
||||||
) {
|
) {
|
||||||
groups.push(<ActionList.Divider key="bottom-divider" />)
|
groups.push(<ActionList.Divider key="bottom-divider" />)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user