Skip to content

Commit cf6e824

Browse files
authored
Add basic search functionality using a mock endpoint (#1681)
* Add basic search functionality using a mock endpoint * Fix eslint errors * Fix test
1 parent 0d10b54 commit cf6e824

File tree

21 files changed

+421
-19
lines changed

21 files changed

+421
-19
lines changed

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAiAnswer.test.tsx renamed to src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiAnswer.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const mockSendQuestion = jest.fn(() => Promise.resolve())
1111
const mockRetry = jest.fn()
1212
const mockAbort = jest.fn()
1313

14-
jest.mock('./search.store', () => ({
14+
jest.mock('../search.store', () => ({
1515
useAskAiTerm: jest.fn(() => 'What is Elasticsearch?'),
1616
}))
1717

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAiAnswer.tsx renamed to src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiAnswer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useAskAiTerm } from './search.store'
1+
import { useAskAiTerm } from '../search.store'
22
import { LlmGatewayMessage, useLlmGateway } from './useLlmGateway'
33
import {
44
EuiFlexGroup,

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAiSuggestions.tsx renamed to src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiSuggestions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useSearchActions, useSearchTerm } from './search.store'
1+
import { useSearchActions, useSearchTerm } from '../search.store'
22
import { EuiButton, EuiSpacer, EuiText, useEuiTheme } from '@elastic/eui'
33
import { css } from '@emotion/react'
44
import * as React from 'react'
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useSearchTerm } from '../search.store'
2+
import { useSearchQuery } from './useSearchQuery'
3+
import {
4+
EuiButton,
5+
EuiLoadingSpinner,
6+
EuiSpacer,
7+
EuiText,
8+
useEuiTheme,
9+
} from '@elastic/eui'
10+
import { css } from '@emotion/react'
11+
import * as React from 'react'
12+
13+
export const SearchResults = () => {
14+
const searchTerm = useSearchTerm()
15+
const { data, error, isLoading } = useSearchQuery()
16+
const { euiTheme } = useEuiTheme()
17+
18+
if (!searchTerm) {
19+
return
20+
}
21+
22+
if (error) {
23+
return <div>Error loading search results: {error.message}</div>
24+
}
25+
26+
if (isLoading) {
27+
return (
28+
<div>
29+
<EuiLoadingSpinner size="s" /> Loading search results...
30+
</div>
31+
)
32+
}
33+
34+
if (!data || data.results.length === 0) {
35+
return <EuiText size="xs">No results found for "{searchTerm}"</EuiText>
36+
}
37+
38+
const buttonCss = css`
39+
border: none;
40+
vertical-align: top;
41+
justify-content: flex-start;
42+
block-size: 100%;
43+
padding-block: 4px;
44+
& > span {
45+
justify-content: flex-start;
46+
align-items: flex-start;
47+
}
48+
svg {
49+
color: ${euiTheme.colors.textSubdued};
50+
}
51+
.euiIcon {
52+
margin-top: 4px;
53+
}
54+
`
55+
56+
const trimDescription = (description: string) => {
57+
const limit = 200
58+
return description.length > limit
59+
? description.slice(0, limit) + '...'
60+
: description
61+
}
62+
63+
return (
64+
<div
65+
css={`
66+
li:not(:first-child) {
67+
margin-top: ${euiTheme.size.xs};
68+
}
69+
`}
70+
>
71+
<EuiText size="xs">Search Results for "{searchTerm}"</EuiText>
72+
<EuiSpacer size="s" />
73+
<ul>
74+
{data.results.map((result) => (
75+
<li key={result.url}>
76+
<EuiButton
77+
css={buttonCss}
78+
iconType="document"
79+
color="text"
80+
size="s"
81+
fullWidth
82+
>
83+
<div
84+
css={css`
85+
width: 100%;
86+
text-align: left;
87+
`}
88+
>
89+
{result.title}
90+
<EuiSpacer size="xs" />
91+
<EuiText
92+
css={css`
93+
text-wrap: pretty;
94+
`}
95+
textAlign="left"
96+
size="xs"
97+
color="subdued"
98+
>
99+
{trimDescription(result.description)}
100+
</EuiText>
101+
</div>
102+
</EuiButton>
103+
{/*<EuiIcon type="document" color="subdued" />*/}
104+
{/*<EuiText>{result.title}</EuiText>*/}
105+
</li>
106+
))}
107+
</ul>
108+
</div>
109+
)
110+
}

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchSuggestions.tsx renamed to src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchSuggestions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useModalActions } from './modal.store'
1+
import { useModalActions } from '../modal.store'
22
import {
33
EuiButton,
44
EuiSpacer,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useSearchTerm } from '../search.store'
2+
import { useQuery } from '@tanstack/react-query'
3+
import { useDebounce } from '@uidotdev/usehooks'
4+
import { z } from 'zod'
5+
6+
const SearchResultItem = z.object({
7+
url: z.string(),
8+
title: z.string(),
9+
description: z.string(),
10+
score: z.number(),
11+
})
12+
13+
const SearchResponse = z.object({
14+
results: z.array(SearchResultItem),
15+
totalResults: z.number(),
16+
})
17+
18+
type SearchResponse = z.infer<typeof SearchResponse>
19+
20+
export const useSearchQuery = () => {
21+
const searchTerm = useSearchTerm()
22+
const trimmedSearchTerm = searchTerm.trim()
23+
const debouncedSearchTerm = useDebounce(trimmedSearchTerm, 300)
24+
return useQuery<SearchResponse>({
25+
queryKey: ['search', { searchTerm: debouncedSearchTerm }],
26+
queryFn: async () => {
27+
const response = await fetch(
28+
'/_api/v1/search?q=' + encodeURIComponent(debouncedSearchTerm)
29+
)
30+
if (!response.ok) {
31+
throw new Error(
32+
'Failed to fetch search results: ' + response.statusText
33+
)
34+
}
35+
const data = await response.json()
36+
return SearchResponse.parse(data)
37+
},
38+
enabled: !!trimmedSearchTerm && trimmedSearchTerm.length >= 1,
39+
refetchOnWindowFocus: false,
40+
staleTime: 1000 * 60 * 10, // 10 minutes
41+
})
42+
}

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { AskAiAnswer } from './AskAiAnswer'
1+
import { AskAiAnswer } from './AskAi/AskAiAnswer'
2+
import { SearchResults } from './Search/SearchResults'
23
import { Suggestions } from './Suggestions'
34
import { useAskAiTerm, useSearchActions, useSearchTerm } from './search.store'
45
import {
@@ -9,15 +10,18 @@ import {
910
EuiHorizontalRule,
1011
} from '@elastic/eui'
1112
import { css } from '@emotion/react'
13+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
1214
import * as React from 'react'
1315

1416
export const SearchOrAskAiModal = () => {
1517
const searchTerm = useSearchTerm()
1618
const askAiTerm = useAskAiTerm()
1719
const { setSearchTerm, submitAskAiTerm } = useSearchActions()
1820

21+
const queryClient = new QueryClient()
22+
1923
return (
20-
<>
24+
<QueryClientProvider client={queryClient}>
2125
<EuiFieldSearch
2226
fullWidth
2327
placeholder="Search the docs or ask Elastic Docs AI Assistant"
@@ -30,6 +34,7 @@ export const SearchOrAskAiModal = () => {
3034
autoFocus={true}
3135
/>
3236
<EuiSpacer size="m" />
37+
<SearchResults />
3338
{askAiTerm ? <AskAiAnswer /> : <Suggestions />}
3439
<EuiHorizontalRule margin="m" />
3540
<div
@@ -54,6 +59,6 @@ export const SearchOrAskAiModal = () => {
5459
This feature is in beta. Got feedback? We'd love to hear it!
5560
</EuiText>
5661
</div>
57-
</>
62+
</QueryClientProvider>
5863
)
5964
}

0 commit comments

Comments
 (0)