1
0
mirror of synced 2025-12-19 18:10:59 -05:00

improve the error state for AI Search (#55018)

This commit is contained in:
Evan Bonsignori
2025-03-27 09:37:42 -07:00
committed by GitHub
parent 61479a011e
commit 5b99a0312a
5 changed files with 93 additions and 25 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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}`,
) )

View File

@@ -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)
} }
}, },

View File

@@ -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" />)
} }