| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- 'use client'
- import { compact, debounce, uniqBy } from 'lodash'
- import { useCallback, useMemo, useReducer, useRef } from 'react'
- import { isFeatureEnabled } from '../enabled-features'
- const NUMBER_SOURCES = 2
- const BRIVEN_URL = process.env.NEXT_PUBLIC_BRIVEN_URL
- const BRIVEN_ANON_KEY = process.env.NEXT_PUBLIC_BRIVEN_ANON_KEY
- const FUNCTIONS_URL = '/functions/v1/'
- enum PageType {
- Markdown = 'markdown',
- Reference = 'reference',
- Integration = 'partner-integration',
- GithubDiscussion = 'github-discussions',
- Troubleshooting = 'troubleshooting',
- }
- interface PageSection {
- heading: string
- slug: string
- }
- interface Page {
- id: number
- path: string
- type: PageType
- title: string
- subtitle: string | null
- description: string | null
- sections: PageSection[]
- }
- type SearchState =
- | {
- status: 'initial'
- key: number
- }
- | {
- status: 'loading'
- key: number
- staleResults: Page[]
- }
- | {
- status: 'partialResults'
- key: number
- results: Page[]
- }
- | {
- status: 'fullResults'
- key: number
- results: Page[]
- }
- | {
- status: 'noResults'
- key: number
- }
- | {
- status: 'error'
- key: number
- message: string
- }
- type Action =
- | {
- type: 'resultsReturned'
- key: number
- sourcesLoaded: number
- results: unknown[]
- }
- | {
- type: 'newSearchDispatched'
- key: number
- }
- | {
- type: 'reset'
- key: number
- }
- | {
- type: 'errored'
- key: number
- sourcesLoaded: number
- message: string
- }
- function reshapeResults(result: unknown): Page | null {
- if (typeof result !== 'object' || result === null) {
- return null
- }
- if (!('id' in result && 'path' in result && 'type' in result && 'title' in result)) {
- return null
- }
- const sections: PageSection[] = []
- if (
- 'headings' in result &&
- Array.isArray(result.headings) &&
- 'slugs' in result &&
- Array.isArray(result.slugs) &&
- result.headings.length === result.slugs.length
- ) {
- result.headings.forEach((heading, idx) => {
- const slug = (result.slugs as Array<string>)[idx]
- if (heading && slug) {
- sections.push({ heading, slug })
- }
- })
- }
- return {
- id: result.id as number,
- path: result.path as string,
- type: result.type as PageType,
- title: result.title as string,
- subtitle: 'subtitle' in result ? (result.subtitle as string) : null,
- description: 'description' in result ? (result.description as string) : null,
- sections,
- }
- }
- function reducer(state: SearchState, action: Action): SearchState {
- // Ignore responses from outdated async functions
- if (state.key > action.key) {
- return state
- }
- switch (action.type) {
- case 'resultsReturned':
- const allSourcesLoaded = action.sourcesLoaded === NUMBER_SOURCES
- const newResults = compact(action.results.map(reshapeResults))
- // If the new responses are from the same request as the current responses,
- // combine the responses.
- // If the new responses are from a fresher request, replace the current responses.
- const allResults =
- state.status === 'partialResults' && state.key === action.key
- ? uniqBy(state.results.concat(newResults), (res) => res.id)
- : newResults
- if (!allResults.length) {
- return allSourcesLoaded
- ? {
- status: 'noResults',
- key: action.key,
- }
- : {
- status: 'loading',
- key: action.key,
- staleResults:
- 'results' in state
- ? state.results
- : 'staleResults' in state
- ? state.staleResults
- : [],
- }
- }
- return allSourcesLoaded
- ? {
- status: 'fullResults',
- key: action.key,
- results: allResults,
- }
- : {
- status: 'partialResults',
- key: action.key,
- results: allResults,
- }
- case 'newSearchDispatched':
- return {
- status: 'loading',
- key: action.key,
- staleResults:
- 'results' in state ? state.results : 'staleResults' in state ? state.staleResults : [],
- }
- case 'reset':
- return {
- status: 'initial',
- key: action.key,
- }
- case 'errored':
- // At least one search has failed and all non-failing searches have come back empty
- if (action.sourcesLoaded === NUMBER_SOURCES && !('results' in state)) {
- return {
- status: 'error',
- key: action.key,
- message: action.message,
- }
- }
- return state
- default:
- return state
- }
- }
- const useDocsSearch = () => {
- const [state, dispatch] = useReducer(reducer, { status: 'initial', key: 0 })
- const key = useRef(0)
- const handleSearch = useCallback(async (query: string) => {
- key.current += 1
- const localKey = key.current
- dispatch({ type: 'newSearchDispatched', key: localKey })
- let sourcesLoaded = 0
- const useAlternateSearchIndex = !isFeatureEnabled('search:fullIndex')
- const searchEndpoint = useAlternateSearchIndex ? 'docs_search_fts_nimbus' : 'docs_search_fts'
- fetch(`${BRIVEN_URL}/rest/v1/rpc/${searchEndpoint}`, {
- method: 'POST',
- headers: {
- 'content-type': 'application/json',
- ...(BRIVEN_ANON_KEY && {
- apikey: BRIVEN_ANON_KEY,
- authorization: `Bearer ${BRIVEN_ANON_KEY}`,
- }),
- },
- body: JSON.stringify({ query: query.trim() }),
- })
- .then((res) => res.json())
- .then((data) => {
- sourcesLoaded += 1
- if (!Array.isArray(data)) {
- dispatch({
- type: 'errored',
- key: localKey,
- sourcesLoaded,
- message: data?.message ?? '',
- })
- } else {
- dispatch({
- type: 'resultsReturned',
- key: localKey,
- sourcesLoaded,
- results: data,
- })
- }
- })
- .catch((error: unknown) => {
- sourcesLoaded += 1
- console.error(`[ERROR] Error fetching Full Text Search results: ${error}`)
- dispatch({
- type: 'errored',
- key: localKey,
- sourcesLoaded,
- message: '',
- })
- })
- fetch(`${BRIVEN_URL}${FUNCTIONS_URL}search-embeddings`, {
- method: 'POST',
- body: JSON.stringify({ query, useAlternateSearchIndex }),
- })
- .then((response) => response.json())
- .then((results) => {
- if (!Array.isArray(results)) {
- throw Error("didn't get expected results array")
- }
- sourcesLoaded += 1
- dispatch({
- type: 'resultsReturned',
- key: localKey,
- sourcesLoaded,
- results,
- })
- })
- .catch((error) => {
- sourcesLoaded += 1
- dispatch({
- type: 'errored',
- key: localKey,
- sourcesLoaded,
- message: error.message ?? '',
- })
- })
- }, [])
- const debouncedSearch = useMemo(() => debounce(handleSearch, 150), [handleSearch])
- const resetSearch = useCallback(() => {
- key.current += 1
- dispatch({
- type: 'reset',
- key: key.current,
- })
- }, [])
- return {
- searchState: state,
- handleDocsSearch: handleSearch,
- handleDocsSearchDebounced: debouncedSearch,
- resetSearch,
- }
- }
- export { useDocsSearch, PageType as DocsSearchResultType }
- export type { Page as DocsSearchResult, PageSection as DocsSearchResultSection }
|