feat(theme): upgrade DocSearch to 4.5 with sidepanel (#5092)

---------

Co-authored-by: Divyansh Singh <40380293+brc-dd@users.noreply.github.com>
pull/5106/head
Dylan Tientcheu 6 months ago committed by GitHub
parent d4796a0373
commit 0d646a66cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,7 +4,7 @@ describe('local search', () => {
})
test('exclude content from search results', async () => {
await page.locator('#local-search button').click()
await page.locator('.VPNavBarSearchButton').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('local')

@ -0,0 +1,196 @@
import {
buildAskAiConfig,
hasAskAi,
hasKeywordSearch,
mergeLangFacetFilters,
validateCredentials
} from 'client/theme-default/support/docsearch'
describe('client/theme-default/support/docsearch', () => {
describe('mergeLangFacetFilters', () => {
test('adds a lang facet filter when none is provided', () => {
expect(mergeLangFacetFilters(undefined, 'en')).toEqual(['lang:en'])
})
test('replaces existing lang facet filters', () => {
expect(mergeLangFacetFilters('lang:fr', 'en')).toEqual(['lang:en'])
expect(mergeLangFacetFilters(['foo', 'lang:fr'], 'en')).toEqual([
'foo',
'lang:en'
])
})
test('handles nested facet filters (OR conditions)', () => {
expect(
mergeLangFacetFilters([['tag:foo', 'tag:bar'], 'lang:fr'], 'en')
).toEqual([['tag:foo', 'tag:bar'], 'lang:en'])
})
test('removes empty nested arrays', () => {
expect(mergeLangFacetFilters([['lang:fr'], 'other'], 'en')).toEqual([
'other',
'lang:en'
])
})
test('handles multiple lang filters in nested arrays', () => {
expect(
mergeLangFacetFilters([['lang:fr', 'tag:foo'], 'bar'], 'en')
).toEqual([['tag:foo'], 'bar', 'lang:en'])
})
})
describe('hasKeywordSearch', () => {
test('returns true when all credentials are provided', () => {
expect(
hasKeywordSearch({
appId: 'app',
apiKey: 'key',
indexName: 'index'
})
).toBe(true)
})
test('returns false when any credential is missing', () => {
expect(
hasKeywordSearch({
appId: undefined,
apiKey: 'key',
indexName: 'index'
})
).toBe(false)
expect(
hasKeywordSearch({
appId: 'app',
apiKey: undefined,
indexName: 'index'
})
).toBe(false)
expect(
hasKeywordSearch({
appId: 'app',
apiKey: 'key',
indexName: undefined
})
).toBe(false)
})
})
describe('hasAskAi', () => {
test('returns true for valid string assistantId', () => {
expect(hasAskAi('assistant123')).toBe(true)
})
test('returns false for empty string assistantId', () => {
expect(hasAskAi('')).toBe(false)
})
test('returns true for object with assistantId', () => {
expect(hasAskAi({ assistantId: 'assistant123' } as any)).toBe(true)
})
test('returns false for object without assistantId', () => {
expect(hasAskAi({ assistantId: null } as any)).toBe(false)
expect(hasAskAi({} as any)).toBe(false)
})
test('returns false for undefined', () => {
expect(hasAskAi(undefined)).toBe(false)
})
})
describe('validateCredentials', () => {
test('validates complete credentials', () => {
const result = validateCredentials({
appId: 'app',
apiKey: 'key',
indexName: 'index'
})
expect(result.valid).toBe(true)
expect(result.appId).toBe('app')
expect(result.apiKey).toBe('key')
expect(result.indexName).toBe('index')
})
test('invalidates incomplete credentials', () => {
expect(
validateCredentials({
appId: undefined,
apiKey: 'key',
indexName: 'index'
}).valid
).toBe(false)
})
})
describe('buildAskAiConfig', () => {
test('builds config from string assistantId', () => {
const result = buildAskAiConfig(
'assistant123',
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any,
'en'
)
expect(result.assistantId).toBe('assistant123')
expect(result.appId).toBe('app')
expect(result.apiKey).toBe('key')
expect(result.indexName).toBe('index')
})
test('builds config from object with overrides', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
appId: 'custom-app',
apiKey: 'custom-key',
indexName: 'custom-index'
} as any,
{
appId: 'default-app',
apiKey: 'default-key',
indexName: 'default-index'
} as any,
'en'
)
expect(result.assistantId).toBe('assistant123')
expect(result.appId).toBe('custom-app')
expect(result.apiKey).toBe('custom-key')
expect(result.indexName).toBe('custom-index')
})
test('merges facet filters with lang', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
searchParameters: {
facetFilters: ['tag:docs']
}
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any,
'en'
)
expect(result.searchParameters?.facetFilters).toContain('tag:docs')
expect(result.searchParameters?.facetFilters).toContain('lang:en')
})
test('always adds lang facet filter to searchParameters', () => {
const result = buildAskAiConfig(
'assistant123',
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any,
'en'
)
expect(result.searchParameters?.facetFilters).toEqual(['lang:en'])
})
})
})

@ -118,7 +118,10 @@ export default defineConfig({
appId: '8J64VVRP8K',
apiKey: '52f578a92b88ad6abde815aae2b0ad7c',
indexName: 'vitepress',
askAi: 'YaVSonfX5bS8'
askAi: {
assistantId: 'YaVSonfX5bS8',
sidePanel: true
}
}
},

@ -4,7 +4,7 @@ layout: home
hero:
name: VitePress
text: Vite & Vue Powered Static Site Generator
tagline: Markdown to Beautiful Docs in Minutes
tagline: Markdown to beautiful docs in minutes
actions:
- theme: brand
text: What is VitePress?
@ -21,7 +21,7 @@ hero:
features:
- icon: 📝
title: Focus on Your Content
title: Focus on your content
details: Effortlessly create beautiful documentation sites with just markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
title: Enjoy the Vite DX
@ -30,6 +30,6 @@ features:
title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes with Vue.
- icon: 🚀
title: Ship Fast Sites
title: Ship fast sites
details: Fast initial load with static HTML, fast post-load navigation with client-side routing.
---

@ -190,7 +190,7 @@ export default defineConfig({
## Algolia Search
VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer to their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
```ts
import { defineConfig } from 'vitepress'
@ -213,6 +213,19 @@ export default defineConfig({
You can use a config like this to use multilingual search:
<details>
<summary>View full example</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Refer [official Algolia docs](https://docsearch.algolia.com/docs/api#translations) to learn more about them. To quickly get started, you can also copy the translations used by this site from [our GitHub repo](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Algolia Ask AI Support {#ask-ai}
If you would like to include **Ask AI**, pass the `askAi` option (or any of the partial fields) inside `options`:
```ts
import { defineConfig } from 'vitepress'
@ -224,79 +237,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: {
askAiPlaceholder: '向 AI 提问: '
},
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
}
}
// askAi: "YOUR-ASSISTANT-ID"
// OR
askAi: {
// at minimum you must provide the assistantId you received from Algolia
assistantId: 'XXXYYY',
// optional overrides if omitted, the top-level appId/apiKey/indexName values are reused
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -304,11 +253,13 @@ export default defineConfig({
})
```
[These options](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) can be overridden. Refer official Algolia docs to learn more about them.
::: warning Note
If you want to default to keyword search and do not want to use Ask AI, omit the `askAi` property.
:::
### Algolia Ask AI Support {#ask-ai}
### Ask AI Side Panel {#ask-ai-side-panel}
If you would like to include **Ask AI**, pass the `askAi` option (or any of the partial fields) inside `options`:
DocSearch v4.5+ supports an optional **Ask AI side panel**. When enabled, it can be opened with **Ctrl/Cmd+I** by default. The [Sidepanel API Reference](https://docsearch.algolia.com/docs/sidepanel/api-reference) contains the full list of options.
```ts
import { defineConfig } from 'vitepress'
@ -321,15 +272,17 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: "YOUR-ASSISTANT-ID"
// OR
askAi: {
// at minimum you must provide the assistantId you received from Algolia
assistantId: 'XXXYYY',
// optional overrides if omitted, the top-level appId/apiKey/indexName values are reused
// apiKey: '...',
// appId: '...',
// indexName: '...'
sidePanel: {
panel: {
variant: 'floating', // or 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
@ -337,116 +290,70 @@ export default defineConfig({
})
```
::: warning Note
If want to default to keyword search and do not want to use Ask AI, just omit the `askAi` property
:::
If you need to disable the keyboard shortcut, use the `keyboardShortcuts` option at the sidepanel root level:
The translations for the Ask AI UI live under `options.translations.modal.askAiScreen` and `options.translations.resultsScreen` — see the [type definitions](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) for all keys.
```ts
import { defineConfig } from 'vitepress'
### Crawler Config
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
Here is an example config based on what this site uses:
#### Mode (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
You can optionally control how VitePress integrates keyword search and Ask AI:
- `mode: 'auto'` (default): infer `hybrid` when keyword search is configured, otherwise `sidePanel` when Ask AI side panel is configured.
- `mode: 'sidePanel'`: force side panel only (hides the keyword search button).
- `mode: 'hybrid'`: enable keyword search modal + Ask AI side panel (requires keyword search configuration).
- `mode: 'modal'`: keep Ask AI inside the DocSearch modal (even if you configured the side panel).
#### Ask AI only (no keyword search) {#ask-ai-only}
If you want to use **Ask AI side panel only**, you can omit top-level keyword search config and provide credentials under `askAi`:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### Crawler Config
Here is an example config based on what this site uses:
<<< @/snippets/algolia-crawler.js

@ -181,7 +181,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Buscar documentos',
translations: {
button: {
buttonText: 'Buscar',
@ -189,46 +188,72 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: 'Limpiar búsqueda',
clearButtonAriaLabel: 'Limpiar búsqueda',
clearButtonTitle: 'Limpiar',
clearButtonAriaLabel: 'Borrar la consulta',
closeButtonText: 'Cerrar',
closeButtonAriaLabel: 'Cerrar',
placeholderText: undefined,
placeholderTextAskAi: undefined,
placeholderText: 'Buscar en la documentación o preguntar a Ask AI',
placeholderTextAskAi: 'Haz otra pregunta...',
placeholderTextAskAiStreaming: 'Respondiendo...',
searchInputLabel: 'Buscar',
backToKeywordSearchButtonText:
'Volver a la búsqueda por palabras clave',
backToKeywordSearchButtonAriaLabel:
'Volver a la búsqueda por palabras clave'
'Volver a la búsqueda por palabras clave',
newConversationPlaceholder: 'Haz una pregunta',
conversationHistoryTitle: 'Mi historial de conversaciones',
startNewConversationText: 'Iniciar una nueva conversación',
viewConversationHistoryText: 'Historial de conversaciones',
threadDepthErrorPlaceholder: 'Se alcanzó el límite de conversación'
},
newConversation: {
newConversationTitle: '¿Cómo puedo ayudarte hoy?',
newConversationDescription:
'Busco en tu documentación para ayudarte a encontrar guías de configuración, detalles de funciones y consejos de solución de problemas rápidamente.'
},
footer: {
selectText: 'Seleccionar',
submitQuestionText: 'Enviar pregunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Flecha arriba',
navigateDownKeyAriaLabel: 'Flecha abajo',
closeText: 'Cerrar',
backToSearchText: 'Volver a la búsqueda',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Con la tecnología de'
},
errorScreen: {
titleText: 'No se pueden obtener resultados',
helpText: 'Puede que quieras comprobar tu conexión de red.'
},
startScreen: {
recentSearchesTitle: 'Historial de búsqueda',
noRecentSearchesText: 'Ninguna búsqueda reciente',
saveRecentSearchButtonTitle: 'Guardar en el historial de búsqueda',
removeRecentSearchButtonTitle: 'Borrar del historial de búsqueda',
recentSearchesTitle: 'Recientes',
noRecentSearchesText: 'No hay búsquedas recientes',
saveRecentSearchButtonTitle: 'Guardar esta búsqueda',
removeRecentSearchButtonTitle: 'Eliminar esta búsqueda del historial',
favoriteSearchesTitle: 'Favoritos',
removeFavoriteSearchButtonTitle: 'Borrar de favoritos',
removeFavoriteSearchButtonTitle:
'Eliminar esta búsqueda de favoritos',
recentConversationsTitle: 'Conversaciones recientes',
removeRecentConversationButtonTitle:
'Eliminar esta conversación del historial'
},
errorScreen: {
titleText: 'No fue posible obtener resultados',
helpText: 'Verifique su conexión de red'
},
noResultsScreen: {
noResultsText: 'No fue posible encontrar resultados',
suggestedQueryText: 'Puede intentar una nueva búsqueda',
noResultsText: 'No se encontraron resultados para',
suggestedQueryText: 'Intenta buscar',
reportMissingResultsText:
'¿Deberían haber resultados para esta consulta?',
reportMissingResultsLinkText: 'Click para enviar feedback'
'¿Crees que esta consulta debería devolver resultados?',
reportMissingResultsLinkText: 'Avísanos.'
},
resultsScreen: {
askAiPlaceholder: 'Preguntar a la IA: '
askAiPlaceholder: 'Preguntar a la IA: ',
noResultsAskAiPlaceholder:
'¿No lo encontraste en la documentación? Pide ayuda a Ask AI: '
},
askAiScreen: {
disclaimerText:
'Las respuestas son generadas por IA y pueden contener errores. Verifica las respuestas.',
'Las respuestas se generan con IA y pueden contener errores. Verifícalas.',
relatedSourcesText: 'Fuentes relacionadas',
thinkingText: 'Pensando...',
copyButtonText: 'Copiar',
@ -236,23 +261,70 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
copyButtonTitle: 'Copiar',
likeButtonTitle: 'Me gusta',
dislikeButtonTitle: 'No me gusta',
thanksForFeedbackText: '¡Gracias por tu opinión!',
thanksForFeedbackText: '¡Gracias por tu comentario!',
preToolCallText: 'Buscando...',
duringToolCallText: 'Buscando ',
afterToolCallText: 'Búsqueda de',
aggregatedToolCallText: 'Búsqueda de'
duringToolCallText: 'Buscando...',
afterToolCallText: 'Buscado',
stoppedStreamingText: 'Has detenido esta respuesta',
errorTitleText: 'Error de chat',
threadDepthExceededMessage:
'Esta conversación se ha cerrado para mantener respuestas precisas.',
startNewConversationButtonText: 'Iniciar una nueva conversación'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'Preguntar a la IA',
buttonAriaLabel: 'Preguntar a la IA'
}
},
footer: {
selectText: 'Seleccionar',
submitQuestionText: 'Enviar pregunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Flecha arriba',
navigateDownKeyAriaLabel: 'Flecha abajo',
closeText: 'Cerrar',
backToSearchText: 'Volver a la búsqueda',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Búsqueda por'
panel: {
translations: {
header: {
title: 'Preguntar a la IA',
conversationHistoryTitle: 'Mi historial de conversaciones',
newConversationText: 'Iniciar una nueva conversación',
viewConversationHistoryText: 'Historial de conversaciones'
},
promptForm: {
promptPlaceholderText: 'Haz una pregunta',
promptAnsweringText: 'Respondiendo...',
promptAskAnotherQuestionText: 'Haz otra pregunta',
promptDisclaimerText:
'Las respuestas se generan con IA y pueden contener errores.',
promptLabelText:
'Pulsa Enter para enviar, o Shift+Enter para una nueva línea.',
promptAriaLabelText: 'Entrada de prompt'
},
conversationScreen: {
preToolCallText: 'Buscando...',
searchingText: 'Buscando...',
toolCallResultText: 'Buscado',
conversationDisclaimer:
'Las respuestas se generan con IA y pueden contener errores. Verifícalas.',
reasoningText: 'Razonando...',
thinkingText: 'Pensando...',
relatedSourcesText: 'Fuentes relacionadas',
stoppedStreamingText: 'Has detenido esta respuesta',
copyButtonText: 'Copiar',
copyButtonCopiedText: '¡Copiado!',
likeButtonTitle: 'Me gusta',
dislikeButtonTitle: 'No me gusta',
thanksForFeedbackText: '¡Gracias por tu comentario!',
errorTitleText: 'Error de chat'
},
newConversationScreen: {
titleText: '¿Cómo puedo ayudarte hoy?',
introductionText:
'Busco en tu documentación para ayudarte a encontrar guías de configuración, detalles de funciones y consejos de solución de problemas rápidamente.'
},
logo: {
poweredByText: 'Con la tecnología de'
}
}
}
}
}

@ -39,18 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: {
es: { // usa `root` si quieres traducir la configuración regional predeterminada
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: 'Buscar',
buttonAriaLabel: 'Buscar'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
displayDetails: 'Mostrar lista detallada',
resetButtonTitle: 'Restablecer búsqueda',
backButtonTitle: 'Cerrar búsqueda',
noResultsText: 'No hay resultados',
footer: {
selectText: '选择',
navigateText: '切换'
selectText: 'Seleccionar',
selectKeyAriaLabel: 'Intro',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Flecha arriba',
navigateDownKeyAriaLabel: 'Flecha abajo',
closeText: 'Cerrar',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -62,7 +69,7 @@ export default defineConfig({
})
```
### Opciones MiniSearch {#mini-search-options}
### Opciones MiniSearch {#minisearch-options}
Puedes configurar MiniSearch de esta manera:
@ -116,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// retorne un string HTML
// devuelve una cadena HTML
}
}
}
@ -141,7 +148,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.search === false) return ''
if (env.relativePath.startsWith('algum/caminho')) return ''
if (env.relativePath.startsWith('some/path')) return ''
return html
}
}
@ -197,10 +204,23 @@ export default defineConfig({
})
```
### i18n {#algolia-search-i18n} {#algolia-search-i18n}
### i18n {#algolia-search-i18n}
Puedes utilizar una configuración como esta para utilizar la búsqueda multilingüe:
<details>
<summary>Haz clic para expandir</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Consulta la [documentación oficial de Algolia](https://docsearch.algolia.com/docs/api#translations) para conocer más detalles. Para empezar rápidamente, también puedes copiar las traducciones usadas por este sitio desde [nuestro repositorio de GitHub](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Algolia Ask AI Support {#ask-ai}
Si deseas incluir **Ask AI**, pasa la opción `askAi` (o alguno de sus campos parciales) dentro de `options`:
```ts
import { defineConfig } from 'vitepress'
@ -212,72 +232,51 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: { askAiPlaceholder: '向 AI 提问: ' },
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
// askAi: "TU-ID-DE-ASISTENTE"
// O
askAi: {
// como mínimo debes proporcionar el assistantId que recibiste de Algolia
assistantId: 'XXXYYY',
// anulaciones opcionales — si se omiten, se reutilizan los valores appId/apiKey/indexName de nivel superior
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
}
})
```
::: warning Nota
Si prefieres solo la búsqueda por palabra clave y no la Ask AI, simplemente omite `askAi`.
:::
### Panel lateral de Ask AI {#ask-ai-side-panel}
DocSearch v4.5+ admite un **panel lateral de Ask AI** opcional. Cuando está habilitado, se puede abrir con **Ctrl/Cmd+I** por defecto. La [Referencia de API del Panel Lateral](https://docsearch.algolia.com/docs/sidepanel/api-reference) contiene la lista completa de opciones.
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// Refleja la API de @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // o 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
@ -287,132 +286,70 @@ export default defineConfig({
})
```
### Algolia Ask AI Support {#ask-ai}
Si deseas incluir **Ask AI**, pasa la opción `askAi` (o alguno de sus campos parciales) dentro de `options`:
Si necesitas deshabilitar el atajo de teclado, usa la opción `keyboardShortcuts` del panel lateral:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: 'TU-ASSISTANT-ID'
askAi: {
assistantId: 'XXXYYY'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
}
})
```
::: warning Nota
Si prefieres solo la búsqueda por palabra clave y no la Ask AI, simplemente omite `askAi`.
:::
#### Modo (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
[Estas opciones](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) se pueden superponer. Consulte la documentación oficial de Algolia para obtener más información sobre ellos.
Puedes controlar opcionalmente cómo VitePress integra la búsqueda por palabra clave y Ask AI:
### Configuración _Crawler_ {#crawler-config}
- `mode: 'auto'` (por defecto): infiere `hybrid` cuando la búsqueda por palabra clave está configurada, de lo contrario `sidePanel` cuando el panel lateral de Ask AI está configurado.
- `mode: 'sidePanel'`: fuerza solo el panel lateral (oculta el botón de búsqueda por palabra clave).
- `mode: 'hybrid'`: habilita el modal de búsqueda por palabra clave + panel lateral de Ask AI (requiere configuración de búsqueda por palabra clave).
- `mode: 'modal'`: mantiene Ask AI dentro del modal de DocSearch (incluso si configuraste el panel lateral).
A continuación se muestra un ejemplo de la configuración que utiliza este sitio:
#### Solo Ask AI (sin búsqueda por palabra clave) {#ask-ai-only}
Si quieres usar **solo el panel lateral de Ask AI**, puedes omitir la configuración de búsqueda por palabra clave de nivel superior y proporcionar las credenciales bajo `askAi`:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### Configuración _Crawler_ {#crawler-config}
A continuación se muestra un ejemplo de la configuración que utiliza este sitio:
<<< @/snippets/algolia-crawler.js

@ -182,7 +182,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'جستجوی مستندات',
translations: {
button: {
buttonText: 'جستجو',
@ -190,67 +189,139 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: 'پاک کردن جستجو',
clearButtonAriaLabel: 'پاک کردن جستجو',
clearButtonTitle: 'پاک کردن',
clearButtonAriaLabel: 'پاک کردن عبارت جستجو',
closeButtonText: 'بستن',
closeButtonAriaLabel: 'بستن',
placeholderText: 'جستجوی مستندات',
placeholderTextAskAi: 'از هوش مصنوعی بپرسید: ',
placeholderTextAskAiStreaming: 'در حال پاسخ...',
placeholderText: 'در مستندات جستجو کنید یا از Ask AI بپرسید',
placeholderTextAskAi: 'سؤال دیگری بپرسید...',
placeholderTextAskAiStreaming: 'در حال پاسخ گویی...',
searchInputLabel: 'جستجو',
backToKeywordSearchButtonText: 'بازگشت به جستجوی کلیدواژه',
backToKeywordSearchButtonAriaLabel: 'بازگشت به جستجوی کلیدواژه'
backToKeywordSearchButtonAriaLabel: 'بازگشت به جستجوی کلیدواژه',
newConversationPlaceholder: 'یک سؤال بپرسید',
conversationHistoryTitle: 'تاریخچه گفت وگوی من',
startNewConversationText: 'شروع گفت وگوی جدید',
viewConversationHistoryText: 'تاریخچه گفت وگو',
threadDepthErrorPlaceholder: 'محدودیت گفت وگو رسید'
},
startScreen: {
recentSearchesTitle: 'جستجوهای اخیر',
noRecentSearchesText: 'هیچ جستجوی اخیر',
saveRecentSearchButtonTitle: 'ذخیره در تاریخچه جستجو',
removeRecentSearchButtonTitle: 'حذف از تاریخچه جستجو',
favoriteSearchesTitle: 'علاقه‌مندی‌ها',
removeFavoriteSearchButtonTitle: 'حذف از علاقه‌مندی‌ها',
recentConversationsTitle: 'گفتگوهای اخیر',
removeRecentConversationButtonTitle: 'حذف این گفتگو از تاریخچه'
newConversation: {
newConversationTitle: 'امروز چگونه می توانم کمک کنم؟',
newConversationDescription:
'در مستندات شما جستجو می کنم تا سریع راهنماهای راه اندازی، جزئیات ویژگی ها و نکات رفع اشکال را پیدا کنم.'
},
footer: {
selectText: 'انتخاب',
submitQuestionText: 'ارسال سؤال',
selectKeyAriaLabel: 'کلید Enter',
navigateText: 'پیمایش',
navigateUpKeyAriaLabel: 'پیکان بالا',
navigateDownKeyAriaLabel: 'پیکان پایین',
closeText: 'بستن',
backToSearchText: 'بازگشت به جستجو',
closeKeyAriaLabel: 'کلید Escape',
poweredByText: 'قدرت گرفته از'
},
errorScreen: {
titleText: 'عدم امکان دریافت نتایج',
helpText: 'اتصال شبکه خود را بررسی کنید'
titleText: 'امکان دریافت نتایج وجود ندارد',
helpText: 'ممکن است لازم باشد اتصال شبکه را بررسی کنید.'
},
startScreen: {
recentSearchesTitle: 'اخیر',
noRecentSearchesText: 'جستجوی اخیر وجود ندارد',
saveRecentSearchButtonTitle: 'ذخیره این جستجو',
removeRecentSearchButtonTitle: 'حذف این جستجو از تاریخچه',
favoriteSearchesTitle: 'علاقه مندی ها',
removeFavoriteSearchButtonTitle: 'حذف این جستجو از علاقه مندی ها',
recentConversationsTitle: 'گفت وگوهای اخیر',
removeRecentConversationButtonTitle: 'حذف این گفت وگو از تاریخچه'
},
noResultsScreen: {
noResultsText: 'هیچ نتیجه‌ای یافت نشد',
suggestedQueryText: 'می‌توانید جستجوی دیگری امتحان کنید',
reportMissingResultsText: 'فکر می‌کنید باید نتیجه‌ای نمایش داده شود؟',
reportMissingResultsLinkText: 'برای ارسال بازخورد کلیک کنید'
noResultsText: 'هیچ نتیجه ای برای',
suggestedQueryText: 'سعی کنید جستجو کنید',
reportMissingResultsText:
'فکر می کنید این جستجو باید نتیجه داشته باشد؟',
reportMissingResultsLinkText: 'به ما اطلاع دهید.'
},
resultsScreen: {
askAiPlaceholder: 'از هوش مصنوعی بپرسید: '
askAiPlaceholder: 'از هوش مصنوعی بپرسید: ',
noResultsAskAiPlaceholder:
'در مستندات پیدا نکردید؟ از Ask AI کمک بگیرید: '
},
askAiScreen: {
disclaimerText:
'پاسخها توسط هوش مصنوعی تولید می‌شوند و ممکن است خطا داشته باشند. لطفاً بررسی کنید.',
'پاسخ ها توسط هوش مصنوعی تولید می شوند و ممکن است اشتباه باشند. بررسی کنید.',
relatedSourcesText: 'منابع مرتبط',
thinkingText: 'در حال پردازش...',
thinkingText: 'در حال فکر کردن...',
copyButtonText: 'کپی',
copyButtonCopiedText: 'کپی شد!',
copyButtonTitle: 'کپی',
likeButtonTitle: 'پسندیدم',
dislikeButtonTitle: 'نپسندیدم',
thanksForFeedbackText: 'از بازخورد شما سپاسگزاریم!',
thanksForFeedbackText: 'از بازخورد شما متشکریم!',
preToolCallText: 'در حال جستجو...',
duringToolCallText: 'در حال جستجو برای ',
afterToolCallText: 'جستجو انجام شد',
aggregatedToolCallText: 'جستجو انجام شد'
duringToolCallText: 'در حال جستجو...',
afterToolCallText: 'جستجو برای',
stoppedStreamingText: 'شما این پاسخ را متوقف کردید',
errorTitleText: 'خطای گفتگو',
threadDepthExceededMessage:
'برای حفظ دقت پاسخ ها، این گفت وگو بسته شد.',
startNewConversationButtonText: 'شروع گفت وگوی جدید'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'از هوش مصنوعی بپرسید',
buttonAriaLabel: 'از هوش مصنوعی بپرسید'
}
},
footer: {
selectText: 'انتخاب',
submitQuestionText: 'ارسال پرسش',
selectKeyAriaLabel: 'کلید Enter',
navigateText: 'حرکت',
navigateUpKeyAriaLabel: 'کلید جهت بالا',
navigateDownKeyAriaLabel: 'کلید جهت پایین',
closeText: 'بستن',
backToSearchText: 'بازگشت به جستجو',
closeKeyAriaLabel: 'کلید Escape',
poweredByText: 'جستجو توسط'
panel: {
translations: {
header: {
title: 'از هوش مصنوعی بپرسید',
conversationHistoryTitle: 'تاریخچه گفت وگوی من',
newConversationText: 'شروع گفت وگوی جدید',
viewConversationHistoryText: 'تاریخچه گفت وگو'
},
promptForm: {
promptPlaceholderText: 'یک سؤال بپرسید',
promptAnsweringText: 'در حال پاسخ گویی...',
promptAskAnotherQuestionText: 'سؤال دیگری بپرسید',
promptDisclaimerText:
'پاسخ ها توسط هوش مصنوعی تولید می شوند و ممکن است اشتباه باشند.',
promptLabelText:
'برای ارسال Enter را بزنید، یا برای خط جدید Shift+Enter.',
promptAriaLabelText: 'ورودی پرسش'
},
conversationScreen: {
preToolCallText: 'در حال جستجو...',
searchingText: 'در حال جستجو...',
toolCallResultText: 'جستجو برای',
conversationDisclaimer:
'پاسخ ها توسط هوش مصنوعی تولید می شوند و ممکن است اشتباه باشند. بررسی کنید.',
reasoningText: 'در حال استدلال...',
thinkingText: 'در حال فکر کردن...',
relatedSourcesText: 'منابع مرتبط',
stoppedStreamingText: 'شما این پاسخ را متوقف کردید',
copyButtonText: 'کپی',
copyButtonCopiedText: 'کپی شد!',
likeButtonTitle: 'پسندیدم',
dislikeButtonTitle: 'نپسندیدم',
thanksForFeedbackText: 'از بازخورد شما متشکریم!',
errorTitleText: 'خطای گفتگو'
},
newConversationScreen: {
titleText: 'امروز چگونه می توانم کمک کنم؟',
introductionText:
'در مستندات شما جستجو می کنم تا سریع راهنماهای راه اندازی، جزئیات ویژگی ها و نکات رفع اشکال را پیدا کنم.'
},
logo: {
poweredByText: 'قدرت گرفته از'
}
}
}
}
}

@ -39,25 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: { // اگر می‌خواهید زبان پیش‌فرض را ترجمه کنید، این را به `root` تغییر دهید
fa: { // اگر می خواهید زبان پیش فرض را ترجمه کنید، این را `root` قرار دهید
translations: {
button: {
buttonText: 'جستجو',
buttonAriaLabel: 'جستجو'
},
modal: {
displayDetails: 'نمایش جزئیات',
displayDetails: 'نمایش فهرست کامل',
resetButtonTitle: 'بازنشانی جستجو',
backButtonTitle: 'بستن جستجو',
noResultsText: 'نتیجهای یافت نشد',
noResultsText: 'نتیجه ای یافت نشد',
footer: {
selectText: 'انتخاب',
selectKeyAriaLabel: 'ورود',
selectKeyAriaLabel: 'Enter',
navigateText: 'پیمایش',
navigateUpKeyAriaLabel: 'کلید بالا',
navigateDownKeyAriaLabel: 'کلید پایین',
navigateUpKeyAriaLabel: 'فلش بالا',
navigateDownKeyAriaLabel: 'فلش پایین',
closeText: 'بستن',
closeKeyAriaLabel: 'esc'
closeKeyAriaLabel: 'Esc'
}
}
}
@ -123,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// بازگشت رشته HTML
// رشته HTML را برمی گرداند
}
}
}
@ -208,6 +208,19 @@ export default defineConfig({
می‌توانید با استفاده از تنظیماتی مانند این برای جستجوی چندزبانه استفاده کنید:
<details>
<summary>برای باز کردن کلیک کنید</summary>
<<< @/snippets/algolia-i18n.ts
</details>
برای اطلاعات بیشتر به [مستندات رسمی Algolia](https://docsearch.algolia.com/docs/api#translations) مراجعه کنید. برای شروع سریع‌تر، می‌توانید ترجمه‌های استفاده‌شده در این سایت را از [مخزن GitHub ما](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code) کپی کنید.
### پشتیبانی Algolia Ask AI {#ask-ai}
برای فعال‌سازی **Ask AI** کافی است گزینه `askAi` را اضافه کنید:
```ts
import { defineConfig } from 'vitepress'
@ -219,46 +232,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除搜索条件',
resetButtonAriaLabel: '清除搜索条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '最近搜索',
noRecentSearchesText: '没有最近搜索',
saveRecentSearchButtonTitle: '保存到最近搜索',
removeRecentSearchButtonTitle: '从最近搜索中删除'
},
errorScreen: {
titleText: '无法显示结果',
helpText: '您可能需要检查您的互联网连接'
},
footer: {
selectText: '选择',
navigateText: '导航',
closeText: '关闭',
searchByText: '搜索由'
},
noResultsScreen: {
noResultsText: '没有找到结果',
suggestedQueryText: '您可以尝试',
reportMissingResultsText: '您认为应该有结果吗?',
reportMissingResultsLinkText: '点击这里报告'
}
}
}
}
// askAi: "شناسه-دستیار-شما"
// یا
askAi: {
// حداقل باید assistantId دریافت شده از Algolia را ارائه کنید
assistantId: 'XXXYYY',
// بازنویسی های اختیاری — اگر حذف شوند، مقادیر appId/apiKey/indexName سطح بالا دوباره استفاده می شوند
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -266,131 +248,108 @@ export default defineConfig({
})
```
این [گزینه‌ها](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) می‌توانند بازنویسی شوند. برای یادگیری بیشتر درباره آن‌ها به اسناد رسمی Algolia مراجعه کنید.
::: warning نکته
اگر فقط به جستجوی کلمات کلیدی نیاز دارید، `askAi` را اضافه نکنید.
:::
### پیکربندی Crawler {#crawler-config}
### پنل کناری Ask AI {#ask-ai-side-panel}
در اینجا یک پیکربندی نمونه بر اساس آنچه که این سایت استفاده می‌کند آمده است:
DocSearch v4.5+ از **پنل کناری Ask AI** اختیاری پشتیبانی می‌کند. وقتی فعال باشد، به طور پیش‌فرض می‌توان آن را با **Ctrl/Cmd+I** باز کرد. [مرجع API پنل کناری](https://docsearch.algolia.com/docs/sidepanel/api-reference) شامل لیست کامل گزینه‌ها است.
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: '',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// آینه API @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // یا 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
})
```
اگر نیاز به غیرفعال کردن میانبر صفحه‌کلید دارید، از گزینه `keyboardShortcuts` پنل کناری استفاده کنید:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
### پشتیبانی Algolia Ask AI {#ask-ai}
#### حالت (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
برای فعال‌سازی **Ask AI** کافی است گزینه `askAi` را اضافه کنید:
می‌توانید به صورت اختیاری نحوه ادغام جستجوی کلمات کلیدی و Ask AI در VitePress را کنترل کنید:
- `mode: 'auto'` (پیش‌فرض): وقتی جستجوی کلمات کلیدی پیکربندی شده باشد `hybrid` را استنباط می‌کند، در غیر این صورت وقتی پنل کناری Ask AI پیکربندی شده باشد `sidePanel` را استنباط می‌کند.
- `mode: 'sidePanel'`: فقط پنل کناری را اعمال می‌کند (دکمه جستجوی کلمات کلیدی را پنهان می‌کند).
- `mode: 'hybrid'`: مودال جستجوی کلمات کلیدی + پنل کناری Ask AI را فعال می‌کند (نیاز به پیکربندی جستجوی کلمات کلیدی دارد).
- `mode: 'modal'`: Ask AI را درون مودال DocSearch نگه می‌دارد (حتی اگر پنل کناری را پیکربندی کرده باشید).
#### فقط Ask AI (بدون جستجوی کلمات کلیدی) {#ask-ai-only}
اگر می‌خواهید **فقط پنل کناری Ask AI** را استفاده کنید، می‌توانید پیکربندی جستجوی کلمات کلیدی سطح بالا را حذف کرده و اعتبارنامه‌ها را در `askAi` ارائه دهید:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
}
}
})
```
::: warning نکته
اگر فقط به جستجوی کلمات کلیدی نیاز دارید، `askAi` را اضافه نکنید.
:::
### پیکربندی Crawler {#crawler-config}
در اینجا یک پیکربندی نمونه بر اساس آنچه که این سایت استفاده می‌کند آمده است:
<<< @/snippets/algolia-crawler.js

@ -149,7 +149,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'ドキュメントを検索',
translations: {
button: {
buttonText: '検索',
@ -157,67 +156,138 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: '検索をクリア',
clearButtonAriaLabel: '検索をクリア',
clearButtonTitle: 'クリア',
clearButtonAriaLabel: 'クエリをクリア',
closeButtonText: '閉じる',
closeButtonAriaLabel: '閉じる',
placeholderText: 'ドキュメントを検索',
placeholderTextAskAi: 'AI に質問: ',
placeholderTextAskAiStreaming: '回答を作成中...',
placeholderText: 'ドキュメントを検索するか Ask AI に質問',
placeholderTextAskAi: '別の質問をする...',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '検索',
backToKeywordSearchButtonText: 'キーワード検索に戻る',
backToKeywordSearchButtonAriaLabel: 'キーワード検索に戻る'
backToKeywordSearchButtonAriaLabel: 'キーワード検索に戻る',
newConversationPlaceholder: '質問する',
conversationHistoryTitle: '自分の会話履歴',
startNewConversationText: '新しい会話を開始',
viewConversationHistoryText: '会話履歴',
threadDepthErrorPlaceholder: '会話上限に達しました'
},
newConversation: {
newConversationTitle: '今日はどのようにお手伝いできますか?',
newConversationDescription:
'ドキュメントを検索して、設定ガイド、機能の詳細、トラブルシューティングのヒントをすばやく見つけるお手伝いをします。'
},
footer: {
selectText: '選択',
submitQuestionText: '質問を送信',
selectKeyAriaLabel: 'Enter キー',
navigateText: '移動',
navigateUpKeyAriaLabel: '上矢印',
navigateDownKeyAriaLabel: '下矢印',
closeText: '閉じる',
backToSearchText: '検索に戻る',
closeKeyAriaLabel: 'Escape キー',
poweredByText: '提供'
},
errorScreen: {
titleText: '結果を取得できませんでした',
helpText: 'ネットワーク接続を確認してください。'
},
startScreen: {
recentSearchesTitle: '検索履歴',
recentSearchesTitle: '最近',
noRecentSearchesText: '最近の検索はありません',
saveRecentSearchButtonTitle: '検索履歴に保存',
removeRecentSearchButtonTitle: '検索履歴から削除',
saveRecentSearchButtonTitle: 'この検索を保存',
removeRecentSearchButtonTitle: '履歴からこの検索を削除',
favoriteSearchesTitle: 'お気に入り',
removeFavoriteSearchButtonTitle: 'お気に入りから削除',
removeFavoriteSearchButtonTitle: 'お気に入りからこの検索を削除',
recentConversationsTitle: '最近の会話',
removeRecentConversationButtonTitle: '会話履歴から削除'
},
errorScreen: {
titleText: '結果を取得できません',
helpText: 'ネットワーク接続を確認してください'
removeRecentConversationButtonTitle: '履歴からこの会話を削除'
},
noResultsScreen: {
noResultsText: '結果が見つかりません',
suggestedQueryText: '別の検索語を試してください',
reportMissingResultsText: '結果があるはずだと思いますか?',
reportMissingResultsLinkText: 'フィードバックを送る'
noResultsText: '次の検索結果はありません',
suggestedQueryText: '次を検索してみてください',
reportMissingResultsText:
'この検索には結果があるべきだと思いますか?',
reportMissingResultsLinkText: 'お知らせください。'
},
resultsScreen: {
askAiPlaceholder: 'AI に質問: '
askAiPlaceholder: 'AI に質問:',
noResultsAskAiPlaceholder:
'ドキュメントに見つかりませんでしたか? Ask AI に相談:'
},
askAiScreen: {
disclaimerText:
'AI が生成した回答には誤りが含まれる可能性があります。必ずご確認ください。',
'回答は AI により生成され、誤りが含まれる場合があります。内容をご確認ください。',
relatedSourcesText: '関連ソース',
thinkingText: '考え中...',
copyButtonText: 'コピー',
copyButtonCopiedText: 'コピーしました!',
copyButtonTitle: 'コピー',
likeButtonTitle: 'いいね',
dislikeButtonTitle: 'よくない',
dislikeButtonTitle: 'よくない',
thanksForFeedbackText: 'フィードバックありがとうございます!',
preToolCallText: '検索中...',
duringToolCallText: '検索中 ',
afterToolCallText: '検索完了',
aggregatedToolCallText: '検索完了'
duringToolCallText: '検索中...',
afterToolCallText: '検索しました',
stoppedStreamingText: 'この応答を停止しました',
errorTitleText: 'チャットエラー',
threadDepthExceededMessage:
'回答の正確性を保つため、この会話は終了しました。',
startNewConversationButtonText: '新しい会話を開始'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'AI に質問',
buttonAriaLabel: 'AI に質問'
}
},
footer: {
selectText: '選択',
submitQuestionText: '質問を送信',
selectKeyAriaLabel: 'Enter キー',
navigateText: '移動',
navigateUpKeyAriaLabel: '上矢印キー',
navigateDownKeyAriaLabel: '下矢印キー',
closeText: '閉じる',
backToSearchText: '検索に戻る',
closeKeyAriaLabel: 'Esc キー',
poweredByText: '提供: '
panel: {
translations: {
header: {
title: 'AI に質問',
conversationHistoryTitle: '自分の会話履歴',
newConversationText: '新しい会話を開始',
viewConversationHistoryText: '会話履歴'
},
promptForm: {
promptPlaceholderText: '質問する',
promptAnsweringText: '回答中...',
promptAskAnotherQuestionText: '別の質問をする',
promptDisclaimerText:
'回答は AI により生成され、誤りが含まれる場合があります。',
promptLabelText: 'Enterで送信、Shift+Enterで改行。',
promptAriaLabelText: 'プロンプト入力'
},
conversationScreen: {
preToolCallText: '検索中...',
searchingText: '検索中...',
toolCallResultText: '検索しました',
conversationDisclaimer:
'回答は AI により生成され、誤りが含まれる場合があります。内容をご確認ください。',
reasoningText: '推論中...',
thinkingText: '考え中...',
relatedSourcesText: '関連ソース',
stoppedStreamingText: 'この応答を停止しました',
copyButtonText: 'コピー',
copyButtonCopiedText: 'コピーしました!',
likeButtonTitle: 'いいね',
dislikeButtonTitle: 'よくないね',
thanksForFeedbackText: 'フィードバックありがとうございます!',
errorTitleText: 'チャットエラー'
},
newConversationScreen: {
titleText: '今日はどのようにお手伝いできますか?',
introductionText:
'ドキュメントを検索して、設定ガイド、機能の詳細、トラブルシューティングのヒントをすばやく見つけるお手伝いをします。'
},
logo: {
poweredByText: '提供'
}
}
}
}
}

@ -29,6 +29,7 @@ export default defineConfig({
- <https://www.npmjs.com/package/vitepress-plugin-search>
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
### i18n {#local-search-i18n}
@ -43,25 +44,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: { // 既定ロケールの文言も翻訳したい場合はこれを `root`
ja: { // 既定ロケールを翻訳する場合は `root` にしてください
translations: {
button: {
buttonText: '索',
buttonAriaLabel: '索'
buttonText: '索',
buttonAriaLabel: '索'
},
modal: {
displayDetails: '显示详细列表',
resetButtonTitle: '重置搜索',
backButtonTitle: '关闭搜索',
noResultsText: '没有结果',
displayDetails: '詳細一覧を表示',
resetButtonTitle: '検索をリセット',
backButtonTitle: '検索を閉じる',
noResultsText: '結果が見つかりません',
footer: {
selectText: '选择',
selectKeyAriaLabel: '输入',
navigateText: '导航',
navigateUpKeyAriaLabel: '上箭头',
navigateDownKeyAriaLabel: '下箭头',
closeText: '关闭',
closeKeyAriaLabel: 'esc'
selectText: '選択',
selectKeyAriaLabel: 'Enter',
navigateText: '移動',
navigateUpKeyAriaLabel: '上矢印',
navigateDownKeyAriaLabel: '下矢印',
closeText: '閉じる',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -73,7 +74,7 @@ export default defineConfig({
})
```
### miniSearch のオプション {#mini-search-options}
### miniSearch のオプション {#minisearch-options}
MiniSearch の設定例です。
@ -212,6 +213,19 @@ export default defineConfig({
多言語検索の設定例です。
<details>
<summary>クリックして展開</summary>
<<< @/snippets/algolia-i18n.ts
</details>
詳しくは[公式 Algolia ドキュメント](https://docsearch.algolia.com/docs/api#translations)を参照してください。すぐに始めるには、このサイトで使っている翻訳を[GitHub リポジトリ](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code)からコピーすることもできます。
### Algolia Ask AI のサポート {#ask-ai}
**Ask AI** を有効にするには、`options` 内に `askAi` オプション(またはその一部)を指定します。
```ts
import { defineConfig } from 'vitepress'
@ -223,79 +237,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: {
askAiPlaceholder: '向 AI 提问: '
},
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
}
}
// askAi: "あなたのアシスタントID"
// または
askAi: {
// 最低限、Algolia から受け取った assistantId を指定する必要があります
assistantId: 'XXXYYY',
// 任意の上書き — 省略した場合は上位の appId/apiKey/indexName を再利用
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -303,11 +253,13 @@ export default defineConfig({
})
```
[これらのオプション](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) は上書きできます。詳細は Algolia の公式ドキュメントを参照してください。
::: warning 注意
キーワード検索を既定にして Ask AI を使わない場合は、`askAi` を指定しないでください。
:::
### Algolia Ask AI のサポート {#ask-ai}
### Ask AI サイドパネル {#ask-ai-side-panel}
**Ask AI** を有効にするには、`options` 内に `askAi` オプション(またはその一部)を指定します。
DocSearch v4.5+ はオプションの **Ask AI サイドパネル**をサポートしています。有効にすると、デフォルトで **Ctrl/Cmd+I** で開くことができます。[サイドパネル API リファレンス](https://docsearch.algolia.com/docs/sidepanel/api-reference)にオプションの完全なリストがあります。
```ts
import { defineConfig } from 'vitepress'
@ -320,15 +272,18 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: "YOUR-ASSISTANT-ID"
// または
askAi: {
// 少なくとも Algolia から受け取った assistantId を指定
assistantId: 'XXXYYY',
// 任意の上書き — 省略時は上位の appId/apiKey/indexName を再利用
// apiKey: '...',
// appId: '...',
// indexName: '...'
sidePanel: {
// @docsearch/sidepanel-js SidepanelProps API をミラー
panel: {
variant: 'floating', // または 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
@ -336,116 +291,70 @@ export default defineConfig({
})
```
::: warning 注意
キーワード検索を既定にして Ask AI を使わない場合は、`askAi` を指定しないでください。
:::
キーボードショートカットを無効にする必要がある場合は、サイドパネルの `keyboardShortcuts` オプションを使用してください:
Ask AI UI の翻訳は `options.translations.modal.askAiScreen``options.translations.resultsScreen` にあります。すべてのキーは[型定義](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)を参照してください。
```ts
import { defineConfig } from 'vitepress'
### クローラー設定 {#crawler-config}
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
このサイトで使用している設定を元にした例です。
#### モード (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
VitePress がキーワード検索と Ask AI を統合する方法をオプションで制御できます:
- `mode: 'auto'`(デフォルト):キーワード検索が設定されている場合は `hybrid` を推論し、それ以外の場合は Ask AI サイドパネルが設定されている場合は `sidePanel` を推論します。
- `mode: 'sidePanel'`:サイドパネルのみを強制(キーワード検索ボタンを非表示)。
- `mode: 'hybrid'`:キーワード検索モーダル + Ask AI サイドパネルを有効化(キーワード検索設定が必要)。
- `mode: 'modal'`Ask AI を DocSearch モーダル内に保持(サイドパネルを設定した場合でも)。
#### Ask AI のみ(キーワード検索なし) {#ask-ai-only}
**Ask AI サイドパネルのみ**を使用する場合は、トップレベルのキーワード検索設定を省略し、`askAi` の下に認証情報を提供できます:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### クローラー設定 {#crawler-config}
このサイトで使用している設定を元にした例です。
<<< @/snippets/algolia-crawler.js

@ -223,7 +223,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: '문서 검색',
translations: {
button: {
buttonText: '검색',
@ -231,44 +230,67 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: '검색 지우기',
clearButtonAriaLabel: '검색 지우기',
clearButtonTitle: '지우기',
clearButtonAriaLabel: '검색 지우기',
closeButtonText: '닫기',
closeButtonAriaLabel: '닫기',
placeholderText: '문서 검색',
placeholderTextAskAi: 'AI에게 물어보기: ',
placeholderTextAskAiStreaming: '답변 작성 중...',
placeholderText: '문서 검색하거나 Ask AI에 질문',
placeholderTextAskAi: '다른 질문하기...',
placeholderTextAskAiStreaming: '답변 중...',
searchInputLabel: '검색',
backToKeywordSearchButtonText: '키워드 검색으로 돌아가기',
backToKeywordSearchButtonAriaLabel: '키워드 검색으로 돌아가기'
backToKeywordSearchButtonAriaLabel: '키워드 검색으로 돌아가기',
newConversationPlaceholder: '질문하기',
conversationHistoryTitle: '내 대화 기록',
startNewConversationText: '새 대화 시작',
viewConversationHistoryText: '대화 기록',
threadDepthErrorPlaceholder: '대화 한도에 도달했습니다'
},
newConversation: {
newConversationTitle: '오늘 무엇을 도와드릴까요?',
newConversationDescription:
'문서를 검색해 설정 가이드, 기능 설명, 문제 해결 팁을 빠르게 찾아드립니다.'
},
footer: {
selectText: '선택',
submitQuestionText: '질문 제출',
selectKeyAriaLabel: 'Enter 키',
navigateText: '이동',
navigateUpKeyAriaLabel: '위 화살표',
navigateDownKeyAriaLabel: '아래 화살표',
closeText: '닫기',
backToSearchText: '검색으로 돌아가기',
closeKeyAriaLabel: 'Escape 키',
poweredByText: '제공'
},
errorScreen: {
titleText: '결과를 불러올 수 없습니다',
helpText: '네트워크 연결을 확인해 주세요.'
},
startScreen: {
recentSearchesTitle: '검색 기록',
noRecentSearchesText: '최근 검색 없음',
saveRecentSearchButtonTitle: '검색 기록에 저장',
removeRecentSearchButtonTitle: '검색 기록에서 삭제',
recentSearchesTitle: '최근',
noRecentSearchesText: '최근 검색이 없습니다',
saveRecentSearchButtonTitle: '검색 저장',
removeRecentSearchButtonTitle: '기록에서 이 검색 제거',
favoriteSearchesTitle: '즐겨찾기',
removeFavoriteSearchButtonTitle: '즐겨찾기에서 삭제',
removeFavoriteSearchButtonTitle: '즐겨찾기에서 이 검색 제거',
recentConversationsTitle: '최근 대화',
removeRecentConversationButtonTitle: '대화를 기록에서 삭제'
},
errorScreen: {
titleText: '결과를 가져올 수 없습니다',
helpText: '네트워크 연결을 확인하세요'
removeRecentConversationButtonTitle: '기록에서 이 대화 제거'
},
noResultsScreen: {
noResultsText: '결과를 찾을 수 없습니다',
suggestedQueryText: '다른 검색어를 시도해 보세요',
reportMissingResultsText: '결과가 있어야 한다고 생각하나요?',
reportMissingResultsLinkText: '피드백 보내기'
noResultsText: '다음에 대한 결과를 찾을 수 없습니다',
suggestedQueryText: '다음을 검색해 보세요',
reportMissingResultsText: '이 검색은 결과가 있어야 하나요?',
reportMissingResultsLinkText: '알려주세요.'
},
resultsScreen: {
askAiPlaceholder: 'AI에게 물어보기: '
askAiPlaceholder: 'AI에게 묻기: ',
noResultsAskAiPlaceholder: '문서에서 찾지 못했나요? Ask AI에 문의: '
},
askAiScreen: {
disclaimerText:
'AI가 생성한 답변으로 오류가 있을 수 있습니다. 반드시 확인하세요.',
relatedSourcesText: '관련 소스',
'답변은 AI가 생성하며 오류가 있을 수 있습니다. 확인해 주세요.',
relatedSourcesText: '관련 출처',
thinkingText: '생각 중...',
copyButtonText: '복사',
copyButtonCopiedText: '복사됨!',
@ -277,21 +299,67 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
dislikeButtonTitle: '싫어요',
thanksForFeedbackText: '피드백 감사합니다!',
preToolCallText: '검색 중...',
duringToolCallText: '검색 중 ',
afterToolCallText: '검색 완료',
aggregatedToolCallText: '검색 완료'
duringToolCallText: '검색 중...',
afterToolCallText: '검색함',
stoppedStreamingText: '이 응답을 중지했습니다',
errorTitleText: '채팅 오류',
threadDepthExceededMessage:
'정확성을 유지하기 위해 이 대화는 종료되었습니다.',
startNewConversationButtonText: '새 대화 시작'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'AI에게 묻기',
buttonAriaLabel: 'AI에게 묻기'
}
},
footer: {
selectText: '선택',
submitQuestionText: '질문 보내기',
selectKeyAriaLabel: 'Enter 키',
navigateText: '탐색',
navigateUpKeyAriaLabel: '위쪽 화살표',
navigateDownKeyAriaLabel: '아래쪽 화살표',
closeText: '닫기',
backToSearchText: '검색으로 돌아가기',
closeKeyAriaLabel: 'Esc 키',
poweredByText: '제공: '
panel: {
translations: {
header: {
title: 'AI에게 묻기',
conversationHistoryTitle: '내 대화 기록',
newConversationText: '새 대화 시작',
viewConversationHistoryText: '대화 기록'
},
promptForm: {
promptPlaceholderText: '질문하기',
promptAnsweringText: '답변 중...',
promptAskAnotherQuestionText: '다른 질문하기',
promptDisclaimerText:
'답변은 AI가 생성하며 오류가 있을 수 있습니다.',
promptLabelText: 'Enter로 전송, Shift+Enter로 줄바꿈.',
promptAriaLabelText: '프롬프트 입력'
},
conversationScreen: {
preToolCallText: '검색 중...',
searchingText: '검색 중...',
toolCallResultText: '검색함',
conversationDisclaimer:
'답변은 AI가 생성하며 오류가 있을 수 있습니다. 확인해 주세요.',
reasoningText: '추론 중...',
thinkingText: '생각 중...',
relatedSourcesText: '관련 출처',
stoppedStreamingText: '이 응답을 중지했습니다',
copyButtonText: '복사',
copyButtonCopiedText: '복사됨!',
likeButtonTitle: '좋아요',
dislikeButtonTitle: '싫어요',
thanksForFeedbackText: '피드백 감사합니다!',
errorTitleText: '채팅 오류'
},
newConversationScreen: {
titleText: '오늘 무엇을 도와드릴까요?',
introductionText:
'문서를 검색해 설정 가이드, 기능 설명, 문제 해결 팁을 빠르게 찾아드립니다.'
},
logo: {
poweredByText: '제공'
}
}
}
}
}

@ -39,7 +39,7 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
ko: { // 기본 로케일을 번역하려면 이것을 `root`로 만드십시오.
ko: { // 기본 로케일을 번역하려면 `root`로 설정하세요
translations: {
button: {
buttonText: '검색',
@ -47,17 +47,17 @@ export default defineConfig({
},
modal: {
displayDetails: '상세 목록 표시',
resetButtonTitle: '검색 지우기',
resetButtonTitle: '검색 재설정',
backButtonTitle: '검색 닫기',
noResultsText: '결과를 찾을 수 없습니다',
noResultsText: '결과 없습니다',
footer: {
selectText: '선택',
selectKeyAriaLabel: '선택하기',
navigateText: '탐색',
navigateUpKeyAriaLabel: '위',
navigateDownKeyAriaLabel: '아래',
selectKeyAriaLabel: 'Enter',
navigateText: '이동',
navigateUpKeyAriaLabel: '위쪽 화살표',
navigateDownKeyAriaLabel: '아래쪽 화살표',
closeText: '닫기',
closeKeyAriaLabel: 'esc'
closeKeyAriaLabel: 'Esc'
}
}
}
@ -69,7 +69,7 @@ export default defineConfig({
})
```
### MiniSearch 옵션 {#mini-search-options}
### MiniSearch 옵션 {#minisearch-options}
MiniSearch를 다음과 같이 구성할 수 있습니다:
@ -123,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// return html string
// HTML 문자열을 반환
}
}
}
@ -208,6 +208,19 @@ export default defineConfig({
다국어 검색을 사용하려면 다음과 같이 구성해야 합니다:
<details>
<summary>클릭하여 펼치기</summary>
<<< @/snippets/algolia-i18n.ts
</details>
자세한 내용은 [공식 Algolia 문서](https://docsearch.algolia.com/docs/api#translations)를 참고하세요. 빠르게 시작하려면 이 사이트에서 사용하는 번역을 [GitHub 저장소](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code)에서 복사할 수도 있습니다.
### Algolia Ask AI 지원 {#ask-ai}
**Ask AI** 기능을 사용하려면 `askAi` 옵션을 추가하세요:
```ts
import { defineConfig } from 'vitepress'
@ -219,48 +232,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
ko: {
placeholder: '문서 검색',
translations: {
button: {
buttonText: '검색',
buttonAriaLabel: '검색'
},
modal: {
searchBox: {
resetButtonTitle: '검색 지우기',
resetButtonAriaLabel: '검색 지우기',
cancelButtonText: '취소',
cancelButtonAriaLabel: '취소'
},
startScreen: {
recentSearchesTitle: '검색 기록',
noRecentSearchesText: '최근 검색 없음',
saveRecentSearchButtonTitle: '검색 기록에 저장',
removeRecentSearchButtonTitle: '검색 기록에서 삭제',
favoriteSearchesTitle: '즐겨찾기',
removeFavoriteSearchButtonTitle: '즐겨찾기에서 삭제'
},
errorScreen: {
titleText: '결과를 가져올 수 없습니다',
helpText: '네트워크 연결을 확인하세요'
},
footer: {
selectText: '선택',
navigateText: '탐색',
closeText: '닫기',
searchByText: '검색 기준'
},
noResultsScreen: {
noResultsText: '결과를 찾을 수 없습니다',
suggestedQueryText: '새로운 검색을 시도할 수 있습니다',
reportMissingResultsText: '해당 검색어에 대한 결과가 있어야 합니까?',
reportMissingResultsLinkText: '피드백 보내기 클릭'
}
}
}
}
// askAi: "내-어시스턴트-ID"
// 또는
askAi: {
// 최소한 Algolia에서 받은 assistantId를 제공해야 합니다
assistantId: 'XXXYYY',
// 선택적 재정의 — 생략하면 상위 appId/apiKey/indexName 값이 재사용됩니다
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -268,129 +248,108 @@ export default defineConfig({
})
```
[이 옵션들](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)은 재작성 할 수 있습니다. 이에 대해 자세히 알고 싶다면 Algolia 공식 문서를 참고하세요.
::: warning 참고
Ask AI를 사용하지 않으려면 `askAi` 옵션을 생략하면 됩니다.
:::
### 크롤러 구성 {#crawler-config}
### Ask AI 사이드 패널 {#ask-ai-side-panel}
이 사이트에서 사용하는 예제 구성을 소개합니다:
DocSearch v4.5+는 선택적 **Ask AI 사이드 패널**을 지원합니다. 활성화되면 기본적으로 **Ctrl/Cmd+I**로 열 수 있습니다. [사이드 패널 API 참조](https://docsearch.algolia.com/docs/sidepanel/api-reference)에 전체 옵션 목록이 있습니다.
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// @docsearch/sidepanel-js SidepanelProps API 반영
panel: {
variant: 'floating', // 또는 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
})
```
키보드 단축키를 비활성화해야 하는 경우 사이드 패널의 `keyboardShortcuts` 옵션을 사용하세요:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
### Algolia Ask AI 지원 {#ask-ai}
#### 모드 (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
**Ask AI** 기능을 사용하려면 `askAi` 옵션을 추가하세요:
VitePress가 키워드 검색과 Ask AI를 통합하는 방식을 선택적으로 제어할 수 있습니다:
- `mode: 'auto'` (기본값): 키워드 검색이 구성된 경우 `hybrid`를 추론하고, 그렇지 않으면 Ask AI 사이드 패널이 구성된 경우 `sidePanel`을 추론합니다.
- `mode: 'sidePanel'`: 사이드 패널만 강제 (키워드 검색 버튼 숨김).
- `mode: 'hybrid'`: 키워드 검색 모달 + Ask AI 사이드 패널 활성화 (키워드 검색 구성 필요).
- `mode: 'modal'`: Ask AI를 DocSearch 모달 내부에 유지 (사이드 패널을 구성한 경우에도).
#### Ask AI만 (키워드 검색 없음) {#ask-ai-only}
**Ask AI 사이드 패널만** 사용하려면 최상위 키워드 검색 구성을 생략하고 `askAi` 아래에 자격 증명을 제공할 수 있습니다:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: { assistantId: 'XXXYYY' }
}
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
}
})
```
::: warning 참고
Ask AI를 사용하지 않으려면 `askAi` 옵션을 생략하면 됩니다.
:::
### 크롤러 구성 {#crawler-config}
이 사이트에서 사용하는 예제 구성을 소개합니다:
<<< @/snippets/algolia-crawler.js

@ -15,7 +15,7 @@
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.7.1",
"vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.6.5",
"vitepress-plugin-llms": "^1.9.3"
"vitepress-plugin-group-icons": "^1.7.1",
"vitepress-plugin-llms": "^1.10.0"
}
}

@ -178,53 +178,79 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Pesquisar documentos',
translations: {
button: {
buttonText: 'Pesquisar',
buttonAriaLabel: 'Pesquisar'
buttonText: 'Buscar',
buttonAriaLabel: 'Buscar'
},
modal: {
searchBox: {
clearButtonTitle: 'Limpar pesquisa',
clearButtonAriaLabel: 'Limpar pesquisa',
clearButtonTitle: 'Limpar',
clearButtonAriaLabel: 'Limpar a consulta',
closeButtonText: 'Fechar',
closeButtonAriaLabel: 'Fechar',
placeholderText: 'Pesquisar documentos',
placeholderTextAskAi: 'Pergunte à IA: ',
placeholderText: 'Buscar na documentação ou perguntar ao Ask AI',
placeholderTextAskAi: 'Faça outra pergunta...',
placeholderTextAskAiStreaming: 'Respondendo...',
searchInputLabel: 'Pesquisar',
backToKeywordSearchButtonText: 'Voltar à pesquisa por palavras-chave',
searchInputLabel: 'Buscar',
backToKeywordSearchButtonText:
'Voltar para a busca por palavra-chave',
backToKeywordSearchButtonAriaLabel:
'Voltar à pesquisa por palavras-chave'
'Voltar para a busca por palavra-chave',
newConversationPlaceholder: 'Faça uma pergunta',
conversationHistoryTitle: 'Meu histórico de conversas',
startNewConversationText: 'Iniciar uma nova conversa',
viewConversationHistoryText: 'Histórico de conversas',
threadDepthErrorPlaceholder: 'Limite de conversa atingido'
},
newConversation: {
newConversationTitle: 'Como posso ajudar hoje?',
newConversationDescription:
'Eu busco na sua documentação para ajudar a encontrar guias de configuração, detalhes de funcionalidades e dicas de solução de problemas rapidamente.'
},
footer: {
selectText: 'Selecionar',
submitQuestionText: 'Enviar pergunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Seta para cima',
navigateDownKeyAriaLabel: 'Seta para baixo',
closeText: 'Fechar',
backToSearchText: 'Voltar à busca',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Com tecnologia de'
},
errorScreen: {
titleText: 'Não foi possível obter resultados',
helpText: 'Talvez você queira verificar sua conexão de rede.'
},
startScreen: {
recentSearchesTitle: 'Histórico de pesquisa',
recentSearchesTitle: 'Recentes',
noRecentSearchesText: 'Nenhuma pesquisa recente',
saveRecentSearchButtonTitle: 'Salvar no histórico de pesquisas',
removeRecentSearchButtonTitle: 'Remover do histórico de pesquisas',
saveRecentSearchButtonTitle: 'Salvar esta pesquisa',
removeRecentSearchButtonTitle: 'Remover esta pesquisa do histórico',
favoriteSearchesTitle: 'Favoritos',
removeFavoriteSearchButtonTitle: 'Remover dos favoritos',
removeFavoriteSearchButtonTitle:
'Remover esta pesquisa dos favoritos',
recentConversationsTitle: 'Conversas recentes',
removeRecentConversationButtonTitle:
'Remover esta conversa do histórico'
},
errorScreen: {
titleText: 'Não foi possível obter resultados',
helpText: 'Verifique sua conexão de rede'
},
noResultsScreen: {
noResultsText: 'Nenhum resultado encontrado',
suggestedQueryText: 'Você pode tentar uma nova consulta',
reportMissingResultsText: 'Acha que deveria haver resultados?',
reportMissingResultsLinkText: 'Clique para enviar feedback'
noResultsText: 'Nenhum resultado encontrado para',
suggestedQueryText: 'Tente pesquisar por',
reportMissingResultsText:
'Acha que esta consulta deveria retornar resultados?',
reportMissingResultsLinkText: 'Avise-nos.'
},
resultsScreen: {
askAiPlaceholder: 'Pergunte à IA: '
askAiPlaceholder: 'Perguntar à IA: ',
noResultsAskAiPlaceholder:
'Não encontrou nos documentos? Peça ajuda ao Ask AI: '
},
askAiScreen: {
disclaimerText:
'As respostas são geradas por IA e podem conter erros. Verifique as respostas.',
'As respostas são geradas por IA e podem conter erros. Verifique.',
relatedSourcesText: 'Fontes relacionadas',
thinkingText: 'Pensando...',
copyButtonText: 'Copiar',
@ -232,23 +258,70 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
copyButtonTitle: 'Copiar',
likeButtonTitle: 'Curtir',
dislikeButtonTitle: 'Não curtir',
thanksForFeedbackText: 'Obrigado pelo feedback!',
preToolCallText: 'Pesquisando...',
duringToolCallText: 'Pesquisando ',
afterToolCallText: 'Pesquisa concluída',
aggregatedToolCallText: 'Pesquisa concluída'
thanksForFeedbackText: 'Obrigado pelo seu feedback!',
preToolCallText: 'Buscando...',
duringToolCallText: 'Buscando...',
afterToolCallText: 'Pesquisado',
stoppedStreamingText: 'Você interrompeu esta resposta',
errorTitleText: 'Erro no chat',
threadDepthExceededMessage:
'Esta conversa foi encerrada para manter respostas precisas.',
startNewConversationButtonText: 'Iniciar uma nova conversa'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'Perguntar à IA',
buttonAriaLabel: 'Perguntar à IA'
}
},
footer: {
selectText: 'Selecionar',
submitQuestionText: 'Enviar pergunta',
selectKeyAriaLabel: 'Tecla Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Seta para cima',
navigateDownKeyAriaLabel: 'Seta para baixo',
closeText: 'Fechar',
backToSearchText: 'Voltar à pesquisa',
closeKeyAriaLabel: 'Tecla Escape',
poweredByText: 'Pesquisa por'
panel: {
translations: {
header: {
title: 'Perguntar à IA',
conversationHistoryTitle: 'Meu histórico de conversas',
newConversationText: 'Iniciar uma nova conversa',
viewConversationHistoryText: 'Histórico de conversas'
},
promptForm: {
promptPlaceholderText: 'Faça uma pergunta',
promptAnsweringText: 'Respondendo...',
promptAskAnotherQuestionText: 'Faça outra pergunta',
promptDisclaimerText:
'As respostas são geradas por IA e podem conter erros.',
promptLabelText:
'Pressione Enter para enviar ou Shift+Enter para nova linha.',
promptAriaLabelText: 'Entrada do prompt'
},
conversationScreen: {
preToolCallText: 'Buscando...',
searchingText: 'Buscando...',
toolCallResultText: 'Pesquisado',
conversationDisclaimer:
'As respostas são geradas por IA e podem conter erros. Verifique.',
reasoningText: 'Raciocinando...',
thinkingText: 'Pensando...',
relatedSourcesText: 'Fontes relacionadas',
stoppedStreamingText: 'Você interrompeu esta resposta',
copyButtonText: 'Copiar',
copyButtonCopiedText: 'Copiado!',
likeButtonTitle: 'Curtir',
dislikeButtonTitle: 'Não curtir',
thanksForFeedbackText: 'Obrigado pelo seu feedback!',
errorTitleText: 'Erro no chat'
},
newConversationScreen: {
titleText: 'Como posso ajudar hoje?',
introductionText:
'Eu busco na sua documentação para ajudar a encontrar guias de configuração, detalhes de funcionalidades e dicas de solução de problemas rapidamente.'
},
logo: {
poweredByText: 'Com tecnologia de'
}
}
}
}
}

@ -39,18 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: {
pt: { // torne isto `root` se quiser traduzir a localidade padrão
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: 'Pesquisar',
buttonAriaLabel: 'Pesquisar'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
displayDetails: 'Mostrar lista detalhada',
resetButtonTitle: 'Redefinir pesquisa',
backButtonTitle: 'Fechar pesquisa',
noResultsText: 'Nenhum resultado',
footer: {
selectText: '选择',
navigateText: '切换'
selectText: 'Selecionar',
selectKeyAriaLabel: 'Enter',
navigateText: 'Navegar',
navigateUpKeyAriaLabel: 'Seta para cima',
navigateDownKeyAriaLabel: 'Seta para baixo',
closeText: 'Fechar',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -62,7 +69,7 @@ export default defineConfig({
})
```
### Opções MiniSearch {#mini-search-options}
### Opções MiniSearch {#minisearch-options}
Você pode configurar o MiniSearch assim:
@ -116,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// retorne a string HTML
// retorna uma string HTML
}
}
}
@ -141,7 +148,7 @@ export default defineConfig({
async _render(src, env, md) {
const html = await md.renderAsync(src, env)
if (env.frontmatter?.search === false) return ''
if (env.relativePath.startsWith('algum/caminho')) return ''
if (env.relativePath.startsWith('some/path')) return ''
return html
}
}
@ -197,10 +204,23 @@ export default defineConfig({
})
```
### i18n {#algolia-search-i18n} {#algolia-search-i18n}
### i18n {#algolia-search-i18n}
Você pode usar uma configuração como esta para usar a pesquisa multilínguas:
<details>
<summary>Clique para expandir</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Consulte a [documentação oficial da Algolia](https://docsearch.algolia.com/docs/api#translations) para saber mais. Para começar rapidamente, você também pode copiar as traduções usadas por este site do [nosso repositório no GitHub](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Suporte ao Algolia Ask AI {#ask-ai}
Se quiser incluir o **Ask AI**, adicione `askAi` em `options`:
```ts
import { defineConfig } from 'vitepress'
@ -212,48 +232,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}
// askAi: "SEU-ID-DO-ASSISTENTE"
// OU
askAi: {
// no mínimo, você deve fornecer o assistantId recebido da Algolia
assistantId: 'XXXYYY',
// substituições opcionais — se omitidas, os valores appId/apiKey/indexName de nível superior são reutilizados
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -261,129 +248,108 @@ export default defineConfig({
})
```
[Essas opções](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) podem ser sobrepostas. Consulte a documentação oficial Algolia para obter mais informações sobre elas.
::: warning Nota
Caso queira apenas a pesquisa por palavra-chave, omita `askAi`.
:::
### Configuração _Crawler_ {#crawler-config}
### Painel Lateral do Ask AI {#ask-ai-side-panel}
Aqui está um exemplo de configuração baseado na qual este site usa:
O DocSearch v4.5+ suporta um **painel lateral do Ask AI** opcional. Quando habilitado, pode ser aberto com **Ctrl/Cmd+I** por padrão. A [Referência da API do Painel Lateral](https://docsearch.algolia.com/docs/sidepanel/api-reference) contém a lista completa de opções.
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// Espelha a API do @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // ou 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
})
```
Se precisar desabilitar o atalho de teclado, use a opção `keyboardShortcuts` do painel lateral:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
### Suporte ao Algolia Ask AI {#ask-ai}
#### Modo (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
Se quiser incluir o **Ask AI**, adicione `askAi` em `options`:
Você pode controlar opcionalmente como o VitePress integra a pesquisa por palavra-chave e o Ask AI:
- `mode: 'auto'` (padrão): infere `hybrid` quando a pesquisa por palavra-chave está configurada, caso contrário `sidePanel` quando o painel lateral do Ask AI está configurado.
- `mode: 'sidePanel'`: força apenas o painel lateral (oculta o botão de pesquisa por palavra-chave).
- `mode: 'hybrid'`: habilita o modal de pesquisa por palavra-chave + painel lateral do Ask AI (requer configuração de pesquisa por palavra-chave).
- `mode: 'modal'`: mantém o Ask AI dentro do modal do DocSearch (mesmo se você configurou o painel lateral).
#### Apenas Ask AI (sem pesquisa por palavra-chave) {#ask-ai-only}
Se quiser usar **apenas o painel lateral do Ask AI**, você pode omitir a configuração de pesquisa por palavra-chave de nível superior e fornecer as credenciais em `askAi`:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: { assistantId: 'XXXYYY' }
}
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
}
})
```
::: warning Nota
Caso queira apenas a pesquisa por palavra-chave, omita `askAi`.
:::
### Configuração _Crawler_ {#crawler-config}
Aqui está um exemplo de configuração baseado na qual este site usa:
<<< @/snippets/algolia-crawler.js

@ -178,7 +178,6 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: 'Поиск в документации',
translations: {
button: {
buttonText: 'Поиск',
@ -186,45 +185,70 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
},
modal: {
searchBox: {
clearButtonTitle: 'Очистить поиск',
clearButtonAriaLabel: 'Очистить поиск',
clearButtonTitle: 'Очистить',
clearButtonAriaLabel: 'Очистить запрос',
closeButtonText: 'Закрыть',
closeButtonAriaLabel: 'Закрыть',
placeholderText: 'Поиск в документации',
placeholderTextAskAi: 'Задайте вопрос ИИ: ',
placeholderTextAskAiStreaming: 'Формируется ответ...',
placeholderText: 'Поиск по документации или задайте вопрос Ask AI',
placeholderTextAskAi: 'Задайте другой вопрос...',
placeholderTextAskAiStreaming: 'Отвечаю...',
searchInputLabel: 'Поиск',
backToKeywordSearchButtonText:
'Вернуться к поиску по ключевым словам',
backToKeywordSearchButtonText: 'Назад к поиску по ключевым словам',
backToKeywordSearchButtonAriaLabel:
'Вернуться к поиску по ключевым словам'
'Назад к поиску по ключевым словам',
newConversationPlaceholder: 'Задайте вопрос',
conversationHistoryTitle: 'Моя история разговоров',
startNewConversationText: 'Начать новый разговор',
viewConversationHistoryText: 'История разговоров',
threadDepthErrorPlaceholder: 'Достигнут лимит разговора'
},
startScreen: {
recentSearchesTitle: 'История поиска',
noRecentSearchesText: 'Нет истории поиска',
saveRecentSearchButtonTitle: 'Сохранить в истории поиска',
removeRecentSearchButtonTitle: 'Удалить из истории поиска',
favoriteSearchesTitle: 'Избранное',
removeFavoriteSearchButtonTitle: 'Удалить из избранного',
recentConversationsTitle: 'Недавние диалоги',
removeRecentConversationButtonTitle: 'Удалить этот диалог из истории'
newConversation: {
newConversationTitle: 'Чем могу помочь сегодня?',
newConversationDescription:
'Я ищу по вашей документации, чтобы быстро помочь найти руководства по настройке, детали функций и советы по устранению неполадок.'
},
footer: {
selectText: 'Выбрать',
submitQuestionText: 'Отправить вопрос',
selectKeyAriaLabel: 'Клавиша Enter',
navigateText: 'Навигация',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'Закрыть',
backToSearchText: 'Назад к поиску',
closeKeyAriaLabel: 'Клавиша Escape',
poweredByText: 'При поддержке'
},
errorScreen: {
titleText: 'Невозможно получить результаты',
helpText: 'Проверьте подключение к Интернету'
titleText: 'Не удалось получить результаты',
helpText: 'Возможно, стоит проверить подключение к сети.'
},
startScreen: {
recentSearchesTitle: 'Недавние',
noRecentSearchesText: 'Нет недавних поисков',
saveRecentSearchButtonTitle: 'Сохранить этот поиск',
removeRecentSearchButtonTitle: 'Удалить этот поиск из истории',
favoriteSearchesTitle: 'Избранное',
removeFavoriteSearchButtonTitle: 'Удалить этот поиск из избранного',
recentConversationsTitle: 'Недавние разговоры',
removeRecentConversationButtonTitle:
'Удалить этот разговор из истории'
},
noResultsScreen: {
noResultsText: 'Ничего не найдено',
suggestedQueryText: 'Попробуйте изменить запрос',
reportMissingResultsText: 'Считаете, что результаты должны быть?',
reportMissingResultsLinkText: 'Сообщите об этом'
noResultsText: 'Не найдено результатов для',
suggestedQueryText: 'Попробуйте поискать',
reportMissingResultsText:
'Считаете, что по этому запросу должны быть результаты?',
reportMissingResultsLinkText: 'Сообщите нам.'
},
resultsScreen: {
askAiPlaceholder: 'Задайте вопрос ИИ: '
askAiPlaceholder: 'Спросить ИИ: ',
noResultsAskAiPlaceholder:
'Не нашли в документации? Попросите Ask AI помочь: '
},
askAiScreen: {
disclaimerText:
'Ответы генерируются ИИ и могут содержать ошибки. Проверяйте информацию.',
'Ответы генерируются ИИ и могут содержать ошибки. Проверьте их.',
relatedSourcesText: 'Связанные источники',
thinkingText: 'Думаю...',
copyButtonText: 'Копировать',
@ -233,22 +257,69 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
likeButtonTitle: 'Нравится',
dislikeButtonTitle: 'Не нравится',
thanksForFeedbackText: 'Спасибо за отзыв!',
preToolCallText: 'Поиск...',
duringToolCallText: 'Поиск ',
afterToolCallText: 'Поиск завершён',
aggregatedToolCallText: 'Поиск завершён'
preToolCallText: 'Ищу...',
duringToolCallText: 'Ищу...',
afterToolCallText: 'Искал',
stoppedStreamingText: 'Вы остановили этот ответ',
errorTitleText: 'Ошибка чата',
threadDepthExceededMessage:
'Этот разговор закрыт, чтобы сохранить точность ответов.',
startNewConversationButtonText: 'Начать новый разговор'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: 'Спросить ИИ',
buttonAriaLabel: 'Спросить ИИ'
}
},
footer: {
selectText: 'выбрать',
submitQuestionText: 'Отправить вопрос',
selectKeyAriaLabel: 'Клавиша Enter',
navigateText: 'перейти',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'закрыть',
backToSearchText: 'Вернуться к поиску',
closeKeyAriaLabel: 'Клавиша Esc',
poweredByText: 'поиск от'
panel: {
translations: {
header: {
title: 'Спросить ИИ',
conversationHistoryTitle: 'Моя история разговоров',
newConversationText: 'Начать новый разговор',
viewConversationHistoryText: 'История разговоров'
},
promptForm: {
promptPlaceholderText: 'Задайте вопрос',
promptAnsweringText: 'Отвечаю...',
promptAskAnotherQuestionText: 'Задайте другой вопрос',
promptDisclaimerText:
'Ответы генерируются ИИ и могут содержать ошибки.',
promptLabelText:
'Нажмите Enter, чтобы отправить, или Shift+Enter для новой строки.',
promptAriaLabelText: 'Ввод запроса'
},
conversationScreen: {
preToolCallText: 'Ищу...',
searchingText: 'Ищу...',
toolCallResultText: 'Искал',
conversationDisclaimer:
'Ответы генерируются ИИ и могут содержать ошибки. Проверьте их.',
reasoningText: 'Рассуждаю...',
thinkingText: 'Думаю...',
relatedSourcesText: 'Связанные источники',
stoppedStreamingText: 'Вы остановили этот ответ',
copyButtonText: 'Копировать',
copyButtonCopiedText: 'Скопировано!',
likeButtonTitle: 'Нравится',
dislikeButtonTitle: 'Не нравится',
thanksForFeedbackText: 'Спасибо за отзыв!',
errorTitleText: 'Ошибка чата'
},
newConversationScreen: {
titleText: 'Чем могу помочь сегодня?',
introductionText:
'Я ищу по вашей документации, чтобы быстро помочь найти руководства по настройке, детали функций и советы по устранению неполадок.'
},
logo: {
poweredByText: 'При поддержке'
}
}
}
}
}

@ -29,6 +29,7 @@ export default defineConfig({
- <https://www.npmjs.com/package/vitepress-plugin-search>
- <https://www.npmjs.com/package/vitepress-plugin-pagefind>
- <https://www.npmjs.com/package/@orama/plugin-vitepress>
- <https://www.npmjs.com/package/vitepress-plugin-typesense>
### i18n {#local-search-i18n}
@ -43,25 +44,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
ru: { // используйте ключ `root`, если хотите перевести локаль по умолчанию
ru: { // используйте `root`, если хотите перевести локаль по умолчанию
translations: {
button: {
buttonText: 'Поиск',
buttonAriaLabel: 'Поиск'
},
modal: {
displayDetails: 'Отобразить подробный список',
displayDetails: 'Показать подробный список',
resetButtonTitle: 'Сбросить поиск',
backButtonTitle: 'Закрыть поиск',
noResultsText: 'Нет результатов по запросу',
noResultsText: 'Нет результатов',
footer: {
selectText: 'выбрать',
selectKeyAriaLabel: 'выбрать',
navigateText: 'перейти',
navigateUpKeyAriaLabel: 'стрелка вверх',
navigateDownKeyAriaLabel: 'стрелка вниз',
closeText: 'закрыть',
closeKeyAriaLabel: 'esc'
selectText: 'Выбрать',
selectKeyAriaLabel: 'Enter',
navigateText: 'Навигация',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'Закрыть',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -127,7 +128,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// возвращаем html
// вернуть строку HTML
}
}
}
@ -212,6 +213,19 @@ export default defineConfig({
Пример конфигурации для использования многоязычного поиска:
<details>
<summary>Нажмите, чтобы развернуть</summary>
<<< @/snippets/algolia-i18n.ts
</details>
Подробности см. в [официальной документации Algolia](https://docsearch.algolia.com/docs/api#translations). Чтобы быстрее начать, можно также скопировать переводы, используемые на этом сайте, из [нашего репозитория GitHub](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code).
### Поддержка Ask AI в Algolia {#ask-ai}
Если вы хотите добавить функцию **Ask AI**, передайте параметр `askAi` (или любые из его отдельных полей) внутри объекта `options`:
```ts
import { defineConfig } from 'vitepress'
@ -223,79 +237,15 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
ru: {
placeholder: 'Поиск в документации',
translations: {
button: {
buttonText: 'Поиск',
buttonAriaLabel: 'Поиск'
},
modal: {
searchBox: {
clearButtonTitle: 'Очистить поиск',
clearButtonAriaLabel: 'Очистить поиск',
closeButtonText: 'Закрыть',
closeButtonAriaLabel: 'Закрыть',
placeholderText: 'Поиск в документации',
placeholderTextAskAi: 'Задайте вопрос ИИ:',
placeholderTextAskAiStreaming: 'Формируется ответ...',
searchInputLabel: 'Поиск',
backToKeywordSearchButtonText: 'Вернуться к поиску по ключевым словам',
backToKeywordSearchButtonAriaLabel: 'Вернуться к поиску по ключевым словам'
},
startScreen: {
recentSearchesTitle: 'История поиска',
noRecentSearchesText: 'Нет истории поиска',
saveRecentSearchButtonTitle: 'Сохранить в истории поиска',
removeRecentSearchButtonTitle: 'Удалить из истории поиска',
favoriteSearchesTitle: 'Избранное',
removeFavoriteSearchButtonTitle: 'Удалить из избранного',
recentConversationsTitle: 'Последние диалоги',
removeRecentConversationButtonTitle: 'Удалить диалог из истории'
},
errorScreen: {
titleText: 'Невозможно получить результаты',
helpText: 'Проверьте подключение к Интернету'
},
noResultsScreen: {
noResultsText: 'Ничего не найдено',
suggestedQueryText: 'Попробуйте изменить запрос',
reportMissingResultsText: 'Считаете, что результаты должны быть?',
reportMissingResultsLinkText: 'Сообщите об этом'
},
resultsScreen: {
askAiPlaceholder: 'Задайте вопрос ИИ: '
},
askAiScreen: {
disclaimerText: 'Ответ сгенерирован ИИ и может быть неточным. Пожалуйста, проверьте информацию самостоятельно.',
relatedSourcesText: 'Связанные источники',
thinkingText: 'Думаю...',
copyButtonText: 'Копировать',
copyButtonCopiedText: 'Скопировано!',
copyButtonTitle: 'Копировать',
likeButtonTitle: 'Нравится',
dislikeButtonTitle: 'Не нравится',
thanksForFeedbackText: 'Спасибо за ваш отзыв!',
preToolCallText: 'Идёт поиск...',
duringToolCallText: 'Поиск ',
afterToolCallText: 'Поиск выполнен'
},
footer: {
selectText: 'выбрать',
submitQuestionText: 'Отправить вопрос',
selectKeyAriaLabel: 'Клавиша Enter',
navigateText: 'перейти',
navigateUpKeyAriaLabel: 'Стрелка вверх',
navigateDownKeyAriaLabel: 'Стрелка вниз',
closeText: 'закрыть',
backToSearchText: 'Вернуться к поиску',
closeKeyAriaLabel: 'Клавиша Esc',
poweredByText: 'поиск от'
}
}
}
}
// askAi: "ВАШ-ID-АССИСТЕНТА"
// ИЛИ
askAi: {
// как минимум нужно указать assistantId, полученный от Algolia
assistantId: 'XXXYYY',
// необязательные переопределения — если их нет, используются значения appId/apiKey/indexName верхнего уровня
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
@ -303,11 +253,13 @@ export default defineConfig({
})
```
[Эти параметры](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) можно переопределить. Чтобы узнать о них больше, обратитесь к официальной документации Algolia.
::: warning Примечание
Если вы хотите использовать обычный поиск по ключевым словам без Ask AI, просто не указывайте свойство `askAi`
:::
### Поддержка Ask AI в Algolia {#ask-ai}
### Боковая панель Ask AI {#ask-ai-side-panel}
Если вы хотите добавить функцию **Ask AI**, передайте параметр `askAi` (или любые из его отдельных полей) внутри объекта `options`:
DocSearch v4.5+ поддерживает опциональную **боковую панель Ask AI**. Когда она включена, её можно открыть с помощью **Ctrl/Cmd+I** по умолчанию. [Справочник API боковой панели](https://docsearch.algolia.com/docs/sidepanel/api-reference) содержит полный список опций.
```ts
import { defineConfig } from 'vitepress'
@ -320,15 +272,18 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
// askAi: "ID-ВАШЕГО-АССИСТЕНТА"
// ИЛИ
askAi: {
// минимум вы должны указать assistantId, полученный от Algolia
assistantId: 'XXXYYY',
// опциональные переопределения если не указаны, используются значения appId/apiKey/indexName верхнего уровня
// apiKey: '...',
// appId: '...',
// indexName: '...'
sidePanel: {
// Отражает API @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // или 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
}
}
@ -336,116 +291,70 @@ export default defineConfig({
})
```
::: warning Примечание
Если вы хотите использовать обычный поиск по ключевым словам без Ask AI, просто не указывайте свойство `askAi`
:::
Если вам нужно отключить сочетание клавиш, используйте опцию `keyboardShortcuts` боковой панели:
Переводы для интерфейса Ask AI находятся в `options.translations.modal.askAiScreen` и `options.translations.resultsScreen` — полный список ключей смотрите в [типах](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts).
```ts
import { defineConfig } from 'vitepress'
### Конфигурация поискового робота {#crawler-config}
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
})
```
Вот пример конфигурации, основанной на той, что используется на этом сайте:
#### Режим (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
Вы можете опционально контролировать, как VitePress интегрирует поиск по ключевым словам и Ask AI:
- `mode: 'auto'` (по умолчанию): выводит `hybrid`, когда настроен поиск по ключевым словам, иначе `sidePanel`, когда настроена боковая панель Ask AI.
- `mode: 'sidePanel'`: принудительно использовать только боковую панель (скрывает кнопку поиска по ключевым словам).
- `mode: 'hybrid'`: включает модальное окно поиска по ключевым словам + боковую панель Ask AI (требует настройки поиска по ключевым словам).
- `mode: 'modal'`: сохраняет Ask AI внутри модального окна DocSearch (даже если вы настроили боковую панель).
#### Только Ask AI (без поиска по ключевым словам) {#ask-ai-only}
Если вы хотите использовать **только боковую панель Ask AI**, вы можете опустить конфигурацию поиска по ключевым словам верхнего уровня и предоставить учётные данные в `askAi`:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
```
### Конфигурация поискового робота {#crawler-config}
Вот пример конфигурации, основанной на той, что используется на этом сайте:
<<< @/snippets/algolia-crawler.js

@ -0,0 +1,101 @@
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})

@ -0,0 +1,155 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
translations: {
button: {
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
searchBox: {
clearButtonTitle: '清除',
clearButtonAriaLabel: '清除查询',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档或向 AI 提问',
placeholderTextAskAi: '再问一个问题...',
placeholderTextAskAiStreaming: '正在回答...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键词搜索',
backToKeywordSearchButtonAriaLabel: '返回关键词搜索',
newConversationPlaceholder: '提问',
conversationHistoryTitle: '我的对话历史',
startNewConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史',
threadDepthErrorPlaceholder: '对话已达上限'
},
newConversation: {
newConversationTitle: '我今天能帮你什么?',
newConversationDescription:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: '回车键',
navigateText: '导航',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '由…提供支持'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查网络连接。'
},
startScreen: {
recentSearchesTitle: '最近',
noRecentSearchesText: '暂无最近搜索',
saveRecentSearchButtonTitle: '保存此搜索',
removeRecentSearchButtonTitle: '从历史记录中移除此搜索',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除此搜索',
recentConversationsTitle: '最近对话',
removeRecentConversationButtonTitle: '从历史记录中移除此对话'
},
noResultsScreen: {
noResultsText: '未找到相关结果',
suggestedQueryText: '尝试搜索',
reportMissingResultsText: '认为此查询应该有结果?',
reportMissingResultsLinkText: '告诉我们。'
},
resultsScreen: {
askAiPlaceholder: '询问 AI',
noResultsAskAiPlaceholder: '文档里没找到?让 Ask AI 帮忙:'
},
askAiScreen: {
disclaimerText: '回答由 AI 生成,可能会出错。请核实。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索中...',
afterToolCallText: '已搜索',
stoppedStreamingText: '你已停止此回复',
errorTitleText: '聊天错误',
threadDepthExceededMessage: '为保持回答准确,此对话已关闭。',
startNewConversationButtonText: '开始新的对话'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: '询问 AI',
buttonAriaLabel: '询问 AI'
}
},
panel: {
translations: {
header: {
title: '询问 AI',
conversationHistoryTitle: '我的对话历史',
newConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史'
},
promptForm: {
promptPlaceholderText: '提问',
promptAnsweringText: '正在回答...',
promptAskAnotherQuestionText: '再问一个问题',
promptDisclaimerText: '回答由 AI 生成,可能会出错。',
promptLabelText: '按回车发送Shift+回车换行。',
promptAriaLabelText: '问题输入'
},
conversationScreen: {
preToolCallText: '搜索中...',
searchingText: '搜索中...',
toolCallResultText: '已搜索',
conversationDisclaimer:
'回答由 AI 生成,可能会出错。请核实。',
reasoningText: '推理中...',
thinkingText: '思考中...',
relatedSourcesText: '相关来源',
stoppedStreamingText: '你已停止此回复',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
errorTitleText: '聊天错误'
},
newConversationScreen: {
titleText: '我今天能帮你什么?',
introductionText:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
logo: {
poweredByText: '由…提供支持'
}
}
}
}
}
}
}
}
}
}
})

@ -171,74 +171,139 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
return {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
clearButtonTitle: '清除',
clearButtonAriaLabel: '清除查询',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答...',
placeholderText: '搜索文档或向 AI 提问',
placeholderTextAskAi: '再问一个问题...',
placeholderTextAskAiStreaming: '正在回答...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
backToKeywordSearchButtonText: '返回关键词搜索',
backToKeywordSearchButtonAriaLabel: '返回关键词搜索',
newConversationPlaceholder: '提问',
conversationHistoryTitle: '我的对话历史',
startNewConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史',
threadDepthErrorPlaceholder: '对话已达上限'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
newConversation: {
newConversationTitle: '我今天能帮你什么?',
newConversationDescription:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: '回车键',
navigateText: '导航',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '由…提供支持'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
helpText: '你可能需要检查网络连接。'
},
startScreen: {
recentSearchesTitle: '最近',
noRecentSearchesText: '暂无最近搜索',
saveRecentSearchButtonTitle: '保存此搜索',
removeRecentSearchButtonTitle: '从历史记录中移除此搜索',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除此搜索',
recentConversationsTitle: '最近对话',
removeRecentConversationButtonTitle: '从历史记录中移除此对话'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
noResultsText: '找到相关结果',
suggestedQueryText: '尝试搜索',
reportMissingResultsText: '认为此查询应该有结果?',
reportMissingResultsLinkText: '告诉我们。'
},
resultsScreen: {
askAiPlaceholder: '向 AI 提问: '
askAiPlaceholder: '询问 AI',
noResultsAskAiPlaceholder: '文档里没找到?让 Ask AI 帮忙:'
},
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
disclaimerText: '回答由 AI 生成,可能会出错。请核实。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '',
dislikeButtonTitle: '',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
duringToolCallText: '搜索中...',
afterToolCallText: '已搜索',
aggregatedToolCallText: '已搜索'
stoppedStreamingText: '你已停止此回复',
errorTitleText: '聊天错误',
threadDepthExceededMessage: '为保持回答准确,此对话已关闭。',
startNewConversationButtonText: '开始新的对话'
}
}
},
askAi: {
sidePanel: {
button: {
translations: {
buttonText: '询问 AI',
buttonAriaLabel: '询问 AI'
}
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
panel: {
translations: {
header: {
title: '询问 AI',
conversationHistoryTitle: '我的对话历史',
newConversationText: '开始新的对话',
viewConversationHistoryText: '对话历史'
},
promptForm: {
promptPlaceholderText: '提问',
promptAnsweringText: '正在回答...',
promptAskAnotherQuestionText: '再问一个问题',
promptDisclaimerText: '回答由 AI 生成,可能会出错。',
promptLabelText: '按回车发送Shift+回车换行。',
promptAriaLabelText: '问题输入'
},
conversationScreen: {
preToolCallText: '搜索中...',
searchingText: '搜索中...',
toolCallResultText: '已搜索',
conversationDisclaimer: '回答由 AI 生成,可能会出错。请核实。',
reasoningText: '推理中...',
thinkingText: '思考中...',
relatedSourcesText: '相关来源',
stoppedStreamingText: '你已停止此回复',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
likeButtonTitle: '喜欢',
dislikeButtonTitle: '不喜欢',
thanksForFeedbackText: '感谢你的反馈!',
errorTitleText: '聊天错误'
},
newConversationScreen: {
titleText: '我今天能帮你什么?',
introductionText:
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
logo: {
poweredByText: '由…提供支持'
}
}
}
}
}

@ -39,18 +39,25 @@ export default defineConfig({
provider: 'local',
options: {
locales: {
zh: {
zh: { // 如果你想翻译默认语言,请将此处设为 `root`
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
buttonText: '搜索',
buttonAriaLabel: '搜索'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
displayDetails: '显示详细列表',
resetButtonTitle: '重置搜索',
backButtonTitle: '关闭搜索',
noResultsText: '没有结果',
footer: {
selectText: '选择',
navigateText: '切换'
selectKeyAriaLabel: '输入',
navigateText: '导航',
navigateUpKeyAriaLabel: '上箭头',
navigateDownKeyAriaLabel: '下箭头',
closeText: '关闭',
closeKeyAriaLabel: 'Esc'
}
}
}
@ -62,7 +69,7 @@ export default defineConfig({
})
```
### MiniSearch 配置项 {#mini-search-options}
### MiniSearch 配置项 {#minisearch-options}
你可以像这样配置 MiniSearch
@ -116,7 +123,7 @@ export default defineConfig({
* @param {import('markdown-it-async')} md
*/
async _render(src, env, md) {
// 返回 html 字符串
// 返回 HTML 字符串
}
}
}
@ -201,6 +208,19 @@ export default defineConfig({
你可以使用这样的配置来使用多语言搜索:
<details>
<summary>点击展开</summary>
<<< @/snippets/algolia-i18n.ts
</details>
更多信息请参考[官方 Algolia 文档](https://docsearch.algolia.com/docs/api#translations)。想要快速开始,你也可以从[我们的 GitHub 仓库](https://github.com/search?q=repo:vuejs/vitepress+%22function+searchOptions%22&type=code)复制此站点使用的翻译。
### Algolia Ask AI 支持 {#ask-ai}
如果需要启用 **Ask AI**,只需在 `options` 中添加 `askAi`
```ts
import { defineConfig } from 'vitepress'
@ -212,72 +232,51 @@ export default defineConfig({
appId: '...',
apiKey: '...',
indexName: '...',
locales: {
zh: {
placeholder: '搜索文档',
translations: {
button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
modal: {
searchBox: {
clearButtonTitle: '清除查询条件',
clearButtonAriaLabel: '清除查询条件',
closeButtonText: '关闭',
closeButtonAriaLabel: '关闭',
placeholderText: '搜索文档',
placeholderTextAskAi: '向 AI 提问:',
placeholderTextAskAiStreaming: '回答中...',
searchInputLabel: '搜索',
backToKeywordSearchButtonText: '返回关键字搜索',
backToKeywordSearchButtonAriaLabel: '返回关键字搜索'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除',
recentConversationsTitle: '最近的对话',
removeRecentConversationButtonTitle: '从历史记录中删除对话'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '请检查网络连接'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
},
resultsScreen: { askAiPlaceholder: '向 AI 提问: ' },
askAiScreen: {
disclaimerText: '答案由 AI 生成,可能不准确,请自行验证。',
relatedSourcesText: '相关来源',
thinkingText: '思考中...',
copyButtonText: '复制',
copyButtonCopiedText: '已复制!',
copyButtonTitle: '复制',
likeButtonTitle: '赞',
dislikeButtonTitle: '踩',
thanksForFeedbackText: '感谢你的反馈!',
preToolCallText: '搜索中...',
duringToolCallText: '搜索 ',
afterToolCallText: '已搜索'
},
footer: {
selectText: '选择',
submitQuestionText: '提交问题',
selectKeyAriaLabel: 'Enter 键',
navigateText: '切换',
navigateUpKeyAriaLabel: '向上箭头',
navigateDownKeyAriaLabel: '向下箭头',
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '搜索提供者'
}
}
// askAi: "你的助手ID"
// 或
askAi: {
// 至少需要提供从 Algolia 获取的 assistantId
assistantId: 'XXXYYY',
// 可选覆盖 — 若省略,将复用顶层 appId/apiKey/indexName 的值
// apiKey: '...',
// appId: '...',
// indexName: '...'
}
}
}
}
})
```
::: warning 提示
若仅需关键词搜索,可省略 `askAi`
:::
### Ask AI 侧边栏 {#ask-ai-side-panel}
DocSearch v4.5+ 支持可选的 **Ask AI 侧边栏**。启用后,默认可通过 **Ctrl/Cmd+I** 打开。完整的选项列表请参阅[侧边栏 API 参考](https://docsearch.algolia.com/docs/sidepanel/api-reference)。
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// 镜像 @docsearch/sidepanel-js SidepanelProps API
panel: {
variant: 'floating', // 或 'inline'
side: 'right',
width: '360px',
expandedWidth: '580px',
suggestedQuestions: true
}
}
}
@ -287,131 +286,70 @@ export default defineConfig({
})
```
### Algolia Ask AI 支持 {#ask-ai}
如果需要启用 **Ask AI**,只需在 `options` 中添加 `askAi`
如果需要禁用键盘快捷键,请使用侧边栏的 `keyboardShortcuts` 选项:
```ts
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY'
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
appId: '...',
apiKey: '...',
indexName: '...',
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
}
}
}
}
}
}
})
```
::: warning 提示
若仅需关键词搜索,可省略 `askAi`
:::
#### 模式 (auto / sidePanel / hybrid / modal) {#ask-ai-mode}
[这些选项](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts)可以被覆盖。请参阅 Algolia 官方文档以了解更多信息。
你可以选择性地控制 VitePress 如何集成关键词搜索和 Ask AI
### 爬虫配置 {#crawler-config}
- `mode: 'auto'`(默认):当配置了关键词搜索时推断为 `hybrid`,否则当配置了 Ask AI 侧边栏时推断为 `sidePanel`
- `mode: 'sidePanel'`:强制仅使用侧边栏(隐藏关键词搜索按钮)。
- `mode: 'hybrid'`:启用关键词搜索模态框 + Ask AI 侧边栏(需要关键词搜索配置)。
- `mode: 'modal'`:将 Ask AI 保留在 DocSearch 模态框内(即使你配置了侧边栏)。
以下是基于此站点使用的示例配置:
#### 仅 Ask AI无关键词搜索 {#ask-ai-only}
如果你想**仅使用 Ask AI 侧边栏**,可以省略顶级关键词搜索配置,并在 `askAi` 下提供凭据:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: 'section.has-active div h2',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'algolia',
options: {
mode: 'sidePanel',
askAi: {
assistantId: 'XXXYYY',
appId: '...',
apiKey: '...',
indexName: '...',
sidePanel: true
}
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
### 爬虫配置 {#crawler-config}
以下是基于此站点使用的示例配置:
<<< @/snippets/algolia-crawler.js

@ -95,28 +95,29 @@
"*": "prettier --experimental-cli --ignore-unknown --write"
},
"dependencies": {
"@docsearch/css": "^4.3.2",
"@docsearch/js": "^4.3.2",
"@iconify-json/simple-icons": "^1.2.59",
"@shikijs/core": "^3.15.0",
"@shikijs/transformers": "^3.15.0",
"@shikijs/types": "^3.15.0",
"@docsearch/css": "^4.5.3",
"@docsearch/js": "^4.5.3",
"@docsearch/sidepanel-js": "^4.5.3",
"@iconify-json/simple-icons": "^1.2.68",
"@shikijs/core": "^3.21.0",
"@shikijs/transformers": "^3.21.0",
"@shikijs/types": "^3.21.0",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/devtools-api": "^8.0.5",
"@vue/shared": "^3.5.24",
"@vueuse/core": "^14.0.0",
"@vueuse/integrations": "^14.0.0",
"focus-trap": "^7.6.6",
"@vue/shared": "^3.5.27",
"@vueuse/core": "^14.1.0",
"@vueuse/integrations": "^14.1.0",
"focus-trap": "^7.8.0",
"mark.js": "8.11.1",
"minisearch": "^7.2.0",
"shiki": "^3.15.0",
"vite": "^7.2.2",
"vue": "^3.5.24"
"shiki": "^3.21.0",
"vite": "^7.3.1",
"vue": "^3.5.27"
},
"devDependencies": {
"@clack/prompts": "^1.0.0-alpha.6",
"@iconify/utils": "^3.0.2",
"@clack/prompts": "^1.0.0",
"@iconify/utils": "^3.1.0",
"@mdit-vue/plugin-component": "^3.0.2",
"@mdit-vue/plugin-frontmatter": "^3.0.2",
"@mdit-vue/plugin-headers": "^3.0.2",
@ -135,51 +136,51 @@
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
"@types/markdown-it-attrs": "^4.1.3",
"@types/markdown-it-container": "^2.0.10",
"@types/markdown-it-container": "^4.0.0",
"@types/markdown-it-emoji": "^3.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^24.10.1",
"@types/node": "^25.1.0",
"@types/picomatch": "^4.0.2",
"@types/prompts": "^2.4.9",
"chokidar": "^4.0.3",
"conventional-changelog": "^7.1.1",
"conventional-changelog-angular": "^8.1.0",
"cross-spawn": "^7.0.6",
"esbuild": "^0.25.12",
"execa": "^9.6.0",
"fs-extra": "^11.3.2",
"esbuild": "^0.27.2",
"execa": "^9.6.1",
"fs-extra": "^11.3.3",
"get-port": "^7.1.0",
"gray-matter": "^4.0.3",
"lint-staged": "^16.2.6",
"lint-staged": "^16.2.7",
"lodash.template": "^4.5.0",
"lru-cache": "^11.2.2",
"lru-cache": "^11.2.5",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-async": "^2.2.0",
"markdown-it-attrs": "^4.3.1",
"markdown-it-cjk-friendly": "^1.3.2",
"markdown-it-cjk-friendly": "^2.0.1",
"markdown-it-container": "^4.0.0",
"markdown-it-emoji": "^3.0.0",
"markdown-it-mathjax3": "^4.3.2",
"minimist": "^1.2.8",
"nanoid": "^5.1.6",
"obug": "^2.0.0",
"ora": "^9.0.0",
"obug": "^2.1.1",
"ora": "^9.1.0",
"oxc-minify": "^0.98.0",
"p-map": "^7.0.4",
"package-directory": "^8.1.0",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"playwright-chromium": "^1.56.1",
"playwright-chromium": "^1.58.1",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.0",
"prettier": "^3.6.2",
"postcss-selector-parser": "^7.1.1",
"prettier": "^3.8.1",
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rimraf": "^6.1.0",
"rollup": "^4.53.2",
"rimraf": "^6.1.2",
"rollup": "^4.57.1",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.3",
@ -189,7 +190,7 @@
"tinyglobby": "^0.2.15",
"typescript": "^5.9.3",
"vitest": "4.0.0-beta.4",
"vue-tsc": "^3.1.4",
"vue-tsc": "^3.2.4",
"wait-on": "^9.0.3"
},
"peerDependencies": {
@ -208,5 +209,5 @@
"optional": true
}
},
"packageManager": "pnpm@10.22.0"
"packageManager": "pnpm@10.28.2"
}

File diff suppressed because it is too large Load Diff

@ -1,84 +1,238 @@
<script setup lang="ts">
import docsearch from '@docsearch/js'
import { useRouter } from 'vitepress'
import type { DocSearchInstance, DocSearchProps } from '@docsearch/js'
import type { SidepanelInstance, SidepanelProps } from '@docsearch/sidepanel-js'
import { inBrowser, useRouter } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onMounted, watch } from 'vue'
import { nextTick, onUnmounted, watch } from 'vue'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { useData } from '../composables/data'
import { resolveMode, validateCredentials } from '../support/docsearch'
import '../styles/docsearch.css'
const props = defineProps<{
algolia: DefaultTheme.AlgoliaSearchOptions
algoliaOptions: DefaultTheme.AlgoliaSearchOptions
openRequest?: {
target: 'search' | 'askAi' | 'toggleAskAi'
nonce: number
} | null
}>()
const router = useRouter()
const { site, localeIndex, lang } = useData()
const { site } = useData()
onMounted(update)
watch(localeIndex, update)
let cleanup = () => {}
let docsearchInstance: DocSearchInstance | undefined
let sidepanelInstance: SidepanelInstance | undefined
let openOnReady: 'search' | 'askAi' | null = null
let initializeCount = 0
let docsearchLoader: Promise<typeof import('@docsearch/js')> | undefined
let sidepanelLoader: Promise<typeof import('@docsearch/sidepanel-js')> | undefined
let lastFocusedElement: HTMLElement | null = null
let skipEventDocsearch = false
let skipEventSidepanel = false
async function update() {
await nextTick()
const options = {
...props.algolia,
...props.algolia.locales?.[localeIndex.value]
}
const rawFacetFilters = options.searchParameters?.facetFilters ?? []
const facetFilters = [
...(Array.isArray(rawFacetFilters)
? rawFacetFilters
: [rawFacetFilters]
).filter((f) => !f.startsWith('lang:')),
`lang:${lang.value}`
]
// Rebuild the askAi prop as an object:
// If the askAi prop is a string, treat it as the assistantId and use
// the default indexName, apiKey and appId from the main options.
// If the askAi prop is an object, spread its explicit values.
const askAiProp = options.askAi
const isAskAiString = typeof askAiProp === 'string'
const askAi = askAiProp
? {
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
appId: isAskAiString ? options.appId : askAiProp.appId,
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId,
// Re-use the merged facetFilters from the search parameters so that
// Ask AI uses the same language filtering as the regular search.
searchParameters: facetFilters.length ? { facetFilters } : undefined
watch(() => props.algoliaOptions, update, { immediate: true })
onUnmounted(cleanup)
watch(
() => props.openRequest?.nonce,
() => {
const req = props.openRequest
if (!req) return
if (req.target === 'search') {
if (docsearchInstance?.isReady) {
onBeforeOpen('docsearch', () => docsearchInstance?.open())
} else {
openOnReady = 'search'
}
} else if (req.target === 'toggleAskAi') {
if (sidepanelInstance?.isOpen) {
sidepanelInstance.close()
} else {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
}
} else {
// askAi - open sidepanel or fallback to docsearch modal
if (sidepanelInstance?.isReady) {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
} else if (sidepanelInstance) {
openOnReady = 'askAi'
} else if (docsearchInstance?.isReady) {
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
} else {
openOnReady = 'askAi'
}
: undefined
}
},
{ immediate: true }
)
initialize({
...options,
searchParameters: {
...options.searchParameters,
facetFilters
},
askAi
async function update(options: DefaultTheme.AlgoliaSearchOptions) {
if (!inBrowser) return
await nextTick()
const askAi = options.askAi as DocSearchAskAi | undefined
const { valid, ...credentials } = validateCredentials({
appId: options.appId ?? askAi?.appId,
apiKey: options.apiKey ?? askAi?.apiKey,
indexName: options.indexName ?? askAi?.indexName
})
if (!valid) {
console.warn('[vitepress] Algolia search cannot be initialized: missing appId/apiKey/indexName.')
return
}
await initialize({ ...options, ...credentials })
}
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const options = Object.assign({}, userOptions, {
container: '#docsearch',
async function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const currentInitialize = ++initializeCount
// Always tear down previous instances first (e.g. on locale changes)
cleanup()
const { useSidePanel } = resolveMode(userOptions)
const askAi = userOptions.askAi as DocSearchAskAi | undefined
const { default: docsearch } = await loadDocsearch()
if (currentInitialize !== initializeCount) return
if (useSidePanel && askAi?.sidePanel) {
const { default: sidepanel } = await loadSidepanel()
if (currentInitialize !== initializeCount) return
sidepanelInstance = sidepanel({
...(askAi.sidePanel === true ? {} : askAi.sidePanel),
container: '#vp-docsearch-sidepanel',
indexName: askAi.indexName ?? userOptions.indexName,
appId: askAi.appId ?? userOptions.appId,
apiKey: askAi.apiKey ?? userOptions.apiKey,
assistantId: askAi.assistantId,
onOpen: focusInput,
onClose: onClose.bind(null, 'sidepanel'),
onReady: () => {
if (openOnReady === 'askAi') {
openOnReady = null
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
}
},
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
} as SidepanelProps)
}
const options = {
...userOptions,
container: '#vp-docsearch',
navigator: {
navigate(item: { itemUrl: string }) {
navigate(item) {
router.go(item.itemUrl)
}
},
transformItems(items: { url: string }[]) {
return items.map((item) => {
return Object.assign({}, item, {
url: getRelativePath(item.url)
})
})
transformItems: (items) => items.map((item) => ({ ...item, url: getRelativePath(item.url) })),
// When sidepanel is enabled, intercept Ask AI events to open it instead (hybrid mode)
...(useSidePanel && sidepanelInstance && {
interceptAskAiEvent: (initialMessage) => {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open(initialMessage))
return true
}
}),
onOpen: focusInput,
onClose: onClose.bind(null, 'docsearch'),
onReady: () => {
if (openOnReady === 'search') {
openOnReady = null
onBeforeOpen('docsearch', () => docsearchInstance?.open())
} else if (openOnReady === 'askAi' && !sidepanelInstance) {
// No sidepanel configured, use docsearch modal for askAi
openOnReady = null
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
}
},
keyboardShortcuts: {
'/': false,
'Ctrl/Cmd+K': false
}
} as DocSearchProps
docsearchInstance = docsearch(options)
cleanup = () => {
docsearchInstance?.destroy()
sidepanelInstance?.destroy()
docsearchInstance = undefined
sidepanelInstance = undefined
openOnReady = null
lastFocusedElement = null
}
}
function focusInput() {
requestAnimationFrame(() => {
const input =
document.querySelector<HTMLInputElement>('#docsearch-input') ||
document.querySelector<HTMLInputElement>('#docsearch-sidepanel textarea')
input?.focus()
})
}
docsearch(options as any)
function onBeforeOpen(target: 'docsearch' | 'sidepanel', cb: () => void) {
if (target === 'docsearch') {
if (sidepanelInstance?.isOpen) {
skipEventSidepanel = true
sidepanelInstance.close()
} else if (!docsearchInstance?.isOpen) {
if (document.activeElement instanceof HTMLElement) {
lastFocusedElement = document.activeElement
}
}
} else if (target === 'sidepanel') {
if (docsearchInstance?.isOpen) {
skipEventDocsearch = true
docsearchInstance.close()
} else if (!sidepanelInstance?.isOpen) {
if (document.activeElement instanceof HTMLElement) {
lastFocusedElement = document.activeElement
}
}
}
setTimeout(cb, 0)
}
function onClose(target: 'docsearch' | 'sidepanel') {
if (target === 'docsearch') {
if (skipEventDocsearch) {
skipEventDocsearch = false
return
}
} else if (target === 'sidepanel') {
if (skipEventSidepanel) {
skipEventSidepanel = false
return
}
}
if (lastFocusedElement) {
lastFocusedElement.focus()
lastFocusedElement = null
}
}
function loadDocsearch() {
if (!docsearchLoader) {
docsearchLoader = import('@docsearch/js')
}
return docsearchLoader
}
function loadSidepanel() {
if (!sidepanelLoader) {
sidepanelLoader = import('@docsearch/sidepanel-js')
}
return sidepanelLoader
}
function getRelativePath(url: string) {
@ -88,5 +242,6 @@ function getRelativePath(url: string) {
</script>
<template>
<div id="docsearch" />
<div id="vp-docsearch" />
<div id="vp-docsearch-sidepanel" />
</template>

@ -2,7 +2,7 @@
import localSearchIndex from '@localSearchIndex'
import {
computedAsync,
debouncedWatch,
watchDebounced,
onKeyStroke,
useEventListener,
useLocalStorage,
@ -26,7 +26,7 @@ import {
watchEffect,
type Ref
} from 'vue'
import type { ModalTranslations } from '../../../../types/local-search'
import type { LocalSearchTranslations } from '../../../../types/local-search'
import { pathToFile } from '../../app/utils'
import { escapeRegExp } from '../../shared'
import { useData } from '../composables/data'
@ -113,16 +113,6 @@ const disableDetailedView = computed(() => {
)
})
const buttonText = computed(() => {
const options = theme.value.search?.options ?? theme.value.algolia
return (
options?.locales?.[localeIndex.value]?.translations?.button?.buttonText ||
options?.translations?.button?.buttonText ||
'Search'
)
})
watchEffect(() => {
if (disableDetailedView.value) {
showDetailedList.value = false
@ -144,7 +134,7 @@ const mark = computedAsync(async () => {
const cache = new LRUCache<string, Map<string, string>>(16) // 16 files
debouncedWatch(
watchDebounced(
() => [searchIndex.value, filterText.value, showDetailedList.value] as const,
async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => {
if (old?.[0] !== index) {
@ -339,8 +329,12 @@ onKeyStroke('Escape', () => {
emit('close')
})
// Translations
const defaultTranslations: { modal: ModalTranslations } = {
/* Translations */
const defaultTranslations: LocalSearchTranslations = {
button: {
buttonText: 'Search'
},
modal: {
displayDetails: 'Display detailed list',
resetButtonTitle: 'Reset search',
@ -360,7 +354,7 @@ const defaultTranslations: { modal: ModalTranslations } = {
const translate = createSearchTranslate(defaultTranslations)
// Back
/* Back */
onMounted(() => {
// Prevents going to previous site
@ -373,6 +367,7 @@ useEventListener('popstate', (event) => {
})
/** Lock body */
const isLocked = useScrollLock(inBrowser ? document.body : null)
onMounted(() => {
@ -432,7 +427,7 @@ function onMouseMove(e: MouseEvent) {
@submit.prevent=""
>
<label
:title="buttonText"
:title="translate('button.buttonText')"
id="localsearch-label"
for="localsearch-input"
>
@ -461,7 +456,7 @@ function onMouseMove(e: MouseEvent) {
id="localsearch-input"
enterkeyhint="go"
maxlength="64"
:placeholder="buttonText"
:placeholder="translate('button.buttonText')"
spellcheck="false"
type="search"
/>

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useLayout } from '../composables/layout'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
@ -21,21 +20,18 @@ defineEmits<{
const { y } = useWindowScroll()
const { isHome, hasSidebar } = useLayout()
const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
'home': isHome.value,
'top': y.value === 0,
'screen-open': props.isScreenOpen
}
})
</script>
<template>
<div class="VPNavBar" :class="classes">
<div
class="VPNavBar"
:class="{
'has-sidebar': hasSidebar,
'home': isHome,
'top': y === 0,
'screen-open': isScreenOpen
}"
>
<div class="wrapper">
<div class="container">
<div class="title">
@ -55,7 +51,11 @@ watchPostEffect(() => {
<VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
<VPNavBarHamburger
class="hamburger"
:active="isScreenOpen"
@click="$emit('toggle-screen')"
/>
</div>
</div>
</div>
@ -206,12 +206,6 @@ watchPostEffect(() => {
}
}
@media (max-width: 767px) {
.content-body {
column-gap: 0.5rem;
}
}
.menu + .translations::before,
.menu + .appearance::before,
.menu + .social-links::before,

@ -0,0 +1,31 @@
<template>
<button type="button" class="VPNavBarAskAiButton">
<span class="vpi-sparkles" aria-hidden="true"></span>
</button>
</template>
<style scoped>
.VPNavBarAskAiButton {
display: flex;
align-items: center;
height: var(--vp-nav-height);
padding: 8px 14px;
font-size: 20px;
}
@media (min-width: 768px) {
.VPNavBarAskAiButton {
height: auto;
padding: 11.5px;
transition: color 0.3s ease;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
font-size: 15px;
color: var(--vp-c-text-2);
}
.VPNavBarAskAiButton:hover {
color: var(--vp-c-brand-1);
}
}
</style>

@ -1,9 +1,11 @@
<script lang="ts" setup>
import '@docsearch/css'
import { onKeyStroke } from '@vueuse/core'
import type { DefaultTheme } from 'vitepress/theme'
import { defineAsyncComponent, onMounted, onUnmounted, ref } from 'vue'
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
import { useData } from '../composables/data'
import { resolveMode, resolveOptionsForLanguage } from '../support/docsearch'
import { smartComputed } from '../support/reactivity'
import VPNavBarAskAiButton from './VPNavBarAskAiButton.vue'
import VPNavBarSearchButton from './VPNavBarSearchButton.vue'
const VPLocalSearchBox = __VP_LOCAL_SEARCH__
@ -14,7 +16,37 @@ const VPAlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./VPAlgoliaSearchBox.vue'))
: () => null
const { theme } = useData()
const { theme, localeIndex, lang } = useData()
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
// #region Algolia Search
const algoliaOptions = smartComputed<DefaultTheme.AlgoliaSearchOptions>(() => {
return resolveOptionsForLanguage(
theme.value.search?.options || {},
localeIndex.value,
lang.value
)
})
const resolvedMode = computed(() => resolveMode(algoliaOptions.value))
const askAiSidePanelConfig = computed(() => {
if (!resolvedMode.value.useSidePanel) return null
const askAi = algoliaOptions.value.askAi
if (!askAi || typeof askAi === 'string') return null
if (!askAi.sidePanel) return null
return askAi.sidePanel === true ? {} : askAi.sidePanel
})
const askAiShortcutEnabled = computed(() => {
return askAiSidePanelConfig.value?.keyboardShortcuts?.['Ctrl/Cmd+I'] !== 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
@ -22,86 +54,74 @@ const { theme } = useData()
const loaded = ref(false)
const actuallyLoaded = ref(false)
const preconnect = () => {
onMounted(() => {
if (!__ALGOLIA__) return
const id = 'VPAlgoliaPreconnect'
if (document.getElementById(id)) return
const appId =
algoliaOptions.value.appId ||
(typeof algoliaOptions.value.askAi === 'object'
? algoliaOptions.value.askAi?.appId
: undefined)
if (!appId) return
const rIC = window.requestIdleCallback || setTimeout
rIC(() => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.href = `https://${appId}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
}
onMounted(() => {
if (!__ALGOLIA__) {
return
}
preconnect()
})
const handleSearchHotKey = (event: KeyboardEvent) => {
if (__ALGOLIA__) {
onKeyStroke('k', (event) => {
if (
(event.key?.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) ||
(!isEditingContent(event) && event.key === '/')
resolvedMode.value.showKeywordSearch &&
(event.ctrlKey || event.metaKey)
) {
event.preventDefault()
load()
remove()
loadAndOpen('search')
}
}
const remove = () => {
window.removeEventListener('keydown', handleSearchHotKey)
}
})
window.addEventListener('keydown', handleSearchHotKey)
onKeyStroke('i', (event) => {
if (
askAiSidePanelConfig.value &&
askAiShortcutEnabled.value &&
(event.ctrlKey || event.metaKey)
) {
event.preventDefault()
loadAndOpen('askAi')
}
})
onUnmounted(remove)
})
onKeyStroke('/', (event) => {
if (resolvedMode.value.showKeywordSearch && !isEditingContent(event)) {
event.preventDefault()
loadAndOpen('search')
}
})
}
function load() {
function loadAndOpen(target: OpenTarget) {
if (!loaded.value) {
loaded.value = true
setTimeout(poll, 16)
}
}
function poll() {
// programmatically open the search box after initialize
const e = new Event('keydown') as any
e.key = 'k'
e.metaKey = true
window.dispatchEvent(e)
setTimeout(() => {
if (!document.querySelector('.DocSearch-Modal')) {
poll()
}
}, 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 {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
)
}
// #endregion
// Local search
// #region Local Search
const showSearch = ref(false)
@ -121,37 +141,60 @@ if (__VP_LOCAL_SEARCH__) {
})
}
const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
// #endregion
function isEditingContent(event: KeyboardEvent): boolean {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
)
}
</script>
<template>
<div class="VPNavBarSearch">
<template v-if="provider === 'local'">
<VPLocalSearchBox
v-if="showSearch"
@close="showSearch = false"
<template v-if="provider === 'algolia'">
<VPNavBarSearchButton
v-if="resolvedMode.showKeywordSearch"
:text="algoliaOptions.translations?.button?.buttonText || 'Search'"
:aria-label="algoliaOptions.translations?.button?.buttonAriaLabel || 'Search'"
:aria-keyshortcuts="'/ control+k meta+k'"
@click="loadAndOpen('search')"
/>
<VPNavBarAskAiButton
v-if="askAiSidePanelConfig"
:aria-label="askAiSidePanelConfig.button?.translations?.buttonAriaLabel || 'Ask AI'"
:aria-keyshortcuts="askAiShortcutEnabled ? 'control+i meta+i' : undefined"
@click="actuallyLoaded ? loadAndOpen('toggleAskAi') : loadAndOpen('askAi')"
/>
<div id="local-search">
<VPNavBarSearchButton @click="showSearch = true" />
</div>
</template>
<template v-else-if="provider === 'algolia'">
<VPAlgoliaSearchBox
v-if="loaded"
:algolia="theme.search?.options ?? theme.algolia"
:algolia-options
:open-request
@vue:beforeMount="actuallyLoaded = true"
/>
<div v-if="!actuallyLoaded" id="docsearch">
<VPNavBarSearchButton @click="load" />
</div>
</template>
<template v-else-if="provider === 'local'">
<VPNavBarSearchButton
:text="algoliaOptions.translations?.button?.buttonText || 'Search'"
:aria-label="algoliaOptions.translations?.button?.buttonAriaLabel || 'Search'"
:aria-keyshortcuts="'/ control+k meta+k'"
@click="showSearch = true"
/>
<VPLocalSearchBox
v-if="showSearch"
@close="showSearch = false"
/>
</template>
</div>
</template>
<style>
<style scoped>
.VPNavBarSearch {
display: flex;
align-items: center;
@ -159,6 +202,7 @@ const provider = __ALGOLIA__ ? 'algolia' : __VP_LOCAL_SEARCH__ ? 'local' : ''
@media (min-width: 768px) {
.VPNavBarSearch {
gap: 8px;
flex-grow: 1;
padding-left: 24px;
}

@ -1,147 +1,67 @@
<script lang="ts" setup>
import type { ButtonTranslations } from '../../../../types/local-search'
import { createSearchTranslate } from '../support/translation'
// button translations
const defaultTranslations: { button: ButtonTranslations } = {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
}
}
const translate = createSearchTranslate(defaultTranslations)
defineProps<{
text: string
}>()
</script>
<template>
<button
type="button"
:aria-label="translate('button.buttonAriaLabel')"
aria-keyshortcuts="/ control+k meta+k"
class="DocSearch DocSearch-Button"
>
<span class="DocSearch-Button-Container">
<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>
<button type="button" class="VPNavBarSearchButton">
<span class="vpi-search" aria-hidden="true"></span>
<span class="text">{{ text }}</span>
<span class="keys" aria-hidden="true">
<kbd class="key-cmd">&#x2318;</kbd>
<kbd class="key-ctrl">Ctrl</kbd>
<kbd>K</kbd>
</span>
</button>
</template>
<style>
[class*='DocSearch'] {
--docsearch-actions-height: auto;
--docsearch-actions-width: auto;
--docsearch-background-color: var(--vp-c-bg-soft);
--docsearch-container-background: var(--vp-backdrop-bg-color);
--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-color: var(--vp-c-text-1);
--docsearch-hit-highlight-color: var(--vp-c-brand-soft);
--docsearch-icon-color: var(--vp-c-text-2);
--docsearch-key-background: transparent;
--docsearch-key-color: var(--vp-c-text-2);
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-primary-color: var(--vp-c-brand-1);
--docsearch-searchbox-focus-background: transparent;
--docsearch-secondary-text-color: var(--vp-c-text-2);
--docsearch-soft-primary-color: var(--vp-c-brand-soft);
--docsearch-subtle-color: var(--vp-c-divider);
--docsearch-success-color: var(--vp-c-brand-soft);
--docsearch-text-color: var(--vp-c-text-1);
}
.dark [class*='DocSearch'] {
--docsearch-modal-shadow: none;
}
.DocSearch-Clear {
padding: 0 8px;
}
.DocSearch-Commands-Key {
padding: 4px;
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
}
.DocSearch-Hit a:focus-visible {
outline: 2px solid var(--docsearch-focus-color);
}
.DocSearch-Logo [class^='cls-'] {
fill: currentColor;
<style scoped>
.VPNavBarSearchButton {
display: flex;
align-items: center;
gap: 8px;
height: var(--vp-nav-height);
padding: 8px 14px;
font-size: 20px;
}
.DocSearch-SearchBar + .DocSearch-Footer {
border-top-color: transparent;
}
.DocSearch-Title {
font-size: revert;
line-height: revert;
}
.DocSearch-Button {
--docsearch-muted-color: var(--docsearch-text-color);
--docsearch-searchbox-background: transparent;
width: auto;
padding: 2px 12px;
border: none;
border-radius: 8px;
.text,
.keys,
:root.mac .key-ctrl,
:root:not(.mac) .key-cmd {
display: none;
}
.DocSearch-Search-Icon {
color: inherit !important;
width: 20px;
height: 20px;
kbd {
font-family: inherit;
font-weight: 500;
}
@media (min-width: 768px) {
.DocSearch-Button {
--docsearch-muted-color: var(--docsearch-secondary-text-color);
--docsearch-searchbox-background: var(--vp-c-bg-alt);
.VPNavBarSearchButton {
height: auto;
padding: 8px 12px;
background-color: var(--vp-c-bg-alt);
border-radius: 8px;
font-size: 14px;
line-height: 1;
color: var(--vp-c-text-2);
}
.DocSearch-Search-Icon {
width: 15px;
height: 15px;
}
.DocSearch-Button-Placeholder {
.text {
display: inline;
font-size: 13px;
}
}
.DocSearch-Button-Keys {
min-width: auto;
margin: 0;
padding: 4px 6px;
background-color: var(--docsearch-key-background);
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
font-size: 12px;
line-height: 1;
color: var(--docsearch-key-color);
}
.DocSearch-Button-Keys > * {
display: none;
}
.DocSearch-Button-Keys:after {
/*rtl:ignore*/
direction: ltr;
content: 'Ctrl K';
}
.mac .DocSearch-Button-Keys:after {
content: '\2318 K';
.keys {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 12px;
}
}
</style>

@ -1,7 +1,6 @@
<script setup lang="ts">
import { useScrollLock } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { ref } from 'vue'
import VPNavScreenAppearance from './VPNavScreenAppearance.vue'
import VPNavScreenMenu from './VPNavScreenMenu.vue'
import VPNavScreenSocialLinks from './VPNavScreenSocialLinks.vue'
@ -11,7 +10,6 @@ defineProps<{
open: boolean
}>()
const screen = ref<HTMLElement | null>(null)
const isLocked = useScrollLock(inBrowser ? document.body : null)
</script>
@ -21,7 +19,7 @@ const isLocked = useScrollLock(inBrowser ? document.body : null)
@enter="isLocked = true"
@after-leave="isLocked = false"
>
<div v-if="open" class="VPNavScreen" ref="screen" id="VPNavScreen">
<div v-if="open" class="VPNavScreen" id="VPNavScreen">
<div class="container">
<slot name="nav-screen-content-before" />
<VPNavScreenMenu class="menu" />

@ -0,0 +1,120 @@
@import '@docsearch/css/dist/style.css';
@import '@docsearch/css/dist/sidepanel.css';
#vp-docsearch,
#vp-docsearch-sidepanel,
.DocSearch-SidepanelButton {
display: none;
}
:root:root {
--docsearch-actions-height: auto;
--docsearch-actions-width: auto;
--docsearch-background-color: var(--vp-c-bg-soft);
--docsearch-container-background: var(--vp-backdrop-bg-color);
--docsearch-dropdown-menu-background: var(--vp-c-bg-elv);
--docsearch-dropdown-menu-item-hover-background: var(--vp-c-default-soft);
--docsearch-focus-color: var(--vp-c-brand-1);
--docsearch-footer-background: var(--vp-c-bg-alt);
--docsearch-highlight-color: var(--vp-c-brand-1);
--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);
--docsearch-key-background: var(--vp-code-bg);
--docsearch-modal-background: var(--vp-c-bg-soft);
--docsearch-muted-color: var(--vp-c-text-2);
--docsearch-primary-color: var(--vp-c-brand-1);
--docsearch-searchbox-background: var(--vp-c-bg-alt);
--docsearch-searchbox-focus-background: transparent;
--docsearch-secondary-text-color: var(--vp-c-text-2);
--docsearch-sidepanel-accent-muted: var(--vp-c-text-3);
--docsearch-sidepanel-text-base: var(--vp-c-text-1);
--docsearch-soft-muted-color: var(--vp-c-default-soft);
--docsearch-soft-primary-color: var(--vp-c-brand-soft);
--docsearch-subtle-color: var(--vp-c-divider);
--docsearch-success-color: var(--vp-c-brand-soft);
--docsearch-text-color: var(--vp-c-text-1);
}
:root.dark {
--docsearch-modal-shadow: none;
}
.DocSearch-AskAiScreen-RelatedSources-Item-Link {
padding: 8px 12px 8px 10px;
}
.DocSearch-AskAiScreen-RelatedSources-Item-Link svg {
width: 16px;
height: 16px;
}
.DocSearch-AskAiScreen-RelatedSources-Title {
padding-bottom: 0;
font-size: 12px;
}
.DocSearch-Clear {
padding-right: 6px;
}
.DocSearch-Commands-Key {
padding: 4px;
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
}
.DocSearch-Hit a:focus-visible {
outline: 2px solid var(--docsearch-focus-color);
}
.DocSearch-Logo [class^='cls-'] {
fill: currentColor;
}
.DocSearch-Markdown-Content code {
padding: 0.2em 0.4em;
}
.DocSearch-Menu-content {
margin-top: -4px;
padding: 6px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
box-shadow: var(--vp-shadow-2);
}
.DocSearch-Menu-item {
border-radius: 4px;
}
.DocSearch-SearchBar + .DocSearch-Footer {
border-top-color: transparent;
}
.DocSearch-Sidepanel-Prompt--form {
border-color: var(--docsearch-subtle-color);
transition: border-color 0.2s;
}
.DocSearch-Sidepanel-Prompt--submit {
background-color: var(--docsearch-soft-primary-color);
color: var(--docsearch-primary-color);
}
.DocSearch-Sidepanel-Prompt--submit:hover {
background-color: var(--vp-button-brand-hover-bg);
color: var(--vp-button-brand-text);
}
.DocSearch-Sidepanel-Prompt--submit:disabled,
.DocSearch-Sidepanel-Prompt--submit[aria-disabled='true'] {
background-color: var(--docsearch-soft-muted-color);
color: var(--docsearch-muted-color);
}
.DocSearch-Title {
font-size: revert;
line-height: revert;
}

@ -74,7 +74,10 @@
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2c-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E");
}
.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");
--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%3Cpath d='m21 21l-4.34-4.34'/%3E%3Ccircle cx='11' cy='11' r='8'/%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.8'%3E%3Cpath d='M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594zM20 2v4m2-2h-4'/%3E%3Ccircle cx='4' cy='20' r='2'/%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");

@ -0,0 +1,253 @@
import type { DefaultTheme } from 'vitepress/theme'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { isObject } from '../../shared'
export type FacetFilter = string | string[] | FacetFilter[]
export interface ValidatedCredentials {
valid: boolean
appId?: string
apiKey?: string
indexName?: string
}
export type DocSearchMode = 'auto' | 'sidePanel' | 'hybrid' | 'modal'
export interface ResolvedMode {
mode: DocSearchMode
showKeywordSearch: boolean
useSidePanel: boolean
}
/**
* Resolves the effective mode based on config and available features.
*
* - 'auto': infer hybrid vs sidePanel-only from provided config
* - 'sidePanel': force sidePanel-only even if keyword search is configured
* - 'hybrid': force hybrid (error if keyword search is not configured)
* - 'modal': force modal even if sidePanel is configured
*/
export function resolveMode(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'appId' | 'apiKey' | 'indexName' | 'askAi' | 'mode'
>
): ResolvedMode {
const mode = options.mode ?? 'auto'
const hasKeyword = hasKeywordSearch(options)
const askAi = options.askAi
const hasSidePanelConfig = Boolean(
askAi && typeof askAi === 'object' && askAi.sidePanel
)
switch (mode) {
case 'sidePanel':
// Force sidePanel-only - hide keyword search
return {
mode,
showKeywordSearch: false,
useSidePanel: true
}
case 'hybrid':
// Force hybrid - keyword search must be configured
if (!hasKeyword) {
console.error(
'[vitepress] mode: "hybrid" requires keyword search credentials (appId, apiKey, indexName).'
)
}
return {
mode,
showKeywordSearch: hasKeyword,
useSidePanel: true
}
case 'modal':
// Force modal - don't use sidepanel for askai, even if configured
return {
mode,
showKeywordSearch: hasKeyword,
useSidePanel: false
}
case 'auto':
default:
// Auto-detect based on config
return {
mode: 'auto',
showKeywordSearch: hasKeyword,
useSidePanel: hasSidePanelConfig
}
}
}
export function hasKeywordSearch(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'appId' | 'apiKey' | 'indexName'
>
): boolean {
return Boolean(options.appId && options.apiKey && options.indexName)
}
export function hasAskAi(
askAi: DefaultTheme.AlgoliaSearchOptions['askAi']
): boolean {
if (!askAi) return false
if (typeof askAi === 'string') return askAi.length > 0
return Boolean(askAi.assistantId)
}
/**
* Removes existing `lang:` filters and appends `lang:${lang}`.
* Handles both flat arrays and nested arrays (for OR conditions).
*/
export function mergeLangFacetFilters(
rawFacetFilters: FacetFilter | FacetFilter[] | undefined,
lang: string
): FacetFilter[] {
const input = Array.isArray(rawFacetFilters)
? rawFacetFilters
: rawFacetFilters
? [rawFacetFilters]
: []
const filtered = input
.map((filter) => {
if (Array.isArray(filter)) {
// Handle nested arrays (OR conditions)
return filter.filter(
(f) => typeof f === 'string' && !f.startsWith('lang:')
)
}
return filter
})
.filter((filter) => {
if (typeof filter === 'string') {
return !filter.startsWith('lang:')
}
// Keep nested arrays with remaining filters
return Array.isArray(filter) && filter.length > 0
})
return [...filtered, `lang:${lang}`]
}
/**
* Validates that required Algolia credentials are present.
*/
export function validateCredentials(
options: Pick<
DefaultTheme.AlgoliaSearchOptions,
'appId' | 'apiKey' | 'indexName'
>
): ValidatedCredentials {
const appId = options.appId
const apiKey = options.apiKey
const indexName = options.indexName
return {
valid: Boolean(appId && apiKey && indexName),
appId,
apiKey,
indexName
}
}
/**
* Builds Ask AI configuration from various input formats.
*/
export function buildAskAiConfig(
askAiProp: NonNullable<DefaultTheme.AlgoliaSearchOptions['askAi']>,
options: DefaultTheme.AlgoliaSearchOptions,
lang: string
): DocSearchAskAi {
const isAskAiString = typeof askAiProp === 'string'
const askAiSearchParameters =
!isAskAiString && askAiProp.searchParameters
? { ...askAiProp.searchParameters }
: undefined
// If Ask AI defines its own facetFilters, merge lang filtering into those.
// Otherwise, reuse the keyword search facetFilters so Ask AI follows the
// same language filtering behavior by default.
const askAiFacetFiltersSource =
askAiSearchParameters?.facetFilters ??
options.searchParameters?.facetFilters
const askAiFacetFilters = mergeLangFacetFilters(
askAiFacetFiltersSource as FacetFilter | FacetFilter[] | undefined,
lang
)
const mergedAskAiSearchParameters = {
...askAiSearchParameters,
facetFilters: askAiFacetFilters.length ? askAiFacetFilters : undefined
}
const result: Record<string, any> = {
...(isAskAiString ? {} : askAiProp),
indexName: isAskAiString ? options.indexName : askAiProp.indexName,
apiKey: isAskAiString ? options.apiKey : askAiProp.apiKey,
appId: isAskAiString ? options.appId : askAiProp.appId,
assistantId: isAskAiString ? askAiProp : askAiProp.assistantId
}
// Keep `searchParameters` undefined unless it has at least one key.
if (Object.values(mergedAskAiSearchParameters).some((v) => v != null)) {
result.searchParameters = mergedAskAiSearchParameters
}
return result
}
/**
* Resolves Algolia search options for the given language,
* merging in locale-specific overrides and language facet filters.
*/
export function resolveOptionsForLanguage(
options: DefaultTheme.AlgoliaSearchOptions,
localeIndex: string,
lang: string
): DefaultTheme.AlgoliaSearchOptions {
options = deepMerge(options, options.locales?.[localeIndex] || {})
const facetFilters = mergeLangFacetFilters(
options.searchParameters?.facetFilters,
lang
)
const askAi = options.askAi
? buildAskAiConfig(options.askAi, options, lang)
: undefined
return {
...options,
searchParameters: { ...options.searchParameters, facetFilters },
askAi
}
}
function deepMerge<T>(target: T, source: Partial<T>): T {
const result = { ...target } as any
for (const key in source) {
const value = source[key]
if (value === undefined) continue
// special case: replace entirely
if (key === 'searchParameters') {
result[key] = value
continue
}
// deep-merge only plain objects; arrays are replaced entirely
if (isObject(value) && isObject(result[key])) {
result[key] = deepMerge(result[key], value)
} else {
result[key] = value
}
}
delete result.locales
return result
}

@ -0,0 +1,14 @@
import { type ComputedRef, computed } from 'vue'
export function smartComputed<T>(
getter: () => T,
comparator = (oldValue: T, newValue: T) =>
JSON.stringify(oldValue) === JSON.stringify(newValue)
): ComputedRef<T> {
return computed((oldValue) => {
const newValue = getter()
return oldValue === undefined || !comparator(oldValue, newValue)
? newValue
: oldValue
})
}

@ -41,7 +41,7 @@ declare module 'vite' {
const themeRE = /(?:^|\/)\.vitepress\/theme\/index\.(m|c)?(j|t)s$/
const startsWithThemeRE = /^@theme(?:\/|$)/
const docsearchRE = /\/@docsearch\/css\/dist\/style.css(?:$|\?)/
const docsearchRE = /\/docsearch\.css(?:$|\?)/
const hashRE = /\.([-\w]+)\.js$/
const staticInjectMarkerRE = /\bcreateStaticVNode\((?:(".*")|('.*')), (\d+)\)/g
@ -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: {

@ -39,8 +39,9 @@ export interface TransformPageContext<ThemeConfig = any> {
siteConfig: SiteConfig<ThemeConfig>
}
export interface UserConfig<ThemeConfig = any>
extends LocaleSpecificConfig<ThemeConfig> {
export interface UserConfig<
ThemeConfig = any
> extends LocaleSpecificConfig<ThemeConfig> {
extends?: RawConfigExports<ThemeConfig>
base?: string
@ -207,27 +208,26 @@ export interface UserConfig<ThemeConfig = any>
| AdditionalConfigLoader<ThemeConfig>
}
export interface SiteConfig<ThemeConfig = any>
extends Pick<
UserConfig<ThemeConfig>,
| 'markdown'
| 'vue'
| 'vite'
| 'shouldPreload'
| 'router'
| 'mpa'
| 'metaChunk'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
| 'useWebFonts'
| 'postRender'
| 'buildEnd'
| 'transformHead'
| 'transformHtml'
| 'transformPageData'
| 'sitemap'
> {
export interface SiteConfig<ThemeConfig = any> extends Pick<
UserConfig<ThemeConfig>,
| 'markdown'
| 'vue'
| 'vite'
| 'shouldPreload'
| 'router'
| 'mpa'
| 'metaChunk'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
| 'useWebFonts'
| 'postRender'
| 'buildEnd'
| 'transformHead'
| 'transformHtml'
| 'transformPageData'
| 'sitemap'
> {
root: string
srcDir: string
site: SiteData<ThemeConfig>

@ -128,14 +128,6 @@
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}
<% } else { %>
html {
font-family: Arial, Helvetica;

@ -130,11 +130,6 @@ export namespace DefaultTheme {
| { provider: 'local'; options?: LocalSearchOptions }
| { provider: 'algolia'; options: AlgoliaSearchOptions }
/**
* @deprecated Use `search` instead.
*/
algolia?: AlgoliaSearchOptions
/**
* The carbon ads options. Leave it undefined to disable the ads feature.
*/
@ -402,6 +397,11 @@ export namespace DefaultTheme {
* `@docsearch/react/dist/esm/DocSearch.d.ts`
*/
export interface AlgoliaSearchOptions extends DocSearchProps {
/**
* Locale-specific overrides for Algolia search options.
* These options will be deeply merged with the root options,
* except for `searchParameters`, which is fully replaced.
*/
locales?: Record<string, Partial<DocSearchProps>>
}

244
types/docsearch.d.ts vendored

@ -1,205 +1,63 @@
export interface DocSearchProps {
appId: string
apiKey: string
indexName: string
placeholder?: string
searchParameters?: SearchOptions
disableUserPersonalization?: boolean
initialQuery?: string
insights?: boolean
translations?: DocSearchTranslations
askAi?: DocSearchAskAi | string
}
export interface SearchOptions {
query?: string
similarQuery?: string
facetFilters?: string | string[]
optionalFilters?: string | string[]
numericFilters?: string | string[]
tagFilters?: string | string[]
sumOrFiltersScores?: boolean
filters?: string
page?: number
hitsPerPage?: number
offset?: number
length?: number
attributesToHighlight?: string[]
attributesToSnippet?: string[]
attributesToRetrieve?: string[]
highlightPreTag?: string
highlightPostTag?: string
snippetEllipsisText?: string
restrictHighlightAndSnippetArrays?: boolean
facets?: string[]
maxValuesPerFacet?: number
facetingAfterDistinct?: boolean
minWordSizefor1Typo?: number
minWordSizefor2Typos?: number
allowTyposOnNumericTokens?: boolean
disableTypoToleranceOnAttributes?: string[]
queryType?: 'prefixLast' | 'prefixAll' | 'prefixNone'
removeWordsIfNoResults?: 'none' | 'lastWords' | 'firstWords' | 'allOptional'
advancedSyntax?: boolean
advancedSyntaxFeatures?: ('exactPhrase' | 'excludeWords')[]
optionalWords?: string | string[]
disableExactOnAttributes?: string[]
exactOnSingleWordQuery?: 'attribute' | 'none' | 'word'
alternativesAsExact?: (
| 'ignorePlurals'
| 'singleWordSynonym'
| 'multiWordsSynonym'
)[]
enableRules?: boolean
ruleContexts?: string[]
distinct?: boolean | number
analytics?: boolean
analyticsTags?: string[]
synonyms?: boolean
replaceSynonymsInHighlight?: boolean
minProximity?: number
responseFields?: string[]
maxFacetHits?: number
percentileComputation?: boolean
clickAnalytics?: boolean
personalizationImpact?: number
enablePersonalization?: boolean
restrictSearchableAttributes?: string[]
sortFacetValuesBy?: 'count' | 'alpha'
typoTolerance?: boolean | 'min' | 'strict'
aroundLatLng?: string
aroundLatLngViaIP?: boolean
aroundRadius?: number | 'all'
aroundPrecision?: number | { from: number; value: number }[]
minimumAroundRadius?: number
insideBoundingBox?: number[][]
insidePolygon?: number[][]
ignorePlurals?: boolean | string[]
removeStopWords?: boolean | string[]
naturalLanguages?: string[]
getRankingInfo?: boolean
userToken?: string
enableABTest?: boolean
decompoundQuery?: boolean
relevancyStrictness?: number
}
export interface DocSearchTranslations {
button?: ButtonTranslations
modal?: ModalTranslations
}
export interface ButtonTranslations {
buttonText?: string
buttonAriaLabel?: string
}
export interface ModalTranslations extends ScreenStateTranslations {
searchBox?: SearchBoxTranslations
footer?: FooterTranslations
}
export interface ScreenStateTranslations {
errorScreen?: ErrorScreenTranslations
startScreen?: StartScreenTranslations
resultsScreen?: ResultsScreenTranslations
noResultsScreen?: NoResultsScreenTranslations
askAiScreen?: AskAiScreenTranslations
}
export interface SearchBoxTranslations {
clearButtonTitle?: string
clearButtonAriaLabel?: string
closeButtonText?: string
closeButtonAriaLabel?: string
placeholderText?: string
placeholderTextAskAi?: string
searchInputLabel?: string
placeholderTextAskAiStreaming?: string
backToKeywordSearchButtonText?: string
backToKeywordSearchButtonAriaLabel?: string
}
export interface FooterTranslations {
selectText?: string
submitQuestionText?: string
selectKeyAriaLabel?: string
navigateText?: string
navigateUpKeyAriaLabel?: string
backToSearchText?: string
navigateDownKeyAriaLabel?: string
closeText?: string
closeKeyAriaLabel?: string
poweredByText?: string
}
export interface ErrorScreenTranslations {
titleText?: string
helpText?: string
}
export interface StartScreenTranslations {
recentSearchesTitle?: string
noRecentSearchesText?: string
saveRecentSearchButtonTitle?: string
removeRecentSearchButtonTitle?: string
favoriteSearchesTitle?: string
removeFavoriteSearchButtonTitle?: string
recentConversationsTitle?: string
removeRecentConversationButtonTitle?: string
}
export interface ResultsScreenTranslations {
askAiPlaceholder?: string
}
export interface NoResultsScreenTranslations {
noResultsText?: string
suggestedQueryText?: string
reportMissingResultsText?: string
reportMissingResultsLinkText?: string
}
export interface AskAiScreenTranslations {
disclaimerText?: string
relatedSourcesText?: string
thinkingText?: string
copyButtonText?: string
copyButtonCopiedText?: string
copyButtonTitle?: string
likeButtonTitle?: string
dislikeButtonTitle?: string
thanksForFeedbackText?: string
preToolCallText?: string
duringToolCallText?: string
afterToolCallText?: string
aggregatedToolCallText?: string
}
export interface DocSearchAskAi {
import { type DocSearchProps as DocSearchPropsJS } from '@docsearch/js'
import { type SidepanelProps as SidepanelPropsBase } from '@docsearch/sidepanel-js'
export type DocSearchProps = Partial<
Pick<
DocSearchPropsJS,
| 'appId'
| 'apiKey'
| 'placeholder'
| 'maxResultsPerGroup'
| 'disableUserPersonalization'
| 'initialQuery'
| 'translations'
| 'recentSearchesLimit'
| 'recentSearchesWithFavoritesLimit'
>
> & {
/**
* The index name to use for the ask AI feature. Your assistant will search this index for relevant documents.
* If not provided, the index name will be used.
* Name of the algolia index to query.
*/
indexName?: string
/**
* The API key to use for the ask AI feature. Your assistant will use this API key to search the index.
* If not provided, the API key will be used.
* Additional algolia search parameters to merge into each query.
*/
apiKey?: string
searchParameters?: DocSearchPropsJS['searchParameters']
/**
* The app ID to use for the ask AI feature. Your assistant will use this app ID to search the index.
* If not provided, the app ID will be used.
* Insights client integration options to send analytics events.
*/
appId?: string
insights?: boolean
/**
* The assistant ID to use for the ask AI feature.
* Configuration or assistant id to enable ask ai mode. Pass a string assistant id or a full config object.
*/
assistantId: string | null
askAi?: DocSearchAskAi | string
/**
* Ask AI side panel integration mode.
*
* - 'auto': infer hybrid vs sidePanel-only from provided config
* - 'sidePanel': force sidePanel-only even if keyword search is configured
* - 'hybrid': force hybrid (error if keyword search is not configured)
* - 'modal': force modal even if sidePanel is configured (ask ai in modal stays in modal)
*
* @default 'auto'
*/
mode?: 'auto' | 'sidePanel' | 'hybrid' | 'modal'
}
export type DocSearchAskAi = Partial<
Exclude<DocSearchPropsJS['askAi'], string | undefined>
> & {
/**
* Ask AI side panel configuration.
*/
sidePanel?: boolean | SidepanelProps
}
export type SidepanelProps = Partial<
Pick<SidepanelPropsBase, 'button' | 'keyboardShortcuts'>
> & {
/**
* The search parameters to use for the ask AI feature.
* Props specific to the Sidepanel panel.
*/
searchParameters?: {
facetFilters?: SearchOptions['facetFilters']
}
panel?: Omit<NonNullable<SidepanelPropsBase['panel']>, 'portalContainer'>
}

Loading…
Cancel
Save