Skip to content

Commit 180f801

Browse files
authored
Merge pull request #37930 from github/repo-sync
Repo sync
2 parents 1b9148a + b0c439e commit 180f801

File tree

5 files changed

+102
-44
lines changed

5 files changed

+102
-44
lines changed

data/ui.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ search:
4949
references: Additional docs
5050
loading_status_message: Loading Copilot response...
5151
done_loading_status_message: Done loading Copilot response
52-
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different query.
5352
copy_answer: Copy answer
5453
copied_announcement: Copied!
5554
thumbs_up: This answer was helpful
5655
thumbs_down: This answer was not helpful
5756
thumbs_announcement: Thank you for your feedback!
5857
back_to_search: Back to search
58+
responses:
59+
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different question.
60+
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
61+
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
62+
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
5963
failure:
6064
general_title: There was an error loading search results.
6165
ai_title: There was an error loading Copilot.

src/fixtures/fixtures/data/ui.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ search:
4949
references: Additional docs
5050
loading_status_message: Loading Copilot response...
5151
done_loading_status_message: Done loading Copilot response
52-
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different query.
5352
copy_answer: Copy answer
5453
copied_announcement: Copied!
5554
thumbs_up: This answer was helpful
5655
thumbs_down: This answer was not helpful
5756
thumbs_announcement: Thank you for your feedback!
5857
back_to_search: Back to search
58+
responses:
59+
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different question.
60+
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
61+
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
62+
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
5963
failure:
6064
general_title: There was an error loading search results.
6165
ai_title: There was an error loading Copilot.

src/search/components/input/AskAIResults.tsx

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type AIQueryResultsProps = {
3030
askAIEventGroupId: React.MutableRefObject<string>
3131
aiCouldNotAnswer: boolean
3232
setAICouldNotAnswer: (aiCouldNotAnswer: boolean) => void
33+
listElementsRef: React.RefObject<Array<HTMLLIElement | null>>
3334
}
3435

3536
type AISearchResultEventParams = {
@@ -56,6 +57,7 @@ export function AskAIResults({
5657
askAIEventGroupId,
5758
aiCouldNotAnswer,
5859
setAICouldNotAnswer,
60+
listElementsRef,
5961
}: AIQueryResultsProps) {
6062
const router = useRouter()
6163
const { t } = useTranslation('search')
@@ -78,27 +80,30 @@ export function AskAIResults({
7880

7981
const [conversationId, setConversationId] = useState<string>('')
8082

81-
const handleAICannotAnswer = (passedConversationId?: string) => {
83+
const handleAICannotAnswer = (
84+
passedConversationId?: string,
85+
statusCode = 400,
86+
uiMessage = t('search.ai.responses.unable_to_answer'),
87+
) => {
8288
setInitialLoading(false)
8389
setResponseLoading(false)
8490
setAICouldNotAnswer(true)
85-
const cannedResponse = t('search.ai.unable_to_answer')
8691
sendAISearchResultEvent({
8792
sources: [],
88-
message: cannedResponse,
93+
message: uiMessage,
8994
eventGroupId: askAIEventGroupId.current,
9095
couldNotAnswer: true,
91-
status: 400,
96+
status: statusCode,
9297
connectedEventId: passedConversationId || conversationId,
9398
})
94-
setMessage(cannedResponse)
95-
setAnnouncement(cannedResponse)
99+
setMessage(uiMessage)
100+
setAnnouncement(uiMessage)
96101
setReferences([])
97102
setItem(
98103
query,
99104
{
100105
query,
101-
message: cannedResponse,
106+
message: uiMessage,
102107
sources: [],
103108
aiCouldNotAnswer: true,
104109
connectedEventId: passedConversationId || conversationId,
@@ -156,17 +161,44 @@ export function AskAIResults({
156161
try {
157162
const response = await executeAISearch(router, version, query, debug)
158163
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+
}
170202
} else {
171203
setAISearchError(false)
172204
}
@@ -209,7 +241,7 @@ export function AskAIResults({
209241
return
210242
}
211243
} catch (e) {
212-
console.error(
244+
console.warn(
213245
'Failed to parse JSON:',
214246
e,
215247
'Line:',
@@ -226,7 +258,7 @@ export function AskAIResults({
226258
setConversationId(parsedLine.conversation_id)
227259
} else if (parsedLine.chunkType === 'NO_CONTENT_SIGNAL') {
228260
// Serve canned response. A question that cannot be answered was asked
229-
handleAICannotAnswer(conversationIdBuffer)
261+
handleAICannotAnswer(conversationIdBuffer, 200)
230262
} else if (parsedLine.chunkType === 'SOURCES') {
231263
if (!isCancelled) {
232264
sourcesBuffer = sourcesBuffer.concat(parsedLine.sources)
@@ -240,7 +272,11 @@ export function AskAIResults({
240272
}
241273
} else if (parsedLine.chunkType === 'INPUT_CONTENT_FILTER') {
242274
// 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+
)
244280
}
245281
if (!isCancelled) {
246282
setAnnouncement('Copilot Response Loading...')
@@ -396,6 +432,7 @@ export function AskAIResults({
396432
if (index >= MAX_REFERENCES_TO_SHOW) {
397433
return null
398434
}
435+
const refIndex = index + referencesIndexOffset
399436
return (
400437
<ActionList.Item
401438
sx={{
@@ -408,7 +445,12 @@ export function AskAIResults({
408445
onSelect={() => {
409446
referenceOnSelect(source.url)
410447
}}
411-
active={index + referencesIndexOffset === selectedIndex}
448+
active={refIndex === selectedIndex}
449+
ref={(element) => {
450+
if (listElementsRef.current) {
451+
listElementsRef.current[refIndex] = element
452+
}
453+
}}
412454
>
413455
<ActionList.LeadingVisual aria-hidden="true">
414456
<FileIcon />

src/search/components/input/SearchOverlay.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,15 +417,16 @@ export function SearchOverlay({
417417

418418
// Handle keyboard navigation of suggestions
419419
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
420+
let optionsLength = listElementsRef.current?.length ?? 0
420421
if (event.key === 'ArrowDown') {
421422
event.preventDefault()
422-
if (combinedOptions.length > 0) {
423+
if (optionsLength > 0) {
423424
let newIndex = 0
424425
// If no item is selected, select the first item
425426
if (selectedIndex === -1) {
426427
newIndex = 0
427428
} else {
428-
newIndex = (selectedIndex + 1) % combinedOptions.length
429+
newIndex = (selectedIndex + 1) % optionsLength
429430
// If we go "out of bounds" (i.e. the index is less than the selected index), unselect the item
430431
if (newIndex < selectedIndex) {
431432
newIndex = -1
@@ -439,17 +440,23 @@ export function SearchOverlay({
439440
newIndex += 1
440441
}
441442
setSelectedIndex(newIndex)
443+
if (newIndex !== -1 && listElementsRef.current[newIndex]) {
444+
listElementsRef.current[newIndex]?.scrollIntoView({
445+
behavior: 'smooth',
446+
block: 'center',
447+
})
448+
}
442449
}
443450
} else if (event.key === 'ArrowUp') {
444451
event.preventDefault()
445-
if (combinedOptions.length > 0) {
452+
if (optionsLength > 0) {
446453
let newIndex = 0
447454
// If no item is selected, select the last item
448455
if (selectedIndex === -1) {
449-
newIndex = combinedOptions.length - 1
456+
newIndex = optionsLength - 1
450457
} else {
451458
// Otherwise, select the previous item
452-
newIndex = (selectedIndex - 1 + combinedOptions.length) % combinedOptions.length
459+
newIndex = (selectedIndex - 1 + optionsLength) % optionsLength
453460
// If we go "out of bounds" (i.e. the index is greater than the selected index), unselect the item
454461
if (newIndex > selectedIndex) {
455462
newIndex = -1
@@ -464,6 +471,12 @@ export function SearchOverlay({
464471
newIndex -= 1
465472
}
466473
setSelectedIndex(newIndex)
474+
if (newIndex !== -1 && listElementsRef.current[newIndex]) {
475+
listElementsRef.current[newIndex]?.scrollIntoView({
476+
behavior: 'smooth',
477+
block: 'center',
478+
})
479+
}
467480
}
468481
} else if (event.key === 'Enter') {
469482
event.preventDefault()
@@ -877,6 +890,7 @@ function renderSearchGroups(
877890
askAIEventGroupId={askAIState.askAIEventGroupId}
878891
aiCouldNotAnswer={askAIState.aiCouldNotAnswer}
879892
setAICouldNotAnswer={askAIState.setAICouldNotAnswer}
893+
listElementsRef={listElementsRef}
880894
/>
881895
</ActionList.Group>,
882896
)

src/search/lib/ai-search-proxy.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import got from 'got'
44
import { getHmacWithEpoch } from '@/search/lib/helpers/get-cse-copilot-auth'
55
import { getCSECopilotSource } from '@/search/lib/helpers/cse-copilot-docs-versions'
66

7-
const memoryCache = new Map<string, Buffer>()
8-
97
export const aiSearchProxy = async (req: Request, res: Response) => {
108
const { query, version, language } = req.body
119

@@ -43,15 +41,6 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
4341
]
4442
statsd.increment('ai-search.call', 1, diagnosticTags)
4543

46-
// TODO: Caching here may cause an issue if the cache grows too large. Additionally, the cache will be inconsistent across pods
47-
const cacheKey = `${query}:${version}:${language}`
48-
if (memoryCache.has(cacheKey)) {
49-
statsd.increment('ai-search.cache_hit', 1, diagnosticTags)
50-
res.setHeader('Content-Type', 'application/x-ndjson')
51-
res.send(memoryCache.get(cacheKey))
52-
return
53-
}
54-
5544
const startTime = Date.now()
5645
let totalChars = 0
5746

@@ -84,7 +73,10 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
8473
const errorMessage = `Upstream server responded with status code ${upstreamResponse.statusCode}`
8574
console.error(errorMessage)
8675
statsd.increment('ai-search.stream_response_error', 1, diagnosticTags)
87-
res.status(500).json({ errors: [{ message: errorMessage }] })
76+
res.status(upstreamResponse.statusCode).json({
77+
errors: [{ message: errorMessage }],
78+
upstreamStatus: upstreamResponse.statusCode,
79+
})
8880
stream.destroy()
8981
} else {
9082
// Set response headers
@@ -101,9 +93,11 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
10193
console.error('Error streaming from cse-copilot:', error)
10294

10395
if (error?.code === 'ERR_NON_2XX_3XX_RESPONSE') {
104-
return res
105-
.status(400)
106-
.json({ errors: [{ message: 'Sorry I am unable to answer this question.' }] })
96+
const upstreamStatus = error?.response?.statusCode || 500
97+
return res.status(upstreamStatus).json({
98+
errors: [{ message: 'Upstream server error' }],
99+
upstreamStatus,
100+
})
107101
}
108102

109103
statsd.increment('ai-search.stream_error', 1, diagnosticTags)

0 commit comments

Comments
 (0)