1
0
mirror of synced 2025-12-19 18:10:59 -05:00
Files
docs/src/search/components/hooks/useAISearchAutocomplete.ts
Evan Bonsignori b099e4a9e3 Ai search UI (#53026)
Co-authored-by: Kevin Heis <heiskr@users.noreply.github.com>
Co-authored-by: Ashish Keshan <ashishkeshan@github.com>
2025-02-05 19:46:58 +00:00

153 lines
4.7 KiB
TypeScript

import { useState, useRef, useCallback, useEffect } from 'react'
import debounce from 'lodash/debounce'
import { NextRouter } from 'next/router'
import { AutocompleteSearchHit } from '@/search/types'
import { executeAIAutocompleteSearch } from '@/search/components/helpers/execute-search-actions'
type AutocompleteOptions = {
aiAutocompleteOptions: AutocompleteSearchHit[]
}
type UseAutocompleteProps = {
router: NextRouter
currentVersion: string
debug: boolean
eventGroupIdRef: React.MutableRefObject<string>
}
type UseAutocompleteReturn = {
autoCompleteOptions: AutocompleteOptions
searchLoading: boolean
searchError: boolean
updateAutocompleteResults: (query: string) => void
clearAutocompleteResults: () => void
}
const DEBOUNCE_TIME = 300 // In milliseconds
// Results are only cached for the current session
// We cache results so if a user presses backspace, we can show the results immediately without burdening the API
let sessionCache = {} as Record<string, AutocompleteOptions>
// Helpers surrounding the ai-search-autocomplete request to lessen the # of requests made to our API
// There are 3 methods for reducing the # of requests:
// 1. Debouncing the request to prevent multiple requests while the user is typing
// 2. Caching the results of the request so if the user presses backspace, we can show the results immediately without burdening the API
// 3. Aborting in-flight requests if the user types again before the previous request has completed
export function useAISearchAutocomplete({
router,
currentVersion,
debug,
eventGroupIdRef,
}: UseAutocompleteProps): UseAutocompleteReturn {
const [autoCompleteOptions, setAutoCompleteOptions] = useState<AutocompleteOptions>({
aiAutocompleteOptions: [],
})
const [searchLoading, setSearchLoading] = useState<boolean>(true)
const [searchError, setSearchError] = useState<boolean>(false)
// Support for aborting in-flight requests (e.g. user starts typing while a request is still pending)
const abortControllerRef = useRef<AbortController | null>(null)
// Debounce to prevent requests while user is (quickly) typing
const debouncedFetchRef = useRef<ReturnType<typeof debounce> | null>(null)
useEffect(() => {
debouncedFetchRef.current = debounce((value: string) => {
fetchAutocompleteResults(value)
}, DEBOUNCE_TIME) // 300ms debounce
return () => {
debouncedFetchRef.current?.cancel()
}
}, [])
const fetchAutocompleteResults = useCallback(
async (queryValue: string) => {
// Cancel any ongoing request
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
// Check if the result is in cache
if (sessionCache[queryValue]) {
setAutoCompleteOptions(sessionCache[queryValue])
setSearchLoading(false)
return
}
setSearchLoading(true)
// Create a new AbortController for the new request
const controller = new AbortController()
abortControllerRef.current = controller
try {
const { aiAutocompleteOptions } = await executeAIAutocompleteSearch(
router,
currentVersion,
queryValue,
debug,
controller.signal, // Pass in the signal to allow the request to be aborted
eventGroupIdRef.current,
)
const results: AutocompleteOptions = {
aiAutocompleteOptions,
}
// Update cache
sessionCache[queryValue] = results
// Update state with fetched results
setAutoCompleteOptions(results)
setSearchLoading(false)
} catch (error: any) {
if (error.name === 'AbortError') {
return
}
console.error(error)
setSearchError(true)
setSearchLoading(false)
}
},
[router, currentVersion, debug],
)
// Entry function called when the user types in the search input
const updateAutocompleteResults = useCallback((queryValue: string) => {
// When the input is empty, don't debounce the request
// We want to immediately show the autocomplete options (that may be cached)
if (queryValue === '') {
debouncedFetchRef.current?.cancel()
fetchAutocompleteResults('')
return
} else {
debouncedFetchRef.current?.(queryValue)
}
}, [])
const clearAutocompleteResults = useCallback(() => {
setAutoCompleteOptions({
aiAutocompleteOptions: [],
})
setSearchLoading(false)
setSearchError(false)
}, [])
// Cleanup function to cancel any ongoing requests when unmounting
useEffect(() => {
return () => {
abortControllerRef.current?.abort()
}
}, [])
return {
autoCompleteOptions,
searchLoading,
searchError,
updateAutocompleteResults,
clearAutocompleteResults,
}
}