diff --git a/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts b/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts
index df60b9ae9..4e5a6f474 100644
--- a/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts
+++ b/src/Elastic.Documentation.Site/Assets/eui-icons-cache.ts
@@ -6,7 +6,9 @@ import { icon as EuiIconArrowStart } from '@elastic/eui/es/components/icon/asset
import { icon as EuiIconArrowDown } from '@elastic/eui/es/components/icon/assets/arrow_down'
import { icon as EuiIconArrowLeft } from '@elastic/eui/es/components/icon/assets/arrow_left'
import { icon as EuiIconArrowRight } from '@elastic/eui/es/components/icon/assets/arrow_right'
+import { icon as EuiIconArrowUp } from '@elastic/eui/es/components/icon/assets/arrow_up'
import { icon as EuiIconCheck } from '@elastic/eui/es/components/icon/assets/check'
+import { icon as EuiIconCode } from '@elastic/eui/es/components/icon/assets/code'
import { icon as EuiIconComment } from '@elastic/eui/es/components/icon/assets/comment'
import { icon as EuiIconCopy } from '@elastic/eui/es/components/icon/assets/copy'
import { icon as EuiIconCopyClipboard } from '@elastic/eui/es/components/icon/assets/copy_clipboard'
@@ -24,6 +26,7 @@ import { icon as EuiIconPopout } from '@elastic/eui/es/components/icon/assets/po
import { icon as EuiIconRefresh } from '@elastic/eui/es/components/icon/assets/refresh'
import { icon as EuiIconReturnKey } from '@elastic/eui/es/components/icon/assets/return_key'
import { icon as EuiIconSearch } from '@elastic/eui/es/components/icon/assets/search'
+import { icon as EuiIconSortDown } from '@elastic/eui/es/components/icon/assets/sort_down'
import { icon as EuiIconSortUp } from '@elastic/eui/es/components/icon/assets/sort_up'
import { icon as EuiIconSparkles } from '@elastic/eui/es/components/icon/assets/sparkles'
import { icon as EuiIconThumbDown } from '@elastic/eui/es/components/icon/assets/thumbDown'
@@ -33,11 +36,13 @@ import { icon as EuiIconUser } from '@elastic/eui/es/components/icon/assets/user
import { icon as EuiIconWrench } from '@elastic/eui/es/components/icon/assets/wrench'
import { appendIconComponentCache } from '@elastic/eui/es/components/icon/icon'
-appendIconComponentCache({
+const iconMapping = {
newChat: EuiIconNewChat,
+ arrowUp: EuiIconArrowUp,
arrowDown: EuiIconArrowDown,
arrowLeft: EuiIconArrowLeft,
arrowRight: EuiIconArrowRight,
+ code: EuiIconCode,
document: EuiIconDocument,
dot: EuiIconDot,
empty: EuiIconEmpty,
@@ -62,7 +67,12 @@ appendIconComponentCache({
copy: EuiIconCopy,
play: EuiIconPlay,
sortUp: EuiIconSortUp,
+ sortDown: EuiIconSortDown,
arrowStart: EuiIconArrowStart,
arrowEnd: EuiIconArrowEnd,
comment: EuiIconComment,
-})
+}
+
+appendIconComponentCache(iconMapping)
+
+export const availableIcons = Object.keys(iconMapping)
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx
index 0d742cade..b8f306c0b 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/Chat.tsx
@@ -15,6 +15,7 @@ import {
EuiEmptyPrompt,
EuiSpacer,
EuiTitle,
+ useEuiTheme,
} from '@elastic/eui'
import { css } from '@emotion/react'
import { useCallback, useEffect, useRef, useState } from 'react'
@@ -42,7 +43,6 @@ const messagesStyles = css`
margin: 0 auto;
`
-// Small helper for scroll behavior
const scrollToBottom = (container: HTMLDivElement | null) => {
if (!container) return
container.scrollTop = container.scrollHeight
@@ -74,6 +74,7 @@ const NewConversationHeader = ({
)
export const Chat = () => {
+ const { euiTheme } = useEuiTheme()
const messages = useChatMessages()
const { submitQuestion, clearChat, clearNon429Errors, cancelStreaming } =
useChatActions()
@@ -89,18 +90,15 @@ export const Chat = () => {
${useEuiOverflowScroll('y', true)}
`
- // Check if there's an active streaming query
const isStreaming =
messages.length > 0 &&
messages[messages.length - 1].type === 'ai' &&
messages[messages.length - 1].status === 'streaming'
- // Handle abort function from StreamingAiMessage
- const handleAbortReady = (abort: () => void) => {
+ const handleAbortReady = useCallback((abort: () => void) => {
abortFunctionRef.current = abort
- }
+ }, [])
- // Clear abort function when streaming ends
useEffect(() => {
if (!isStreaming) {
abortFunctionRef.current = null
@@ -109,16 +107,13 @@ export const Chat = () => {
const handleSubmit = useCallback(
(question: string) => {
- if (!question.trim()) return
-
- // Prevent submission during countdown
- if (isCooldownActive) {
+ const trimmedQuestion = question.trim()
+ if (!trimmedQuestion || isCooldownActive) {
return
}
clearNon429Errors()
-
- submitQuestion(question.trim())
+ submitQuestion(trimmedQuestion)
if (inputRef.current) {
inputRef.current.value = ''
@@ -245,6 +240,7 @@ export const Chat = () => {
{
}
css={css`
position: absolute;
- right: 8px;
+ right: ${euiTheme.size.l};
top: 50%;
transform: translateY(-50%);
border-radius: 9999px;
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/highlight-worker.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/highlight-worker.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/InfoBanner.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/InfoBanner.tsx
new file mode 100644
index 000000000..a8c37bc0d
--- /dev/null
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/InfoBanner.tsx
@@ -0,0 +1,49 @@
+import {
+ EuiBetaBadge,
+ EuiHorizontalRule,
+ EuiLink,
+ EuiText,
+ useEuiTheme,
+} from '@elastic/eui'
+import { css } from '@emotion/react'
+
+export const InfoBanner = () => {
+ const { euiTheme } = useEuiTheme()
+ return (
+
+
+
+
+
+
+ This feature is in private preview.{' '}
+
+ Got feedback? We'd love to hear it!
+
+
+
+
+ )
+}
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.test.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.test.tsx
index 7d7208e43..21010931b 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.test.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.test.tsx
@@ -642,8 +642,12 @@ describe('Search Component', () => {
// Act
render()
- // Assert
- expect(screen.getByRole('progressbar')).toBeInTheDocument()
+ // Assert - check that the loading spinner exists (it has aria-label="Loading" without trailing space)
+ const progressbars = screen.getAllByRole('progressbar')
+ const spinner = progressbars.find(
+ (el) => el.getAttribute('aria-label') === 'Loading'
+ )
+ expect(spinner).toBeInTheDocument()
})
it('should show loading spinner when isFetching is true', () => {
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.tsx
index 85a8f22da..9367cd23e 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/Search.tsx
@@ -1,5 +1,7 @@
+import { availableIcons } from '../../../eui-icons-cache'
import { useChatActions } from '../AskAi/chat.store'
import { useIsAskAiCooldownActive } from '../AskAi/useAskAiCooldown'
+import { InfoBanner } from '../InfoBanner'
import { SearchOrAskAiErrorCallout } from '../SearchOrAskAiErrorCallout'
import { useModalActions } from '../modal.store'
import { SearchResults } from './SearchResults/SearchResults'
@@ -12,6 +14,7 @@ import {
EuiFieldText,
EuiSpacer,
EuiButton,
+ EuiHorizontalRule,
EuiIcon,
EuiLoadingSpinner,
EuiText,
@@ -19,7 +22,7 @@ import {
useEuiFontSize,
} from '@elastic/eui'
import { css } from '@emotion/react'
-import { useState } from 'react'
+import React, { useState } from 'react'
export const Search = () => {
const searchTerm = useSearchTerm()
@@ -31,22 +34,30 @@ export const Search = () => {
const [isInputFocused, setIsInputFocused] = useState(false)
const { isLoading, isFetching } = useSearchQuery()
const xsFontSize = useEuiFontSize('xs').fontSize
+ const mFontSize = useEuiFontSize('m').fontSize
const { euiTheme } = useEuiTheme()
- const handleSearch = (e: React.ChangeEvent) => {
+ const handleSearchInputChange = (
+ e: React.ChangeEvent
+ ) => {
setSearchTerm(e.target.value)
}
- const handleAskAi = () => {
- if (isAskAiCooldownActive || searchTerm.trim() === '') {
+ const handleAskAiClick = () => {
+ const trimmedSearchTerm = searchTerm.trim()
+ if (isAskAiCooldownActive || trimmedSearchTerm === '') {
return
}
- // Always start a new conversation
clearChat()
- submitQuestion(searchTerm)
+ submitQuestion(trimmedSearchTerm)
setModalMode('askAi')
}
+ const handleCloseModal = () => {
+ clearSearchTerm()
+ closeModal()
+ }
+
const {
inputRef,
buttonRef,
@@ -54,64 +65,49 @@ export const Search = () => {
handleListItemKeyDown,
focusLastAvailable,
setItemRef,
- } = useKeyboardNavigation(handleAskAi)
+ } = useKeyboardNavigation(handleAskAiClick)
return (
<>
-
{!searchTerm.trim() && (
)}
+
+ {isLoading || isFetching ? (
+
+ ) : (
+
+ )}
setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onKeyDown={handleInputKeyDown}
disabled={isSearchCooldownActive}
/>
- {isLoading || isFetching ? (
-
-
-
- ) : (
-
- )}
{
size="s"
color="text"
minWidth={false}
- onClick={() => {
- clearSearchTerm()
- closeModal()
- }}
+ onClick={handleCloseModal}
>
Esc
+
{searchTerm && (
- <>
+
Ask AI assistant
@@ -143,11 +141,120 @@ export const Search = () => {
ref={buttonRef}
term={searchTerm}
isInputFocused={isInputFocused}
- onAsk={handleAskAi}
+ onAsk={handleAskAiClick}
onArrowUp={focusLastAvailable}
/>
- >
+
)}
+
+
+
>
)
}
+
+const SearchFooter = () => {
+ const { euiTheme } = useEuiTheme()
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+interface KeyboardIconProps {
+ type: string
+}
+
+const KeyboardKey = ({ children }: { children: React.ReactNode }) => {
+ const { euiTheme } = useEuiTheme()
+ return (
+
+ {children}
+
+ )
+}
+
+const KeyboardIcon = ({ type }: KeyboardIconProps) => {
+ const { euiTheme } = useEuiTheme()
+ return (
+
+ {availableIcons.includes(type) ? (
+
+ ) : (
+
+ {type}
+
+ )}
+
+ )
+}
+
+const KeyboardIconsWithLabel = ({
+ types,
+ label,
+}: {
+ types: string[]
+ label: string
+}) => {
+ const { euiTheme } = useEuiTheme()
+ return (
+
+
+ {types.map((type, index) => (
+
+ ))}
+
+ {label}
+
+ )
+}
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx
index 7821029b7..1ec3b7a11 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResults.tsx
@@ -1,14 +1,30 @@
import { SearchOrAskAiErrorCallout } from '../../SearchOrAskAiErrorCallout'
-import { usePageNumber, useSearchActions, useSearchTerm } from '../search.store'
+import { useSearchActions, useSearchTerm } from '../search.store'
+import { useSearchFilters, type FilterType } from '../useSearchFilters'
import { useSearchQuery } from '../useSearchQuery'
import { SearchResultListItem } from './SearchResultsListItem'
-import { EuiPagination, EuiSpacer } from '@elastic/eui'
+import {
+ useEuiOverflowScroll,
+ EuiSpacer,
+ useEuiTheme,
+ EuiHorizontalRule,
+ EuiButton,
+ EuiText,
+ EuiSkeletonRectangle,
+ EuiSkeletonLoading,
+ EuiSkeletonText,
+ EuiSkeletonTitle,
+} from '@elastic/eui'
import { css } from '@emotion/react'
import { useDebounce } from '@uidotdev/usehooks'
-import { useEffect } from 'react'
+import { useEffect, useRef, useCallback } from 'react'
+import type { MouseEvent } from 'react'
interface SearchResultsProps {
- onKeyDown?: (e: React.KeyboardEvent, index: number) => void
+ onKeyDown?: (
+ e: React.KeyboardEvent,
+ index: number
+ ) => void
setItemRef?: (element: HTMLAnchorElement | null, index: number) => void
}
@@ -16,16 +32,41 @@ export const SearchResults = ({
onKeyDown,
setItemRef,
}: SearchResultsProps) => {
+ const { euiTheme } = useEuiTheme()
const searchTerm = useSearchTerm()
- const activePage = usePageNumber()
const { setPageNumber: setActivePage } = useSearchActions()
const debouncedSearchTerm = useDebounce(searchTerm, 300)
+ const scrollContainerRef = useRef(null)
+
+ const scrollbarStyle = css`
+ max-height: 400px;
+ padding-block: ${euiTheme.size.base};
+ margin-right: ${euiTheme.size.s};
+ ${useEuiOverflowScroll('y', true)}
+ `
+
+ const resetScrollToTop = useCallback(() => {
+ if (scrollContainerRef.current) {
+ scrollContainerRef.current.scrollTop = 0
+ }
+ }, [])
useEffect(() => {
setActivePage(0)
- }, [debouncedSearchTerm])
+ }, [debouncedSearchTerm, setActivePage])
+
+ useEffect(() => {
+ resetScrollToTop()
+ }, [debouncedSearchTerm, resetScrollToTop])
+
+ const { data, error, isLoading } = useSearchQuery()
- const { data, error } = useSearchQuery()
+ const { selectedFilters, handleFilterClick, filteredResults, counts } =
+ useSearchFilters({
+ results: data?.results ?? [],
+ })
+
+ const isInitialLoading = isLoading && !data
if (!searchTerm) {
return null
@@ -41,43 +82,215 @@ export const SearchResults = ({
{error && }
{!error && (
-
-
- {data && (
- <>
-
- {data.results.map((result, index) => (
-
- ))}
-
-
-
-
- setActivePage(activePage)
- }
- />
-
- >
+ <>
+
+
+
+
+
+ {!isInitialLoading && filteredResults.length === 0 && (
+
+ No results
+
)}
-
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+ ))}
+
+ }
+ loadedContent={
+ data && (
+
+ {filteredResults.map(
+ (result, index) => (
+
+ )
+ )}
+
+ )
+ }
+ />
+
+ >
)}
>
)
}
+
+const Filter = ({
+ selectedFilters,
+ onFilterClick,
+ counts,
+ isLoading,
+}: {
+ selectedFilters: Set
+ onFilterClick: (filter: FilterType, event?: MouseEvent) => void
+ counts: {
+ apiResultsCount: number
+ docsResultsCount: number
+ totalCount: number
+ }
+ isLoading: boolean
+}) => {
+ const { euiTheme } = useEuiTheme()
+ const { apiResultsCount, docsResultsCount, totalCount } = counts
+
+ const buttonStyle = css`
+ border-radius: 99999px;
+ padding-inline: ${euiTheme.size.m};
+ min-inline-size: auto;
+ `
+
+ const skeletonStyle = css`
+ border-radius: 99999px;
+ `
+
+ return (
+
+
+ onFilterClick('all', e)}
+ css={buttonStyle}
+ aria-label={`Show all results, ${totalCount} total`}
+ aria-pressed={selectedFilters.has('all')}
+ >
+ {isLoading ? 'ALL' : `ALL (${totalCount})`}
+
+
+
+ onFilterClick('doc', e)}
+ css={buttonStyle}
+ aria-label={`Filter to documentation results, ${docsResultsCount} available`}
+ aria-pressed={selectedFilters.has('doc')}
+ >
+ {isLoading ? 'DOCS' : `DOCS (${docsResultsCount})`}
+
+
+
+ onFilterClick('api', e)}
+ css={buttonStyle}
+ aria-label={`Filter to API results, ${apiResultsCount} available`}
+ aria-pressed={selectedFilters.has('api')}
+ >
+ {isLoading ? 'API' : `API (${apiResultsCount})`}
+
+
+
+ )
+}
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx
index 0cab2d657..1e9df8a8b 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/SearchResults/SearchResultsListItem.tsx
@@ -51,7 +51,10 @@ interface SearchResultListItemProps {
index: number
pageNumber: number
pageSize: number
- onKeyDown?: (e: React.KeyboardEvent, index: number) => void
+ onKeyDown?: (
+ e: React.KeyboardEvent,
+ index: number
+ ) => void
setRef?: (element: HTMLAnchorElement | null, index: number) => void
}
@@ -84,65 +87,67 @@ export function SearchResultListItem({
}
return (
-
+
setRef?.(el, index)}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleClick()
- // Navigate to the result URL
window.location.href = result.url
} else {
- // Type mismatch: event is from anchor but handler expects HTMLLIElement
- onKeyDown?.(
- e as unknown as React.KeyboardEvent,
- index
- )
+ onKeyDown?.(e, index)
}
}}
css={css`
- display: flex;
+ display: grid;
+ grid-template-columns: auto 1fr auto;
align-items: center;
gap: ${euiTheme.size.base};
- border-radius: ${euiTheme.border.radius.small};
- width: 100%;
+ border-radius: ${euiTheme.border.radius.medium};
padding-inline: ${euiTheme.size.base};
padding-block: ${euiTheme.size.m};
- :hover {
- background-color: ${euiTheme.colors
- .backgroundBaseSubdued};
- }
+ margin-inline: ${euiTheme.size.base};
+ border: 1px solid transparent;
+ :hover,
:focus {
background-color: ${euiTheme.colors
.backgroundBaseSubdued};
- }
- :focus .return-key-icon {
- visibility: visible;
+ border-color: ${euiTheme.colors.borderBasePlain};
+ .return-key-icon {
+ visibility: visible;
+ }
}
`}
tabIndex={0}
href={result.url}
>
- {/**/}
-
+
+
- {result.title}
+
@@ -157,17 +162,12 @@ export function SearchResultListItem({
overflow: hidden;
//width: 90%;
-
- mark {
- background-color: transparent;
- font-weight: ${euiTheme.font.weight.bold};
- color: ${euiTheme.colors.link};
- }
`}
>
{result.highlightedBody ? (
) : (
{result.description}
@@ -177,7 +177,10 @@ export function SearchResultListItem({
{result.parents.length > 0 && (
<>
-
+
>
)}
@@ -195,7 +198,13 @@ export function SearchResultListItem({
)
}
-function Breadcrumbs({ parents }: { parents: SearchResultItem['parents'] }) {
+function Breadcrumbs({
+ type,
+ parents,
+}: {
+ type: SearchResultItem['type']
+ parents: SearchResultItem['parents']
+}) {
const { euiTheme } = useEuiTheme()
const { fontSize: smallFontsize } = useEuiFontSize('xs')
return (
@@ -208,6 +217,23 @@ function Breadcrumbs({ parents }: { parents: SearchResultItem['parents'] }) {
list-style: none;
`}
>
+
+
+ {type === 'api' ? 'API' : 'Docs'}
+
+
{parents.slice(1).map((parent) => (
{
+ ({ htmlContent, ellipsis }: { htmlContent: string; ellipsis: boolean }) => {
const processed = useMemo(() => {
if (!htmlContent) return ''
@@ -242,8 +268,13 @@ const SanitizedHtmlContent = memo(
KEEP_CONTENT: true,
})
+ if (!ellipsis) {
+ return sanitized
+ }
+
const temp = document.createElement('div')
temp.innerHTML = sanitized
+
const text = temp.textContent || ''
const firstChar = text.trim()[0]
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/TellMeMoreButton.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/TellMeMoreButton.tsx
index ef6d82faa..34ec57456 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/TellMeMoreButton.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/TellMeMoreButton.tsx
@@ -12,7 +12,7 @@ const gradientContainerStyles = css`
background-position: 100% 0%;
}
}
- height: 42px;
+ padding: var(--outline-size);
background: linear-gradient(
90deg,
#f04e98 0%,
@@ -23,9 +23,6 @@ const gradientContainerStyles = css`
);
background-size: 200% 100%;
background-position: 0% 0%;
- display: flex;
- align-items: center;
- justify-content: center;
border-radius: 4px;
animation: gradientMove 3s ease infinite;
`
@@ -53,6 +50,7 @@ export const TellMeMoreButton = forwardRef<
span {
display: flex;
@@ -61,7 +59,6 @@ export const TellMeMoreButton = forwardRef<
width: 100%;
gap: ${euiTheme.size.s};
}
- margin-inline: 1px;
border: none;
position: relative;
:focus .return-key-icon {
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useKeyboardNavigation.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useKeyboardNavigation.ts
index 83418a840..70b601027 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useKeyboardNavigation.ts
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useKeyboardNavigation.ts
@@ -6,7 +6,7 @@ interface KeyboardNavigationReturn {
listItemRefs: MutableRefObject<(HTMLAnchorElement | null)[]>
handleInputKeyDown: (e: React.KeyboardEvent) => void
handleListItemKeyDown: (
- e: React.KeyboardEvent,
+ e: React.KeyboardEvent,
currentIndex: number
) => void
focusLastAvailable: () => void
@@ -54,7 +54,7 @@ export const useKeyboardNavigation = (
}
const handleListItemKeyDown = (
- e: React.KeyboardEvent,
+ e: React.KeyboardEvent,
currentIndex: number
) => {
if (e.key === 'ArrowDown') {
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchFilters.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchFilters.ts
new file mode 100644
index 000000000..0e643384e
--- /dev/null
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchFilters.ts
@@ -0,0 +1,67 @@
+import { SearchResponse } from './useSearchQuery'
+import { useState, useMemo } from 'react'
+import type { MouseEvent } from 'react'
+
+export type FilterType = 'all' | 'doc' | 'api'
+
+interface UseSearchFiltersOptions {
+ results: SearchResponse['results']
+}
+
+export const useSearchFilters = ({ results }: UseSearchFiltersOptions) => {
+ const [selectedFilters, setSelectedFilters] = useState>(
+ new Set(['all'])
+ )
+
+ const isMultiSelectModifierPressed = (event?: MouseEvent): boolean => {
+ return !!(event && (event.metaKey || event.altKey || event.ctrlKey))
+ }
+
+ const toggleFilter = (
+ currentFilters: Set,
+ filter: FilterType
+ ): Set => {
+ const newFilters = new Set(currentFilters)
+ newFilters.delete('all')
+ if (newFilters.has(filter)) {
+ newFilters.delete(filter)
+ } else {
+ newFilters.add(filter)
+ }
+ return newFilters.size === 0 ? new Set(['all']) : newFilters
+ }
+
+ const handleFilterClick = (filter: FilterType, event?: MouseEvent) => {
+ if (filter === 'all') {
+ setSelectedFilters(new Set(['all']))
+ return
+ }
+
+ if (isMultiSelectModifierPressed(event)) {
+ setSelectedFilters((prev) => toggleFilter(prev, filter))
+ } else {
+ setSelectedFilters(new Set([filter]))
+ }
+ }
+
+ const filteredResults = useMemo(() => {
+ if (selectedFilters.has('all')) {
+ return results
+ }
+ return results.filter((result) => selectedFilters.has(result.type))
+ }, [results, selectedFilters])
+
+ const counts = useMemo(() => {
+ const apiResultsCount = results.filter((r) => r.type === 'api').length
+ const docsResultsCount = results.filter((r) => r.type === 'doc').length
+ const totalCount = docsResultsCount + apiResultsCount
+ return { apiResultsCount, docsResultsCount, totalCount }
+ }, [results])
+
+ return {
+ selectedFilters,
+ handleFilterClick,
+ filteredResults,
+ counts,
+ }
+}
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts
index 44ef05335..6bca5d3ad 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/Search/useSearchQuery.ts
@@ -29,12 +29,13 @@ const SearchResultItemParent = z.object({
})
const SearchResultItem = z.object({
- type: z.string().default('doc'),
+ type: z.enum(['doc', 'api']),
url: z.string(),
title: z.string(),
description: z.string(),
score: z.number(),
parents: z.array(SearchResultItemParent),
+ highlightedTitle: z.string().nullish(),
highlightedBody: z.string().nullish(),
})
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiButton.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiButton.tsx
index 2ee2adb5b..2a5354893 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiButton.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiButton.tsx
@@ -44,7 +44,7 @@ export const SearchOrAskAiButton = () => {
left: 50%;
transform: translateX(-50%);
top: 48px;
- width: 80ch;
+ width: 640px;
max-width: 100%;
`
@@ -109,7 +109,11 @@ export const SearchOrAskAiButton = () => {
-
+
diff --git a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx
index d6eb3b0e6..94aade940 100644
--- a/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx
+++ b/src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/SearchOrAskAiModal.tsx
@@ -11,19 +11,17 @@ import {
import { useModalActions, useModalMode } from './modal.store'
import { useCooldown } from './useCooldown'
import {
- EuiBetaBadge,
- EuiText,
- EuiLink,
EuiTabbedContent,
EuiIcon,
- EuiHorizontalRule,
type EuiTabbedContentTab,
+ useEuiTheme,
} from '@elastic/eui'
import { css } from '@emotion/react'
import * as React from 'react'
import { useMemo } from 'react'
export const SearchOrAskAiModal = React.memo(() => {
+ const { euiTheme } = useEuiTheme()
const modalMode = useModalMode()
const { setModalMode } = useModalActions()
@@ -70,47 +68,16 @@ export const SearchOrAskAiModal = React.memo(() => {
return (
<>
setModalMode(tab.id as 'search' | 'askAi')}
/>
-
+ {/**/}
>
)
})
-
-const ModalFooter = () => {
- return (
- <>
-
-
-
-
-
- This feature is in private preview (alpha).{' '}
-
- Got feedback? We'd love to hear it!
-
-
-
- >
- )
-}
diff --git a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs
index 20d6ed6f9..c94e541a4 100644
--- a/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs
+++ b/src/api/Elastic.Documentation.Api.Core/Search/SearchUsecase.cs
@@ -46,7 +46,7 @@ public record SearchRequest
{
public required string Query { get; init; }
public int PageNumber { get; init; } = 1;
- public int PageSize { get; init; } = 5;
+ public int PageSize { get; init; } = 20;
}
public record SearchResponse
@@ -76,4 +76,6 @@ public record SearchResultItem
public string[]? Headings { get; init; }
public float Score { get; init; }
public string? HighlightedBody { get; init; }
+
+ public string? HighlightedTitle { get; init; }
}
diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs
index 0f420ab09..f7d17105b 100644
--- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs
+++ b/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Search/ElasticsearchGateway.cs
@@ -96,7 +96,7 @@ public ElasticsearchGateway(ElasticsearchOptions elasticsearchOptions, ILogger
private static Query BuildLexicalQuery(string searchQuery)
{
- var tokens = searchQuery.Split(" ");
+ var tokens = searchQuery.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries);
if (tokens is ["datastream" or "datastreams" or "data-stream" or "data-streams"])
{
// /docs/api/doc/kibana/operation/operation-delete-fleet-epm-packages-pkgname-pkgversion-datastream-assets
@@ -276,12 +276,16 @@ private static (int TotalHits, List Results) ProcessSearchResp
var hit = response.Hits.ElementAtOrDefault(index);
var highlights = hit?.Highlight;
+ string? highlightedTitle = null;
string? highlightedBody = null;
if (highlights != null)
{
if (highlights.TryGetValue("stripped_body", out var bodyHighlights) && bodyHighlights.Count > 0)
highlightedBody = string.Join(". ", bodyHighlights.Select(h => h.TrimEnd('.')));
+
+ if (highlights.TryGetValue("title", out var titleHighlights) && titleHighlights.Count > 0)
+ highlightedTitle = string.Join(". ", titleHighlights.Select(h => h.TrimEnd('.')));
}
return new SearchResultItem
@@ -297,6 +301,7 @@ private static (int TotalHits, List Results) ProcessSearchResp
Url = parent.Url
}).ToArray(),
Score = (float)(hit?.Score ?? 0.0),
+ HighlightedTitle = highlightedTitle,
HighlightedBody = highlightedBody
};
}).ToList();