fix: docsearch + sidepanel

pull/5092/head
Dylan Tientcheu 4 weeks ago
parent 24d972ad68
commit a6b8ccd7fe

@ -3,7 +3,6 @@ import {
hasAskAi,
hasKeywordSearch,
mergeLangFacetFilters,
resolveDocSearchMode,
validateCredentials
} from 'client/theme-default/support/docsearch'
@ -41,66 +40,6 @@ describe('client/theme-default/support/docsearch', () => {
})
})
describe('resolveDocSearchMode', () => {
test('defaults to keyword when only keyword search is configured', () => {
expect(
resolveDocSearchMode({
appId: 'app',
apiKey: 'key',
indexName: 'index',
askAi: undefined,
mode: undefined
})
).toBe('keyword')
})
test('infers hybrid when keyword search + askAi are configured', () => {
expect(
resolveDocSearchMode({
appId: 'app',
apiKey: 'key',
indexName: 'index',
askAi: { assistantId: 'assistant' } as any,
mode: 'auto'
})
).toBe('hybrid')
})
test('infers sidePanel when only askAi is configured', () => {
expect(
resolveDocSearchMode({
appId: undefined,
apiKey: undefined,
indexName: undefined,
askAi: { assistantId: 'assistant' } as any,
mode: undefined
})
).toBe('sidePanel')
})
test('respects explicit mode overrides', () => {
expect(
resolveDocSearchMode({
appId: 'app',
apiKey: 'key',
indexName: 'index',
askAi: { assistantId: 'assistant' } as any,
mode: 'sidePanel'
})
).toBe('sidePanel')
expect(
resolveDocSearchMode({
appId: undefined,
apiKey: undefined,
indexName: undefined,
askAi: { assistantId: 'assistant' } as any,
mode: 'hybrid'
})
).toBe('hybrid')
})
})
describe('hasKeywordSearch', () => {
test('returns true when all credentials are provided', () => {
expect(

@ -121,7 +121,9 @@ export default defineConfig({
askAi: {
assistantId: 'YaVSonfX5bS8',
sidePanel: {
pushSelector: '.Layout'
button: {
variant: 'inline'
}
}
}
}

@ -95,9 +95,9 @@
"*": "prettier --experimental-cli --ignore-unknown --write"
},
"dependencies": {
"@docsearch/css": "^4.5.0-beta.0",
"@docsearch/js": "^4.5.0-beta.0",
"@docsearch/sidepanel-js": "^4.5.0-beta.0",
"@docsearch/css": "^4.5.0-beta.2",
"@docsearch/js": "^4.5.0-beta.2",
"@docsearch/sidepanel-js": "^4.5.0-beta.2",
"@iconify-json/simple-icons": "^1.2.59",
"@shikijs/core": "^3.15.0",
"@shikijs/transformers": "^3.15.0",

@ -27,14 +27,14 @@ importers:
.:
dependencies:
'@docsearch/css':
specifier: ^4.5.0-beta.0
version: 4.5.0-beta.0
specifier: ^4.5.0-beta.2
version: 4.5.0-beta.2
'@docsearch/js':
specifier: ^4.5.0-beta.0
version: 4.5.0-beta.0
specifier: ^4.5.0-beta.2
version: 4.5.0-beta.2
'@docsearch/sidepanel-js':
specifier: ^4.5.0-beta.0
version: 4.5.0-beta.0
specifier: ^4.5.0-beta.2
version: 4.5.0-beta.2
'@iconify-json/simple-icons':
specifier: ^1.2.59
version: 1.2.59
@ -399,14 +399,14 @@ packages:
conventional-commits-parser:
optional: true
'@docsearch/css@4.5.0-beta.0':
resolution: {integrity: sha512-qVJpvv5Qtfiire7TNFdzi+SQ27EjL+qXu5/EMUFSogJeP7K3PruH1OefTjEPygTp6r020hhfG2rLDXcsrVveNw==}
'@docsearch/css@4.5.0-beta.2':
resolution: {integrity: sha512-ChNiH493EnFiYEy8ntrjcGHG9eYDSNa8VJy3dvSom3A9YPmXBC40aoX7yHUfjFxO3Ue5ZNXhTxuhC5uyWgSUww==}
'@docsearch/js@4.5.0-beta.0':
resolution: {integrity: sha512-Z7PZIm00WgOpdhwsRAZ8w6pUJdF/YGGAtKzORS+vz7Z/v63anc/75TH3uxDfnEow8kbqJY8heN/ij86BQQ7yzw==}
'@docsearch/js@4.5.0-beta.2':
resolution: {integrity: sha512-hAt/P7K5eiYW65Xo/4xbDXjLtGgqXEg2y4knz6CUfCcc9m5KnNoi0o1s4lOyP6kehatv3SAiEHoCNGEa+sIqdA==}
'@docsearch/sidepanel-js@4.5.0-beta.0':
resolution: {integrity: sha512-Bh9ZX6+BJvneO1q7lXwpK05vLGeZC4qNxsRgQHwfKCWY0YDKmk0T2qSPt5gVs4i7TMC8G9DbSFMfaFrYqLJR7Q==}
'@docsearch/sidepanel-js@4.5.0-beta.2':
resolution: {integrity: sha512-HGh0tGlnq5RFowvunjLrQmOgBm5vlFPGfoCbZ6d1xQazepc6yCquBcZn5BTyK+f5D6vzjl0dK4plHX1hmMKVLA==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@ -3254,11 +3254,11 @@ snapshots:
conventional-commits-filter: 5.0.0
conventional-commits-parser: 6.2.1
'@docsearch/css@4.5.0-beta.0': {}
'@docsearch/css@4.5.0-beta.2': {}
'@docsearch/js@4.5.0-beta.0': {}
'@docsearch/js@4.5.0-beta.2': {}
'@docsearch/sidepanel-js@4.5.0-beta.0': {}
'@docsearch/sidepanel-js@4.5.0-beta.2': {}
'@emnapi/core@1.7.1':
dependencies:

@ -1,26 +1,28 @@
<script setup lang="ts">
import docsearch from '@docsearch/js'
import sidepanel from '@docsearch/sidepanel-js'
import docsearch, { type DocSearchInstance, type DocSearchProps } from '@docsearch/js'
import sidepanel, { type SidepanelInstance, type SidepanelProps } from '@docsearch/sidepanel-js'
import { useRouter } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onMounted, onUnmounted, watch } from 'vue'
import { useData } from '../composables/data'
import {
buildAskAiConfig,
hasKeywordSearch,
mergeLangFacetFilters,
resolveDocSearchMode,
validateCredentials
} from '../support/docsearch'
const props = defineProps<{
algolia: DefaultTheme.AlgoliaSearchOptions
openRequest?: { target: 'search' | 'askAi' | 'toggleAskAi'; nonce: number } | null
}>()
const router = useRouter()
const { site, localeIndex, lang } = useData()
let cleanup: (() => void) | undefined
let docsearchInstance: DocSearchInstance | undefined
let sidepanelInstance: SidepanelInstance | undefined
let openOnReady: 'search' | 'askAi' | null = null
onMounted(update)
watch(localeIndex, update)
@ -28,6 +30,39 @@ onUnmounted(() => {
cleanup?.()
})
watch(
() => props.openRequest?.nonce,
() => {
const req = props.openRequest
if (!req) return
if (req.target === 'search') {
if (docsearchInstance?.isReady) {
docsearchInstance.open()
} else {
openOnReady = 'search'
}
} else if (req.target === 'toggleAskAi') {
if (sidepanelInstance?.isOpen) {
sidepanelInstance.close()
} else {
sidepanelInstance?.open()
}
} else {
// askAi - open sidepanel or fallback to docsearch modal
if (sidepanelInstance?.isReady) {
sidepanelInstance.open()
} else if (sidepanelInstance) {
openOnReady = 'askAi'
} else if (docsearchInstance?.isReady) {
docsearchInstance.openAskAi()
} else {
openOnReady = 'askAi'
}
}
},
{ immediate: true }
)
async function update() {
await nextTick()
const options = {
@ -43,30 +78,12 @@ async function update() {
? buildAskAiConfig(options.askAi, options, lang.value)
: undefined
const resolvedMode = resolveDocSearchMode({
mode: options.mode,
appId: options.appId,
apiKey: options.apiKey,
indexName: options.indexName,
askAi: askAi as any
})
const keywordConfigured = hasKeywordSearch(options)
// For sidePanel mode, credentials can come from askAi config
const effectiveCredentials = validateCredentials({
appId: options.appId || (askAi && typeof askAi === 'object' ? askAi.appId : undefined),
apiKey: options.apiKey || (askAi && typeof askAi === 'object' ? askAi.apiKey : undefined),
indexName: options.indexName || (askAi && typeof askAi === 'object' ? askAi.indexName : undefined)
})
if (resolvedMode === 'hybrid' && !keywordConfigured) {
console.warn(
'[vitepress] Algolia search mode is set to "hybrid" but keyword search is not configured (missing appId/apiKey/indexName).'
)
return
}
if (!effectiveCredentials.valid) {
console.warn(
'[vitepress] Algolia search cannot be initialized: missing appId/apiKey/indexName.'
@ -74,10 +91,6 @@ async function update() {
return
}
// Clean up any previous initialization (locale switch etc)
cleanup?.()
cleanup = undefined
initialize({
...options,
appId: effectiveCredentials.appId,
@ -87,20 +100,37 @@ async function update() {
...options.searchParameters,
facetFilters
},
askAi: askAi as any
askAi: askAi as DocSearchProps["askAi"]
})
}
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
// Ensure containers exist and start clean
const searchContainer = document.querySelector('#docsearch')
const sidePanelContainer = document.querySelector('#docsearch-sidepanel')
if (searchContainer) (searchContainer as HTMLElement).innerHTML = ''
if (sidePanelContainer) (sidePanelContainer as HTMLElement).innerHTML = ''
// Always tear down previous instances first (e.g. on locale changes)
cleanup?.()
const askAi = userOptions.askAi
const sidePanelConfig = askAi && typeof askAi === 'object' ? askAi.sidePanel : undefined
if (askAi && typeof askAi === 'object' && sidePanelConfig) {
const { keyboardShortcuts, ...restConfig } = sidePanelConfig !== true ? sidePanelConfig : {} as SidepanelProps
sidepanelInstance = sidepanel({
container: '#docsearch-sidepanel',
indexName: askAi.indexName ?? userOptions.indexName,
appId: askAi.appId ?? userOptions.appId,
apiKey: askAi.apiKey ?? userOptions.apiKey,
assistantId: askAi.assistantId,
onReady: () => {
if (openOnReady === 'askAi') {
openOnReady = null
setTimeout(() => { sidepanelInstance?.open() }, 0)
}
},
...restConfig,
} as SidepanelProps)
}
const options = Object.assign({}, userOptions, {
container: '#docsearch',
navigator: {
navigate(item: { itemUrl: string }) {
router.go(item.itemUrl)
@ -113,34 +143,38 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
url: getRelativePath(item.url)
})
})
},
// When sidepanel is enabled, intercept Ask AI events to open it instead (hybrid mode)
...(sidepanelInstance && {
interceptAskAiEvent: (initialMessage: { query: string; messageId?: string; suggestedQuestionId?: string }) => {
docsearchInstance?.close()
setTimeout(() => sidepanelInstance?.open(initialMessage), 0)
return true
}
}),
onReady: () => {
if (openOnReady === 'search') {
openOnReady = null
setTimeout(() => docsearchInstance?.open(), 0)
} else if (openOnReady === 'askAi' && !sidepanelInstance) {
// No sidepanel configured, use docsearch modal for askAi
openOnReady = null
console.log('openAskAi', docsearchInstance)
setTimeout(() => docsearchInstance?.openAskAi(), 0)
}
}
})
docsearch(options as any)
// Side panel init (mirrors the demo-js example)
// @see https://docsearch.algolia.com/docs/sidepanel/api-reference
const askAi = userOptions.askAi
const sidePanelConfig =
askAi && typeof askAi === 'object' ? askAi.sidePanel : undefined
if (askAi && typeof askAi === 'object' && sidePanelConfig) {
const { keyboardShortcuts, ...restConfig } = sidePanelConfig !== true ? sidePanelConfig : {}
sidepanel({
container: '#docsearch-sidepanel',
indexName: askAi.indexName ?? userOptions.indexName,
appId: askAi.appId ?? userOptions.appId,
apiKey: askAi.apiKey ?? userOptions.apiKey,
assistantId: askAi.assistantId,
...restConfig
// keyboardShortcuts removed - always use default Cmd+I / Ctrl+I
} as any)
}
docsearchInstance = docsearch(options as DocSearchProps)
cleanup = () => {
// best-effort cleanup: remove rendered markup
if (searchContainer) (searchContainer as HTMLElement).innerHTML = ''
if (sidePanelContainer) (sidePanelContainer as HTMLElement).innerHTML = ''
docsearchInstance?.destroy()
sidepanelInstance?.destroy()
docsearchInstance = undefined
sidepanelInstance = undefined
openOnReady = null
}
}

@ -1,17 +1,7 @@
<script lang="ts" setup></script>
<template>
<button
type="button"
aria-label="Ask AI"
class="DocSearch DocSearch-Button VPNavBarAskAiButton"
>
<button type="button" aria-label="Ask AI" class="DocSearch DocSearch-Button VPNavBarAskAiButton">
<span class="DocSearch-Button-Container">
<span class="DocSearch-Button-Placeholder">Ask AI</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key"></kbd>
<span class="vpi-sparkles DocSearch-Search-Icon"></span>
</span>
</button>
</template>
@ -58,5 +48,17 @@
.DocSearch-SidepanelButton {
display: none !important;
}
</style>
.VPNavBarAskAiButton {
transition: all 0.25s;
}
.VPNavBarAskAiButton:hover {
background: var(--docsearch-primary-color);
}
.VPNavBarAskAiButton:hover .DocSearch-Search-Icon {
background-color: var(--vp-c-white);
}
</style>

@ -5,7 +5,7 @@ import { onKeyStroke } from '@vueuse/core'
import type { DefaultTheme } from 'vitepress/theme'
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
import { useData } from '../composables/data'
import { hasKeywordSearch, resolveDocSearchMode } from '../support/docsearch'
import { hasKeywordSearch } from '../support/docsearch'
import VPNavBarAskAiButton from './VPNavBarAskAiButton.vue'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
@ -30,10 +30,8 @@ const algoliaOptions = computed(() => {
} as DefaultTheme.AlgoliaSearchOptions
})
const resolvedAlgoliaMode = computed(() => resolveDocSearchMode(algoliaOptions.value))
const showKeywordSearchButton = computed(
() =>
resolvedAlgoliaMode.value !== 'sidePanel' &&
hasKeywordSearch(algoliaOptions.value)
)
@ -49,7 +47,10 @@ const askAiShortcutEnabled = computed(() => {
return cfg?.keyboardShortcuts?.['Ctrl/Cmd+I'] !== false
})
let isProgrammaticOpen = false
type OpenTarget = 'search' | 'askAi' | 'toggleAskAi'
type OpenRequest = { target: OpenTarget; nonce: number }
const openRequest = ref<OpenRequest | null>(null)
let openNonce = 0
// to avoid loading the docsearch js upfront (which is more than 1/3 of the
// payload), we delay initializing it until the user has actually clicked or
@ -89,8 +90,6 @@ onMounted(() => {
preconnect()
const handleSearchHotKey = (event: KeyboardEvent) => {
if (isProgrammaticOpen) return
const key = event.key?.toLowerCase()
if (
@ -99,7 +98,6 @@ onMounted(() => {
(!isEditingContent(event) && event.key === '/'))
) {
event.preventDefault()
remove()
loadAndOpen('search')
return
}
@ -112,7 +110,6 @@ onMounted(() => {
!isEditingContent(event)
) {
event.preventDefault()
remove()
loadAndOpen('askAi')
}
}
@ -126,61 +123,14 @@ onMounted(() => {
onUnmounted(remove)
})
type OpenTarget = 'search' | 'askAi'
function programmaticOpen(target: OpenTarget) {
isProgrammaticOpen = true
open(target)
queueMicrotask(() => {
isProgrammaticOpen = false
})
}
function loadAndOpen(target: OpenTarget) {
if (!loaded.value) {
loaded.value = true
setTimeout(() => pollOpen(target, 0), 16)
return
}
programmaticOpen(target)
}
function open(target: OpenTarget) {
const e = new Event('keydown') as any
e.key = target === 'search' ? 'k' : 'i'
e.metaKey = true
e.ctrlKey = true
window.dispatchEvent(e)
}
function pollOpen(target: OpenTarget, tries: number) {
// For askAi, first wait until sidepanel-js has rendered its button
if (target === 'askAi') {
const sidepanelReady = document.querySelector(
'#docsearch-sidepanel .DocSearchSidepanel-Button, #docsearch-sidepanel [class*="Sidepanel"]'
)
if (!sidepanelReady) {
if (tries < 120) {
setTimeout(() => pollOpen(target, tries + 1), 16)
}
return
}
}
programmaticOpen(target)
setTimeout(() => {
const opened =
target === 'search'
? Boolean(document.querySelector('.DocSearch-Modal'))
: Boolean(document.querySelector('[class*="Sidepanel"][class*="open"], [class*="Sidepanel"][class*="visible"]'))
if (opened) return
if (tries >= 120) return
pollOpen(target, tries + 1)
}, 16)
// This will either be handled immediately if DocSearch is ready,
// or queued by the AlgoliaSearchBox until its instances become ready.
openRequest.value = { target, nonce: ++openNonce }
}
function isEditingContent(event: KeyboardEvent): boolean {
@ -221,10 +171,7 @@ const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
<template>
<div class="VPNavBarSearch">
<template v-if="provider === 'local'">
<VPLocalSearchBox
v-if="showSearch"
@close="showSearch = false"
/>
<VPLocalSearchBox v-if="showSearch" @close="showSearch = false" />
<div id="local-search">
<VPNavBarSearchButton @click="showSearch = true" />
@ -232,26 +179,15 @@ const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
</template>
<template v-else-if="provider === 'algolia'">
<VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
@vue:beforeMount="actuallyLoaded = true"
/>
<div v-if="!actuallyLoaded" id="docsearch">
<VPNavBarSearchButton
v-if="showKeywordSearchButton"
@click="loadAndOpen('search')"
/>
</div>
<VPAlgoliaSearchBox v-if="loaded" :algolia="theme.search?.options ?? theme.algolia" :open-request="openRequest"
@vue:beforeMount="actuallyLoaded = true" />
<!-- Ask AI button (always visible when configured) -->
<VPNavBarAskAiButton
v-if="askAiSidePanelConfig"
@click="loadAndOpen('askAi')"
/>
<div v-if="!actuallyLoaded">
<VPNavBarSearchButton v-if="showKeywordSearchButton" @click="loadAndOpen('search')" />
</div>
<!-- Sidepanel-js renders the panel into this container -->
<VPNavBarAskAiButton v-if="askAiSidePanelConfig"
@click="actuallyLoaded ? loadAndOpen('toggleAskAi') : loadAndOpen('askAi')" />
<div id="docsearch-sidepanel" />
</template>
</div>

@ -24,10 +24,7 @@ const translate = createSearchTranslate(defaultTranslations)
<span class="vpi-search DocSearch-Search-Icon"></span>
<span class="DocSearch-Button-Placeholder">{{ translate('button.buttonText') }}</span>
</span>
<span class="DocSearch-Button-Keys">
<kbd class="DocSearch-Button-Key"></kbd>
<kbd class="DocSearch-Button-Key"></kbd>
</span>
<span class="DocSearch-Button-Keys"/>
</button>
</template>
@ -40,7 +37,7 @@ const translate = createSearchTranslate(defaultTranslations)
--docsearch-focus-color: var(--vp-c-brand-1);
--docsearch-footer-background: var(--vp-c-bg);
--docsearch-highlight-color: var(--vp-c-brand-1);
--docsearch-hit-background: var(--vp-c-default-soft);
--docsearch-hit-background: var(--vp-c-bg);
--docsearch-hit-color: var(--vp-c-text-1);
--docsearch-hit-highlight-color: var(--vp-c-brand-soft);
--docsearch-icon-color: var(--vp-c-text-2);
@ -92,7 +89,7 @@ const translate = createSearchTranslate(defaultTranslations)
--docsearch-muted-color: var(--docsearch-text-color);
--docsearch-searchbox-background: transparent;
width: auto;
padding: 2px 12px;
padding: 0px 8px;
border: none;
border-radius: 8px;
}

@ -76,9 +76,11 @@
.vpi-search {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.6'%3E%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-sparkles {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.4'%3E%3Cpath d='M12 2l1.7 6.3L20 10l-6.3 1.7L12 18l-1.7-6.3L4 10l6.3-1.7z'/%3E%3Cpath d='M5 15l.8 3.2L9 19l-3.2.8L5 23l-.8-3.2L1 19l3.2-.8z'/%3E%3C/g%3E%3C/svg%3E");
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.3'%3E%3Cpath d='M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z'/%3E%3Cpath d='M20 3v4'/%3E%3Cpath d='M22 5h-4'/%3E%3Cpath d='M4 17v2'/%3E%3Cpath d='M5 18H3'/%3E%3C/g%3E%3C/svg%3E");
}
.vpi-layout-list {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7m-7 5h7m-7 6h7m-7 5h7'/%3E%3C/g%3E%3C/svg%3E");
}

@ -1,6 +1,5 @@
import type { DefaultTheme } from 'vitepress/theme'
export type ResolvedDocSearchMode = 'keyword' | 'hybrid' | 'sidePanel'
export type FacetFilter = string | string[]
export interface ValidatedCredentials {
@ -27,22 +26,6 @@ export function hasAskAi(
return Boolean(askAi.assistantId)
}
export function resolveDocSearchMode(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'mode' | 'appId' | 'apiKey' | 'indexName' | 'askAi'
>
): ResolvedDocSearchMode {
if (options.mode === 'sidePanel') return 'sidePanel'
if (options.mode === 'hybrid') return 'hybrid'
// auto (default)
const keyword = hasKeywordSearch(options)
const askAiEnabled = hasAskAi(options.askAi)
if (askAiEnabled) return keyword ? 'hybrid' : 'sidePanel'
return 'keyword'
}
/**
* Removes existing `lang:` filters and appends `lang:${lang}`.
* Handles both flat arrays and nested arrays (for OR conditions).

@ -147,7 +147,7 @@ export async function createVitePressPlugin(
'vitepress > @vue/devtools-api',
'vitepress > @vueuse/core'
].filter((d) => d != null),
exclude: ['@docsearch/js', 'vitepress']
exclude: ['@docsearch/js', '@docsearch/sidepanel-js', 'vitepress']
},
server: {
fs: {

Loading…
Cancel
Save