Skip to content

Commit 5b99a03

Browse files
authored
improve the error state for AI Search (#55018)
1 parent 61479a0 commit 5b99a03

File tree

5 files changed

+93
-25
lines changed

5 files changed

+93
-25
lines changed

data/ui.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ search:
4242
clear_search_query: Clear
4343
view_all_search_results: View more results
4444
no_results_found: No results found
45+
search_docs_with_query: Search docs for "{{query}}"
4546
ai:
4647
disclaimer: Copilot uses AI. Check for mistakes by reviewing the links in the response.
4748
references: References from these articles

src/fixtures/fixtures/data/ui.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ search:
4242
clear_search_query: Clear
4343
view_all_search_results: View more results
4444
no_results_found: No results found
45+
search_docs_with_query: Search docs for "{{query}}"
4546
ai:
4647
disclaimer: Copilot uses AI. Check for mistakes by reviewing the links in the response.
4748
references: References from these articles

src/search/components/helpers/execute-search-actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export async function executeCombinedSearch(
9898
// Allow the caller to pass in an AbortSignal to cancel the request
9999
signal: abortSignal || undefined,
100100
})
101-
if (!response.ok) {
101+
if (!response?.ok) {
102102
throw new Error(
103103
`Failed to fetch ai autocomplete search results.\nStatus ${response.status}\n${response.statusText}`,
104104
)

src/search/components/hooks/useAISearchAutocomplete.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ export function useCombinedSearchResults({
8989
return
9090
}
9191

92+
// If there is an existing search error, don't return any results
93+
if (searchError) {
94+
setSearchOptions({
95+
aiAutocompleteOptions: [],
96+
generalSearchResults: [],
97+
totalGeneralSearchResults: 0,
98+
})
99+
setSearchLoading(false)
100+
return
101+
}
102+
92103
// Create a new AbortController for the new request
93104
const controller = new AbortController()
94105
abortControllerRef.current = controller
@@ -120,6 +131,11 @@ export function useCombinedSearchResults({
120131
}
121132
console.error(error)
122133
setSearchError(true)
134+
setSearchOptions({
135+
aiAutocompleteOptions: [],
136+
generalSearchResults: [],
137+
totalGeneralSearchResults: 0,
138+
})
123139
setSearchLoading(false)
124140
}
125141
},

src/search/components/input/SearchOverlay.tsx

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ export function SearchOverlay({
123123
useEffect(() => {
124124
let timer: ReturnType<typeof setTimeout>
125125

126+
if (autoCompleteSearchError) {
127+
return setShowSpinner(false)
128+
}
129+
126130
// If it's the initial fetch, show the spinner immediately
127131
if (!aiAutocompleteOptions.length && !generalSearchResults.length) {
128132
return setShowSpinner(true)
@@ -137,7 +141,12 @@ export function SearchOverlay({
137141
return () => {
138142
clearTimeout(timer)
139143
}
140-
}, [searchLoading, aiAutocompleteOptions.length, generalSearchResults.length])
144+
}, [
145+
searchLoading,
146+
aiAutocompleteOptions.length,
147+
generalSearchResults.length,
148+
autoCompleteSearchError,
149+
])
141150

142151
// Filter out any options that match the local query and replace them with a custom user query option that include isUserQuery: true
143152
const filteredAIOptions = aiAutocompleteOptions.filter(
@@ -147,7 +156,14 @@ export function SearchOverlay({
147156
// Create new arrays that prepend the user input
148157
const userInputOptions =
149158
urlSearchInputQuery.trim() !== ''
150-
? [{ term: urlSearchInputQuery, highlights: [], isUserQuery: true }]
159+
? [
160+
{
161+
term: urlSearchInputQuery,
162+
title: urlSearchInputQuery,
163+
highlights: [],
164+
isUserQuery: true,
165+
},
166+
]
151167
: []
152168

153169
// Combine options for key navigation
@@ -165,6 +181,13 @@ export function SearchOverlay({
165181
title: t('search.overlay.view_all_search_results'),
166182
isViewAllResults: true,
167183
} as any)
184+
} else if (autoCompleteSearchError) {
185+
if (urlSearchInputQuery.trim() !== '') {
186+
generalOptionsWithViewStatus.push({
187+
...(userInputOptions[0] || {}),
188+
isSearchDocsOption: true,
189+
} as unknown as GeneralSearchHit)
190+
}
168191
} else if (urlSearchInputQuery.trim() !== '' && !searchLoading) {
169192
generalOptionsWithViewStatus.push({
170193
title: t('search.overlay.no_results_found'),
@@ -205,6 +228,7 @@ export function SearchOverlay({
205228
aiSearchError,
206229
aiReferences,
207230
isAskAIState,
231+
autoCompleteSearchError,
208232
])
209233

210234
// Rather than use `initialFocusRef` to have our Primer <Overlay> component auto-focus our input
@@ -432,7 +456,10 @@ export function SearchOverlay({
432456
) {
433457
const selectedItem = combinedOptions[selectedIndex]
434458
if (selectedItem.group === 'general') {
435-
if ((selectedItem.option as GeneralSearchHitWithOptions).isViewAllResults) {
459+
if (
460+
(selectedItem.option as GeneralSearchHitWithOptions).isViewAllResults ||
461+
(selectedItem.option as GeneralSearchHitWithOptions).isSearchDocsOption
462+
) {
436463
pressedOnContext = 'view-all'
437464
performGeneralSearch()
438465
} else {
@@ -500,7 +527,11 @@ export function SearchOverlay({
500527
className={styles.suggestionsList}
501528
ref={suggestionsListHeightRef}
502529
sx={{
503-
minHeight: `${previousSuggestionsListHeight}px`,
530+
// When there is an error and nothing is typed in by the user, show an empty list with no height
531+
minHeight:
532+
autoCompleteSearchError && !generalOptionsWithViewStatus.length
533+
? '0'
534+
: `${previousSuggestionsListHeight}px`,
504535
}}
505536
>
506537
{/* Always show the AI Search UI error message when it is needed */}
@@ -533,27 +564,9 @@ export function SearchOverlay({
533564
<ActionList.Divider key="error-bottom-divider" />
534565
</>
535566
)}
536-
{/* Only show the autocomplete search UI error message in Dev */}
537-
{process.env.NODE_ENV === 'development' && autoCompleteSearchError && !aiSearchError && (
538-
<Box
539-
sx={{
540-
padding: '0 16px 0 16px',
541-
}}
542-
>
543-
<Banner
544-
tabIndex={0}
545-
className={styles.errorBanner}
546-
title={t('search.failure.general_title')}
547-
description={t('search.failure.description')}
548-
variant="info"
549-
aria-live="assertive"
550-
role="alert"
551-
/>
552-
</Box>
553-
)}
554567
{renderSearchGroups(
555568
t,
556-
autoCompleteSearchError ? [] : generalOptionsWithViewStatus,
569+
generalOptionsWithViewStatus,
557570
aiSearchError ? [] : aiOptionsWithUserInput,
558571
generalSearchResultOnSelect,
559572
aiSearchOptionOnSelect,
@@ -713,6 +726,7 @@ interface AutocompleteSearchHitWithUserQuery extends AutocompleteSearchHit {
713726
interface GeneralSearchHitWithOptions extends GeneralSearchHit {
714727
isViewAllResults?: boolean
715728
isNoResultsFound?: boolean
729+
isSearchDocsOption?: boolean
716730
}
717731

718732
// Render the autocomplete suggestions with AI suggestions first, headings, and a divider between the two
@@ -824,6 +838,40 @@ function renderSearchGroups(
824838
)
825839
// There should be no more items after the no results found item
826840
break
841+
// 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
842+
} else if (option.isSearchDocsOption) {
843+
const isActive = selectedIndex === index
844+
items.push(
845+
<ActionList.Item
846+
key={`general-${index}`}
847+
id={`search-option-general-${index}`}
848+
role="option"
849+
tabIndex={-1}
850+
active={isActive}
851+
onSelect={() => performGeneralSearch()}
852+
aria-label={t('search.overlay.search_docs_with_query').replace('{query}', option.title)}
853+
ref={(element) => {
854+
if (listElementsRef.current) {
855+
listElementsRef.current[index] = element
856+
}
857+
}}
858+
>
859+
<ActionList.LeadingVisual aria-hidden>
860+
<SearchIcon />
861+
</ActionList.LeadingVisual>
862+
{option.title}
863+
<ActionList.TrailingVisual
864+
aria-hidden
865+
sx={{
866+
// Hold the space even when not visible to prevent layout shift
867+
visibility: isActive ? 'visible' : 'hidden',
868+
width: '1rem',
869+
}}
870+
>
871+
<ArrowRightIcon />
872+
</ActionList.TrailingVisual>
873+
</ActionList.Item>,
874+
)
827875
} else if (option.title) {
828876
const isActive = selectedIndex === index
829877
items.push(
@@ -877,13 +925,15 @@ function renderSearchGroups(
877925
// Don't show the bottom divider if:
878926
// 1. We are in the AI could not answer state
879927
// 2. We are in the AI Search error state
928+
// 3. There are no AI suggestions to show in suggestions state
880929
if (
881930
!askAIState.aiCouldNotAnswer &&
882931
!askAIState.aiSearchError &&
883932
(!askAIState.isAskAIState ||
884933
generalSearchOptions.filter(
885934
(option) => !option.isViewAllResults && !option.isNoResultsFound,
886-
).length)
935+
).length) &&
936+
aiOptionsWithUserInput.length
887937
) {
888938
groups.push(<ActionList.Divider key="bottom-divider" />)
889939
}

0 commit comments

Comments
 (0)