@@ -30,6 +30,7 @@ type AIQueryResultsProps = {
30
30
askAIEventGroupId : React . MutableRefObject < string >
31
31
aiCouldNotAnswer : boolean
32
32
setAICouldNotAnswer : ( aiCouldNotAnswer : boolean ) => void
33
+ listElementsRef : React . RefObject < Array < HTMLLIElement | null > >
33
34
}
34
35
35
36
type AISearchResultEventParams = {
@@ -56,6 +57,7 @@ export function AskAIResults({
56
57
askAIEventGroupId,
57
58
aiCouldNotAnswer,
58
59
setAICouldNotAnswer,
60
+ listElementsRef,
59
61
} : AIQueryResultsProps ) {
60
62
const router = useRouter ( )
61
63
const { t } = useTranslation ( 'search' )
@@ -78,27 +80,30 @@ export function AskAIResults({
78
80
79
81
const [ conversationId , setConversationId ] = useState < string > ( '' )
80
82
81
- const handleAICannotAnswer = ( passedConversationId ?: string ) => {
83
+ const handleAICannotAnswer = (
84
+ passedConversationId ?: string ,
85
+ statusCode = 400 ,
86
+ uiMessage = t ( 'search.ai.responses.unable_to_answer' ) ,
87
+ ) => {
82
88
setInitialLoading ( false )
83
89
setResponseLoading ( false )
84
90
setAICouldNotAnswer ( true )
85
- const cannedResponse = t ( 'search.ai.unable_to_answer' )
86
91
sendAISearchResultEvent ( {
87
92
sources : [ ] ,
88
- message : cannedResponse ,
93
+ message : uiMessage ,
89
94
eventGroupId : askAIEventGroupId . current ,
90
95
couldNotAnswer : true ,
91
- status : 400 ,
96
+ status : statusCode ,
92
97
connectedEventId : passedConversationId || conversationId ,
93
98
} )
94
- setMessage ( cannedResponse )
95
- setAnnouncement ( cannedResponse )
99
+ setMessage ( uiMessage )
100
+ setAnnouncement ( uiMessage )
96
101
setReferences ( [ ] )
97
102
setItem (
98
103
query ,
99
104
{
100
105
query,
101
- message : cannedResponse ,
106
+ message : uiMessage ,
102
107
sources : [ ] ,
103
108
aiCouldNotAnswer : true ,
104
109
connectedEventId : passedConversationId || conversationId ,
@@ -156,17 +161,44 @@ export function AskAIResults({
156
161
try {
157
162
const response = await executeAISearch ( router , version , query , debug )
158
163
if ( ! response . ok ) {
159
- console . error (
160
- `Failed to fetch search results.\nStatus ${ response . status } \n${ response . statusText } ` ,
161
- )
162
- sendAISearchResultEvent ( {
163
- sources : [ ] ,
164
- message : '' ,
165
- eventGroupId : askAIEventGroupId . current ,
166
- couldNotAnswer : false ,
167
- status : response . status ,
168
- } )
169
- return setAISearchError ( )
164
+ // If there is JSON and the `upstreamStatus` key, the error is from the upstream sever (CSE)
165
+ let responseJson
166
+ try {
167
+ responseJson = await response . json ( )
168
+ } catch ( error ) {
169
+ console . error ( 'Failed to parse JSON:' , error )
170
+ }
171
+ const upstreamStatus = responseJson ?. upstreamStatus
172
+ // If there is no upstream status, the error is either on our end or a 500 from CSE, so we can show the error
173
+ if ( ! upstreamStatus ) {
174
+ console . error (
175
+ `Failed to fetch search results.\nStatus ${ response . status } \n${ response . statusText } ` ,
176
+ )
177
+ sendAISearchResultEvent ( {
178
+ sources : [ ] ,
179
+ message : '' ,
180
+ eventGroupId : askAIEventGroupId . current ,
181
+ couldNotAnswer : false ,
182
+ status : response . status ,
183
+ } )
184
+ return setAISearchError ( )
185
+ // Query invalid - either sensitive question or spam
186
+ } else if ( upstreamStatus === 400 || upstreamStatus === 422 ) {
187
+ return handleAICannotAnswer ( '' , upstreamStatus , t ( 'search.ai.responses.invalid_query' ) )
188
+ // Query too large
189
+ } else if ( upstreamStatus === 413 ) {
190
+ return handleAICannotAnswer (
191
+ '' ,
192
+ upstreamStatus ,
193
+ t ( 'search.ai.responses.query_too_large' ) ,
194
+ )
195
+ } else if ( upstreamStatus === 429 ) {
196
+ return handleAICannotAnswer (
197
+ '' ,
198
+ upstreamStatus ,
199
+ t ( 'search.ai.responses.asked_too_many_times' ) ,
200
+ )
201
+ }
170
202
} else {
171
203
setAISearchError ( false )
172
204
}
@@ -209,7 +241,7 @@ export function AskAIResults({
209
241
return
210
242
}
211
243
} catch ( e ) {
212
- console . error (
244
+ console . warn (
213
245
'Failed to parse JSON:' ,
214
246
e ,
215
247
'Line:' ,
@@ -226,7 +258,7 @@ export function AskAIResults({
226
258
setConversationId ( parsedLine . conversation_id )
227
259
} else if ( parsedLine . chunkType === 'NO_CONTENT_SIGNAL' ) {
228
260
// Serve canned response. A question that cannot be answered was asked
229
- handleAICannotAnswer ( conversationIdBuffer )
261
+ handleAICannotAnswer ( conversationIdBuffer , 200 )
230
262
} else if ( parsedLine . chunkType === 'SOURCES' ) {
231
263
if ( ! isCancelled ) {
232
264
sourcesBuffer = sourcesBuffer . concat ( parsedLine . sources )
@@ -240,7 +272,11 @@ export function AskAIResults({
240
272
}
241
273
} else if ( parsedLine . chunkType === 'INPUT_CONTENT_FILTER' ) {
242
274
// Serve canned response. A spam question was asked
243
- handleAICannotAnswer ( conversationIdBuffer )
275
+ handleAICannotAnswer (
276
+ conversationIdBuffer ,
277
+ 200 ,
278
+ t ( 'search.ai.responses.invalid_query' ) ,
279
+ )
244
280
}
245
281
if ( ! isCancelled ) {
246
282
setAnnouncement ( 'Copilot Response Loading...' )
@@ -396,6 +432,7 @@ export function AskAIResults({
396
432
if ( index >= MAX_REFERENCES_TO_SHOW ) {
397
433
return null
398
434
}
435
+ const refIndex = index + referencesIndexOffset
399
436
return (
400
437
< ActionList . Item
401
438
sx = { {
@@ -408,7 +445,12 @@ export function AskAIResults({
408
445
onSelect = { ( ) => {
409
446
referenceOnSelect ( source . url )
410
447
} }
411
- active = { index + referencesIndexOffset === selectedIndex }
448
+ active = { refIndex === selectedIndex }
449
+ ref = { ( element ) => {
450
+ if ( listElementsRef . current ) {
451
+ listElementsRef . current [ refIndex ] = element
452
+ }
453
+ } }
412
454
>
413
455
< ActionList . LeadingVisual aria-hidden = "true" >
414
456
< FileIcon />
0 commit comments