@docusaurus/theme-search-algolia 3.8.1 → 3.9.0-canary-6403

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,12 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import {translate} from '@docusaurus/Translate';
8
+ // TODO Docusaurus v4: require DocSearch v4
9
+ // This needs to be cleaned after the upgrade
10
+ // Docusaurus v3 was made compatible with both DocSearch v3 and v4
11
+ // This implies that labels have been kept retro-compatible with v3
12
+ // Once we upgrade, we should be able to rely on v4 types only
13
+ // and remove v3 retro-compatibility labels that do not exist anymore in v4
8
14
  const translations = {
9
15
  button: {
10
16
  buttonText: translate({
@@ -40,6 +46,68 @@ const translations = {
40
46
  message: 'Cancel',
41
47
  description: 'The label and ARIA label for search box cancel button',
42
48
  }),
49
+ // v4
50
+ clearButtonTitle: translate({
51
+ id: 'theme.SearchModal.searchBox.resetButtonTitle',
52
+ message: 'Clear the query',
53
+ description: 'The label and ARIA label for search box reset button',
54
+ }),
55
+ clearButtonAriaLabel: translate({
56
+ id: 'theme.SearchModal.searchBox.resetButtonTitle',
57
+ message: 'Clear the query',
58
+ description: 'The label and ARIA label for search box reset button',
59
+ }),
60
+ closeButtonText: translate({
61
+ id: 'theme.SearchModal.searchBox.cancelButtonText',
62
+ message: 'Cancel',
63
+ description: 'The label and ARIA label for search box cancel button',
64
+ }),
65
+ closeButtonAriaLabel: translate({
66
+ id: 'theme.SearchModal.searchBox.cancelButtonText',
67
+ message: 'Cancel',
68
+ description: 'The label and ARIA label for search box cancel button',
69
+ }),
70
+ placeholderText: translate({
71
+ id: 'theme.SearchModal.searchBox.placeholderText',
72
+ message: 'Search docs',
73
+ description: 'The placeholder text for the main search input field',
74
+ }),
75
+ placeholderTextAskAi: translate({
76
+ id: 'theme.SearchModal.searchBox.placeholderTextAskAi',
77
+ message: 'Ask another question...',
78
+ description: 'The placeholder text when in AI question mode',
79
+ }),
80
+ placeholderTextAskAiStreaming: translate({
81
+ id: 'theme.SearchModal.searchBox.placeholderTextAskAiStreaming',
82
+ message: 'Answering...',
83
+ description:
84
+ 'The placeholder text for search box when AI is streaming an answer',
85
+ }),
86
+ enterKeyHint: translate({
87
+ id: 'theme.SearchModal.searchBox.enterKeyHint',
88
+ message: 'search',
89
+ description: 'The hint for the search box enter key text',
90
+ }),
91
+ enterKeyHintAskAi: translate({
92
+ id: 'theme.SearchModal.searchBox.enterKeyHintAskAi',
93
+ message: 'enter',
94
+ description: 'The hint for the Ask AI search box enter key text',
95
+ }),
96
+ searchInputLabel: translate({
97
+ id: 'theme.SearchModal.searchBox.searchInputLabel',
98
+ message: 'Search',
99
+ description: 'The ARIA label for search input',
100
+ }),
101
+ backToKeywordSearchButtonText: translate({
102
+ id: 'theme.SearchModal.searchBox.backToKeywordSearchButtonText',
103
+ message: 'Back to keyword search',
104
+ description: 'The text for back to keyword search button',
105
+ }),
106
+ backToKeywordSearchButtonAriaLabel: translate({
107
+ id: 'theme.SearchModal.searchBox.backToKeywordSearchButtonAriaLabel',
108
+ message: 'Back to keyword search',
109
+ description: 'The ARIA label for back to keyword search button',
110
+ }),
43
111
  },
44
112
  startScreen: {
45
113
  recentSearchesTitle: translate({
@@ -50,17 +118,17 @@ const translations = {
50
118
  noRecentSearchesText: translate({
51
119
  id: 'theme.SearchModal.startScreen.noRecentSearchesText',
52
120
  message: 'No recent searches',
53
- description: 'The text when no recent searches',
121
+ description: 'The text when there are no recent searches',
54
122
  }),
55
123
  saveRecentSearchButtonTitle: translate({
56
124
  id: 'theme.SearchModal.startScreen.saveRecentSearchButtonTitle',
57
125
  message: 'Save this search',
58
- description: 'The label for save recent search button',
126
+ description: 'The title for save recent search button',
59
127
  }),
60
128
  removeRecentSearchButtonTitle: translate({
61
129
  id: 'theme.SearchModal.startScreen.removeRecentSearchButtonTitle',
62
130
  message: 'Remove this search from history',
63
- description: 'The label for remove recent search button',
131
+ description: 'The title for remove recent search button',
64
132
  }),
65
133
  favoriteSearchesTitle: translate({
66
134
  id: 'theme.SearchModal.startScreen.favoriteSearchesTitle',
@@ -70,91 +138,178 @@ const translations = {
70
138
  removeFavoriteSearchButtonTitle: translate({
71
139
  id: 'theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle',
72
140
  message: 'Remove this search from favorites',
73
- description: 'The label for remove favorite search button',
141
+ description: 'The title for remove favorite search button',
142
+ }),
143
+ recentConversationsTitle: translate({
144
+ id: 'theme.SearchModal.startScreen.recentConversationsTitle',
145
+ message: 'Recent conversations',
146
+ description: 'The title for recent conversations',
147
+ }),
148
+ removeRecentConversationButtonTitle: translate({
149
+ id: 'theme.SearchModal.startScreen.removeRecentConversationButtonTitle',
150
+ message: 'Remove this conversation from history',
151
+ description: 'The title for remove recent conversation button',
74
152
  }),
75
153
  },
76
154
  errorScreen: {
77
155
  titleText: translate({
78
156
  id: 'theme.SearchModal.errorScreen.titleText',
79
157
  message: 'Unable to fetch results',
80
- description: 'The title for error screen of search modal',
158
+ description: 'The title for error screen',
81
159
  }),
82
160
  helpText: translate({
83
161
  id: 'theme.SearchModal.errorScreen.helpText',
84
162
  message: 'You might want to check your network connection.',
85
- description: 'The help text for error screen of search modal',
163
+ description: 'The help text for error screen',
164
+ }),
165
+ },
166
+ resultsScreen: {
167
+ askAiPlaceholder: translate({
168
+ id: 'theme.SearchModal.resultsScreen.askAiPlaceholder',
169
+ message: 'Ask AI: ',
170
+ description: 'The placeholder text for Ask AI input',
171
+ }),
172
+ },
173
+ askAiScreen: {
174
+ disclaimerText: translate({
175
+ id: 'theme.SearchModal.askAiScreen.disclaimerText',
176
+ message:
177
+ 'Answers are generated with AI which can make mistakes. Verify responses.',
178
+ description: 'The disclaimer text for AI answers',
179
+ }),
180
+ relatedSourcesText: translate({
181
+ id: 'theme.SearchModal.askAiScreen.relatedSourcesText',
182
+ message: 'Related sources',
183
+ description: 'The text for related sources',
184
+ }),
185
+ thinkingText: translate({
186
+ id: 'theme.SearchModal.askAiScreen.thinkingText',
187
+ message: 'Thinking...',
188
+ description: 'The text when AI is thinking',
189
+ }),
190
+ copyButtonText: translate({
191
+ id: 'theme.SearchModal.askAiScreen.copyButtonText',
192
+ message: 'Copy',
193
+ description: 'The text for copy button',
194
+ }),
195
+ copyButtonCopiedText: translate({
196
+ id: 'theme.SearchModal.askAiScreen.copyButtonCopiedText',
197
+ message: 'Copied!',
198
+ description: 'The text for copy button when copied',
199
+ }),
200
+ copyButtonTitle: translate({
201
+ id: 'theme.SearchModal.askAiScreen.copyButtonTitle',
202
+ message: 'Copy',
203
+ description: 'The title for copy button',
204
+ }),
205
+ likeButtonTitle: translate({
206
+ id: 'theme.SearchModal.askAiScreen.likeButtonTitle',
207
+ message: 'Like',
208
+ description: 'The title for like button',
209
+ }),
210
+ dislikeButtonTitle: translate({
211
+ id: 'theme.SearchModal.askAiScreen.dislikeButtonTitle',
212
+ message: 'Dislike',
213
+ description: 'The title for dislike button',
214
+ }),
215
+ thanksForFeedbackText: translate({
216
+ id: 'theme.SearchModal.askAiScreen.thanksForFeedbackText',
217
+ message: 'Thanks for your feedback!',
218
+ description: 'The text for thanks for feedback',
219
+ }),
220
+ preToolCallText: translate({
221
+ id: 'theme.SearchModal.askAiScreen.preToolCallText',
222
+ message: 'Searching...',
223
+ description: 'The text before tool call',
224
+ }),
225
+ duringToolCallText: translate({
226
+ id: 'theme.SearchModal.askAiScreen.duringToolCallText',
227
+ message: 'Searching for ',
228
+ description: 'The text during tool call',
229
+ }),
230
+ afterToolCallText: translate({
231
+ id: 'theme.SearchModal.askAiScreen.afterToolCallText',
232
+ message: 'Searched for',
233
+ description: 'The text after tool call',
86
234
  }),
87
235
  },
88
236
  footer: {
89
237
  selectText: translate({
90
238
  id: 'theme.SearchModal.footer.selectText',
91
- message: 'to select',
92
- description: 'The explanatory text of the action for the enter key',
239
+ message: 'Select',
240
+ description: 'The select text for footer',
241
+ }),
242
+ submitQuestionText: translate({
243
+ id: 'theme.SearchModal.footer.submitQuestionText',
244
+ message: 'Submit question',
245
+ description: 'The submit question text for footer',
93
246
  }),
94
247
  selectKeyAriaLabel: translate({
95
248
  id: 'theme.SearchModal.footer.selectKeyAriaLabel',
96
249
  message: 'Enter key',
97
- description:
98
- 'The ARIA label for the Enter key button that makes the selection',
250
+ description: 'The ARIA label for select key in footer',
99
251
  }),
100
252
  navigateText: translate({
101
253
  id: 'theme.SearchModal.footer.navigateText',
102
- message: 'to navigate',
103
- description:
104
- 'The explanatory text of the action for the Arrow up and Arrow down key',
254
+ message: 'Navigate',
255
+ description: 'The navigate text for footer',
105
256
  }),
106
257
  navigateUpKeyAriaLabel: translate({
107
258
  id: 'theme.SearchModal.footer.navigateUpKeyAriaLabel',
108
259
  message: 'Arrow up',
109
- description:
110
- 'The ARIA label for the Arrow up key button that makes the navigation',
260
+ description: 'The ARIA label for navigate up key in footer',
111
261
  }),
112
262
  navigateDownKeyAriaLabel: translate({
113
263
  id: 'theme.SearchModal.footer.navigateDownKeyAriaLabel',
114
264
  message: 'Arrow down',
115
- description:
116
- 'The ARIA label for the Arrow down key button that makes the navigation',
265
+ description: 'The ARIA label for navigate down key in footer',
117
266
  }),
118
267
  closeText: translate({
119
268
  id: 'theme.SearchModal.footer.closeText',
120
- message: 'to close',
121
- description: 'The explanatory text of the action for Escape key',
269
+ message: 'Close',
270
+ description: 'The close text for footer',
122
271
  }),
123
272
  closeKeyAriaLabel: translate({
124
273
  id: 'theme.SearchModal.footer.closeKeyAriaLabel',
125
274
  message: 'Escape key',
126
- description:
127
- 'The ARIA label for the Escape key button that close the modal',
275
+ description: 'The ARIA label for close key in footer',
276
+ }),
277
+ poweredByText: translate({
278
+ id: 'theme.SearchModal.footer.searchByText',
279
+ message: 'Powered by',
280
+ description: "The 'Powered by' text for footer",
128
281
  }),
129
282
  searchByText: translate({
130
283
  id: 'theme.SearchModal.footer.searchByText',
131
- message: 'Search by',
132
- description: 'The text explain that the search is making by Algolia',
284
+ message: 'Powered by',
285
+ description: "The 'Powered by' text for footer",
286
+ }),
287
+ backToSearchText: translate({
288
+ id: 'theme.SearchModal.footer.backToSearchText',
289
+ message: 'Back to search',
290
+ description: 'The back to search text for footer',
133
291
  }),
134
292
  },
135
293
  noResultsScreen: {
136
294
  noResultsText: translate({
137
295
  id: 'theme.SearchModal.noResultsScreen.noResultsText',
138
- message: 'No results for',
139
- description:
140
- 'The text explains that there are no results for the following search',
296
+ message: 'No results found for',
297
+ description: 'The text when there are no results',
141
298
  }),
142
299
  suggestedQueryText: translate({
143
300
  id: 'theme.SearchModal.noResultsScreen.suggestedQueryText',
144
301
  message: 'Try searching for',
145
- description:
146
- 'The text for the suggested query when no results are found for the following search',
302
+ description: 'The text for suggested query',
147
303
  }),
148
304
  reportMissingResultsText: translate({
149
305
  id: 'theme.SearchModal.noResultsScreen.reportMissingResultsText',
150
306
  message: 'Believe this query should return results?',
151
- description:
152
- 'The text for the question where the user thinks there are missing results',
307
+ description: 'The text for reporting missing results',
153
308
  }),
154
309
  reportMissingResultsLinkText: translate({
155
310
  id: 'theme.SearchModal.noResultsScreen.reportMissingResultsLinkText',
156
311
  message: 'Let us know.',
157
- description: 'The text for the link to report missing results',
312
+ description: 'The link text for reporting missing results',
158
313
  }),
159
314
  },
160
315
  },
@@ -5,11 +5,12 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import { Joi } from '@docusaurus/utils-validation';
8
- import type { ThemeConfig, ThemeConfigValidationContext } from '@docusaurus/types';
8
+ import type { ThemeConfigValidationContext } from '@docusaurus/types';
9
+ import type { ThemeConfig } from '@docusaurus/theme-search-algolia';
9
10
  export declare const DEFAULT_CONFIG: {
10
- contextualSearch: boolean;
11
+ contextualSearch: true;
11
12
  searchParameters: {};
12
13
  searchPagePath: string;
13
14
  };
14
15
  export declare const Schema: Joi.ObjectSchema<ThemeConfig>;
15
- export declare function validateThemeConfig({ validate, themeConfig, }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig;
16
+ export declare function validateThemeConfig({ validate, themeConfig: themeConfigInput, }: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig;
@@ -10,6 +10,7 @@ exports.Schema = exports.DEFAULT_CONFIG = void 0;
10
10
  exports.validateThemeConfig = validateThemeConfig;
11
11
  const utils_1 = require("@docusaurus/utils");
12
12
  const utils_validation_1 = require("@docusaurus/utils-validation");
13
+ const docSearchVersion_1 = require("./docSearchVersion");
13
14
  exports.DEFAULT_CONFIG = {
14
15
  // Enabled by default, as it makes sense in most cases
15
16
  // see also https://github.com/facebook/docusaurus/issues/5880
@@ -17,6 +18,7 @@ exports.DEFAULT_CONFIG = {
17
18
  searchParameters: {},
18
19
  searchPagePath: 'search',
19
20
  };
21
+ const FacetFiltersSchema = utils_validation_1.Joi.array().items(utils_validation_1.Joi.alternatives().try(utils_validation_1.Joi.string(), utils_validation_1.Joi.array().items(utils_validation_1.Joi.string())));
20
22
  exports.Schema = utils_validation_1.Joi.object({
21
23
  algolia: utils_validation_1.Joi.object({
22
24
  // Docusaurus attributes
@@ -28,7 +30,9 @@ exports.Schema = utils_validation_1.Joi.object({
28
30
  }),
29
31
  apiKey: utils_validation_1.Joi.string().required(),
30
32
  indexName: utils_validation_1.Joi.string().required(),
31
- searchParameters: utils_validation_1.Joi.object()
33
+ searchParameters: utils_validation_1.Joi.object({
34
+ facetFilters: FacetFiltersSchema.optional(),
35
+ })
32
36
  .default(exports.DEFAULT_CONFIG.searchParameters)
33
37
  .unknown(),
34
38
  searchPagePath: utils_validation_1.Joi.alternatives()
@@ -47,11 +51,69 @@ exports.Schema = utils_validation_1.Joi.object({
47
51
  }).required(),
48
52
  to: utils_validation_1.Joi.string().required(),
49
53
  }).optional(),
54
+ // Ask AI configuration (DocSearch v4 only)
55
+ askAi: utils_validation_1.Joi.alternatives()
56
+ .try(
57
+ // Simple string format (assistantId only)
58
+ utils_validation_1.Joi.string(),
59
+ // Full configuration object
60
+ utils_validation_1.Joi.object({
61
+ indexName: utils_validation_1.Joi.string().required(),
62
+ apiKey: utils_validation_1.Joi.string().required(),
63
+ appId: utils_validation_1.Joi.string().required(),
64
+ assistantId: utils_validation_1.Joi.string().required(),
65
+ searchParameters: utils_validation_1.Joi.object({
66
+ facetFilters: FacetFiltersSchema.optional(),
67
+ }).optional(),
68
+ }))
69
+ .custom((askAiInput, helpers) => {
70
+ if (!askAiInput) {
71
+ return askAiInput;
72
+ }
73
+ const algolia = helpers.state.ancestors[0];
74
+ const algoliaFacetFilters = algolia.searchParameters?.facetFilters;
75
+ if (typeof askAiInput === 'string') {
76
+ return {
77
+ assistantId: askAiInput,
78
+ indexName: algolia.indexName,
79
+ apiKey: algolia.apiKey,
80
+ appId: algolia.appId,
81
+ ...(algoliaFacetFilters
82
+ ? {
83
+ searchParameters: {
84
+ facetFilters: algoliaFacetFilters,
85
+ },
86
+ }
87
+ : {}),
88
+ };
89
+ }
90
+ if (askAiInput.searchParameters?.facetFilters === undefined &&
91
+ algoliaFacetFilters) {
92
+ askAiInput.searchParameters = askAiInput.searchParameters ?? {};
93
+ askAiInput.searchParameters.facetFilters = algoliaFacetFilters;
94
+ }
95
+ return askAiInput;
96
+ })
97
+ .optional()
98
+ .messages({
99
+ 'alternatives.types': 'askAi must be either a string (assistantId) or an object with indexName, apiKey, appId, and assistantId',
100
+ }),
50
101
  })
51
102
  .label('themeConfig.algolia')
52
103
  .required()
53
- .unknown(), // DocSearch 3 is still alpha: don't validate the rest for now
104
+ .unknown(),
54
105
  });
55
- function validateThemeConfig({ validate, themeConfig, }) {
56
- return validate(exports.Schema, themeConfig);
106
+ // TODO Docusaurus v4: remove this check when we drop DocSearch v3
107
+ function ensureAskAISupported(themeConfig) {
108
+ // enforce DocsSearch v4 requirement when AskAI is configured
109
+ if (themeConfig.algolia.askAi && docSearchVersion_1.docSearchV3) {
110
+ throw new Error('The askAi feature is only supported in DocSearch v4. ' +
111
+ 'Please upgrade to DocSearch v4 by installing "@docsearch/react": "^4.0.0" ' +
112
+ 'or remove the askAi configuration from your theme config.');
113
+ }
114
+ }
115
+ function validateThemeConfig({ validate, themeConfig: themeConfigInput, }) {
116
+ const themeConfig = validate(exports.Schema, themeConfigInput);
117
+ ensureAskAISupported(themeConfig);
118
+ return themeConfig;
57
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docusaurus/theme-search-algolia",
3
- "version": "3.8.1",
3
+ "version": "3.9.0-canary-6403",
4
4
  "description": "Algolia search component for Docusaurus.",
5
5
  "main": "lib/index.js",
6
6
  "sideEffects": [
@@ -33,16 +33,16 @@
33
33
  "copy:watch": "node ../../admin/scripts/copyUntypedFiles.js --watch"
34
34
  },
35
35
  "dependencies": {
36
- "@docsearch/react": "^3.9.0",
37
- "@docusaurus/core": "3.8.1",
38
- "@docusaurus/logger": "3.8.1",
39
- "@docusaurus/plugin-content-docs": "3.8.1",
40
- "@docusaurus/theme-common": "3.8.1",
41
- "@docusaurus/theme-translations": "3.8.1",
42
- "@docusaurus/utils": "3.8.1",
43
- "@docusaurus/utils-validation": "3.8.1",
44
- "algoliasearch": "^5.17.1",
45
- "algoliasearch-helper": "^3.22.6",
36
+ "@docsearch/react": "^3.9.0 || ^4.1.0",
37
+ "@docusaurus/core": "3.9.0-canary-6403",
38
+ "@docusaurus/logger": "3.9.0-canary-6403",
39
+ "@docusaurus/plugin-content-docs": "3.9.0-canary-6403",
40
+ "@docusaurus/theme-common": "3.9.0-canary-6403",
41
+ "@docusaurus/theme-translations": "3.9.0-canary-6403",
42
+ "@docusaurus/utils": "3.9.0-canary-6403",
43
+ "@docusaurus/utils-validation": "3.9.0-canary-6403",
44
+ "algoliasearch": "^5.37.0",
45
+ "algoliasearch-helper": "^3.26.0",
46
46
  "clsx": "^2.0.0",
47
47
  "eta": "^2.2.0",
48
48
  "fs-extra": "^11.1.1",
@@ -51,14 +51,14 @@
51
51
  "utility-types": "^3.10.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@docusaurus/module-type-aliases": "3.8.1"
54
+ "@docusaurus/module-type-aliases": "3.9.0-canary-6403"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "react": "^18.0.0 || ^19.0.0",
58
58
  "react-dom": "^18.0.0 || ^19.0.0"
59
59
  },
60
60
  "engines": {
61
- "node": ">=18.0"
61
+ "node": ">=20.0"
62
62
  },
63
- "gitHead": "fa8ae13e668fcbc0481ce10c0a734e2a5b397293"
63
+ "gitHead": "22c95f8cb5c070ce47e115be376fa9b828dd54df"
64
64
  }
@@ -6,5 +6,10 @@
6
6
  */
7
7
 
8
8
  export {useAlgoliaThemeConfig} from './useAlgoliaThemeConfig';
9
- export {useAlgoliaContextualFacetFilters} from './useAlgoliaContextualFacetFilters';
9
+ export {
10
+ useAlgoliaContextualFacetFilters,
11
+ useAlgoliaContextualFacetFiltersIfEnabled,
12
+ } from './useAlgoliaContextualFacetFilters';
10
13
  export {useSearchResultUrlProcessor} from './useSearchResultUrlProcessor';
14
+ export {useAlgoliaAskAi} from './useAlgoliaAskAi';
15
+ export {mergeFacetFilters} from './utils';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import {useCallback, useMemo, useState} from 'react';
9
+ import {version as docsearchVersion} from '@docsearch/react/version';
10
+ import translations from '@theme/SearchTranslations';
11
+ import {useAlgoliaContextualFacetFiltersIfEnabled} from './useAlgoliaContextualFacetFilters';
12
+ import {mergeFacetFilters} from './utils';
13
+ import type {AskAiConfig} from '@docusaurus/theme-search-algolia';
14
+ import type {
15
+ DocSearchModalProps,
16
+ DocSearchTranslations,
17
+ } from '@docsearch/react';
18
+ import type {FacetFilters} from 'algoliasearch/lite';
19
+
20
+ // The minimal props the hook needs from DocSearch v4 props
21
+ // TODO Docusaurus v4: cleanup after we drop support for DocSearch v3
22
+ interface DocSearchV4PropsLite {
23
+ indexName: string;
24
+ apiKey: string;
25
+ appId: string;
26
+ placeholder?: string;
27
+ translations?: DocSearchTranslations;
28
+ searchParameters?: DocSearchModalProps['searchParameters'];
29
+ askAi?: AskAiConfig;
30
+ }
31
+
32
+ const isV4 = docsearchVersion.startsWith('4.');
33
+
34
+ type UseAskAiResult = {
35
+ canHandleAskAi: boolean;
36
+ isAskAiActive: boolean;
37
+ currentPlaceholder: string | undefined;
38
+ onAskAiToggle: (active: boolean) => void;
39
+ askAi?: AskAiConfig;
40
+ extraAskAiProps: Partial<DocSearchModalProps> & {
41
+ askAi?: AskAiConfig;
42
+ canHandleAskAi?: boolean;
43
+ isAskAiActive?: boolean;
44
+ onAskAiToggle?: (active: boolean) => void;
45
+ };
46
+ };
47
+
48
+ // We need to apply contextualSearch facetFilters to AskAI filters
49
+ // This can't be done at config normalization time because contextual filters
50
+ // can only be determined at runtime
51
+ function applyAskAiContextualSearch(
52
+ askAi: AskAiConfig | undefined,
53
+ contextualSearchFilters: FacetFilters | undefined,
54
+ ): AskAiConfig | undefined {
55
+ if (!askAi) {
56
+ return undefined;
57
+ }
58
+ if (!contextualSearchFilters) {
59
+ return askAi;
60
+ }
61
+ const askAiFacetFilters = askAi.searchParameters?.facetFilters;
62
+ return {
63
+ ...askAi,
64
+ searchParameters: {
65
+ ...askAi.searchParameters,
66
+ facetFilters: mergeFacetFilters(
67
+ askAiFacetFilters,
68
+ contextualSearchFilters,
69
+ ),
70
+ },
71
+ };
72
+ }
73
+
74
+ export function useAlgoliaAskAi(props: DocSearchV4PropsLite): UseAskAiResult {
75
+ const [isAskAiActive, setIsAskAiActive] = useState(false);
76
+ const contextualSearchFilters = useAlgoliaContextualFacetFiltersIfEnabled();
77
+
78
+ const askAi = useMemo(() => {
79
+ return applyAskAiContextualSearch(props.askAi, contextualSearchFilters);
80
+ }, [props.askAi, contextualSearchFilters]);
81
+
82
+ const canHandleAskAi = Boolean(askAi);
83
+
84
+ const currentPlaceholder =
85
+ isAskAiActive && isV4
86
+ ? translations.modal?.searchBox?.placeholderTextAskAi
87
+ : translations.modal?.searchBox?.placeholderText || props?.placeholder;
88
+
89
+ const onAskAiToggle = useCallback((askAiToggle: boolean) => {
90
+ setIsAskAiActive(askAiToggle);
91
+ }, []);
92
+
93
+ const extraAskAiProps: UseAskAiResult['extraAskAiProps'] = {
94
+ askAi,
95
+ canHandleAskAi,
96
+ isAskAiActive,
97
+ onAskAiToggle,
98
+ };
99
+
100
+ return {
101
+ canHandleAskAi,
102
+ isAskAiActive,
103
+ currentPlaceholder,
104
+ onAskAiToggle,
105
+ askAi,
106
+ extraAskAiProps,
107
+ };
108
+ }
@@ -8,6 +8,8 @@
8
8
  import {DEFAULT_SEARCH_TAG} from '@docusaurus/theme-common/internal';
9
9
  import {useDocsContextualSearchTags} from '@docusaurus/plugin-content-docs/client';
10
10
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
11
+ import {useAlgoliaThemeConfig} from './useAlgoliaThemeConfig';
12
+ import type {FacetFilters} from 'algoliasearch/lite';
11
13
 
12
14
  function useSearchTags() {
13
15
  // only docs have custom search tags per version
@@ -16,7 +18,7 @@ function useSearchTags() {
16
18
  }
17
19
 
18
20
  // Translate search-engine agnostic search tags to Algolia search filters
19
- export function useAlgoliaContextualFacetFilters(): [string, string[]] {
21
+ export function useAlgoliaContextualFacetFilters(): FacetFilters {
20
22
  const locale = useDocusaurusContext().i18n.currentLocale;
21
23
  const tags = useSearchTags();
22
24
 
@@ -27,3 +29,17 @@ export function useAlgoliaContextualFacetFilters(): [string, string[]] {
27
29
 
28
30
  return [languageFilter, tagsFilter];
29
31
  }
32
+
33
+ export function useAlgoliaContextualFacetFiltersIfEnabled():
34
+ | FacetFilters
35
+ | undefined {
36
+ const {
37
+ algolia: {contextualSearch},
38
+ } = useAlgoliaThemeConfig();
39
+ const facetFilters = useAlgoliaContextualFacetFilters();
40
+ if (contextualSearch) {
41
+ return facetFilters;
42
+ } else {
43
+ return undefined;
44
+ }
45
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import type {FacetFilters} from 'algoliasearch/lite';
9
+
10
+ export function mergeFacetFilters(
11
+ f1: FacetFilters,
12
+ f2: FacetFilters,
13
+ ): FacetFilters;
14
+
15
+ export function mergeFacetFilters(
16
+ f1: FacetFilters | undefined,
17
+ f2: FacetFilters | undefined,
18
+ ): FacetFilters | undefined;
19
+
20
+ export function mergeFacetFilters(
21
+ f1: FacetFilters | undefined,
22
+ f2: FacetFilters | undefined,
23
+ ): FacetFilters | undefined {
24
+ if (f1 === undefined) {
25
+ return f2;
26
+ }
27
+ if (f2 === undefined) {
28
+ return f1;
29
+ }
30
+
31
+ const normalize = (f: FacetFilters): FacetFilters =>
32
+ typeof f === 'string' ? [f] : f;
33
+
34
+ // Historical behavior: we flatten everything
35
+ // TODO I'm pretty sure this is incorrect
36
+ // see https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/?client=javascript
37
+ // Note: Algolia is working to provide a reliable facet merging strategy
38
+ // see https://github.com/facebook/docusaurus/pull/11327#issuecomment-3284742923
39
+ return [...normalize(f1), ...normalize(f2)];
40
+ }