@easyops-cn/docusaurus-search-local 0.52.2 → 0.53.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [0.53.0](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.52.3...v0.53.0) (2026-02-05)
6
+
7
+
8
+ ### Features
9
+
10
+ * support Ask AI ([8a5d29e](https://github.com/easyops-cn/docusaurus-search-local/commit/8a5d29e2e5b3b1cb534b1bbf13075f6824307d2e))
11
+ * support Ask AI ([3b2e339](https://github.com/easyops-cn/docusaurus-search-local/commit/3b2e339a04ac9347e73fe39d63237ffa3e63242a))
12
+
13
+ ## [0.52.3](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.52.2...v0.52.3) (2026-01-29)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * replace useDocsPreferredVersion with useActiveVersion ([7e72ef4](https://github.com/easyops-cn/docusaurus-search-local/commit/7e72ef42c8605f4f4147b2227decc9865ac41afd))
19
+ * replace useDocsPreferredVersion with useActiveVersion ([15ba30d](https://github.com/easyops-cn/docusaurus-search-local/commit/15ba30d8c4aab85984be337f227b3ab166daa93e))
20
+
5
21
  ## [0.52.2](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.52.1...v0.52.2) (2025-12-01)
6
22
 
7
23
 
@@ -4,12 +4,14 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
4
4
  import useIsBrowser from "@docusaurus/useIsBrowser";
5
5
  import { useHistory, useLocation } from "@docusaurus/router";
6
6
  import { translate } from "@docusaurus/Translate";
7
- import { ReactContextError, useDocsPreferredVersion, } from "@docusaurus/theme-common";
8
- import { useActivePlugin } from "@docusaurus/plugin-content-docs/client";
7
+ import { useActivePlugin, useActiveVersion, } from "@docusaurus/plugin-content-docs/client";
8
+ import { AskAIWidget } from "open-ask-ai";
9
+ import "open-ask-ai/styles.css";
9
10
  import { fetchIndexesByWorker, searchByWorker } from "../searchByWorker";
10
11
  import { SuggestionTemplate } from "./SuggestionTemplate";
11
12
  import { EmptyTemplate } from "./EmptyTemplate";
12
- import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated";
13
+ import { SearchDocumentType } from "../../../shared/interfaces";
14
+ import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, askAi, } from "../../utils/proxiedGenerated";
13
15
  import LoadingRing from "../LoadingRing/LoadingRing";
14
16
  import { normalizeContextByPath } from "../../utils/normalizeContextByPath";
15
17
  import { searchResultLimits } from "../../utils/proxiedGeneratedConstants";
@@ -36,29 +38,9 @@ export default function SearchBar({ handleSearchBarToggle, }) {
36
38
  // It returns undefined for non-docs pages
37
39
  const activePlugin = useActivePlugin();
38
40
  let versionUrl = baseUrl;
39
- // For non-docs pages while using plugin-content-docs with custom ids,
40
- // this will throw an error of:
41
- // > Docusaurus plugin global data not found for "docusaurus-plugin-content-docs" plugin with id "default".
42
- // It seems that we can not get the correct id for non-docs pages.
43
- try {
44
- // The try-catch is a hack because useDocsPreferredVersion just throws an
45
- // exception when versions are not used.
46
- // The same hack is used in SearchPage.tsx
47
- // eslint-disable-next-line react-hooks/rules-of-hooks
48
- const { preferredVersion } = useDocsPreferredVersion(activePlugin?.pluginId ?? docsPluginIdForPreferredVersion);
49
- if (preferredVersion && !preferredVersion.isLast) {
50
- versionUrl = preferredVersion.path + "/";
51
- }
52
- }
53
- catch (e) {
54
- if (indexDocs) {
55
- if (e instanceof ReactContextError) {
56
- /* ignore, happens when website doesn't use versions */
57
- }
58
- else {
59
- throw e;
60
- }
61
- }
41
+ const activeVersion = useActiveVersion(activePlugin?.pluginId ?? docsPluginIdForPreferredVersion);
42
+ if (activeVersion && !activeVersion.isLast) {
43
+ versionUrl = activeVersion.path + "/";
62
44
  }
63
45
  const history = useHistory();
64
46
  const location = useLocation();
@@ -70,6 +52,7 @@ export default function SearchBar({ handleSearchBarToggle, }) {
70
52
  const [inputChanged, setInputChanged] = useState(false);
71
53
  const [inputValue, setInputValue] = useState("");
72
54
  const search = useRef(null);
55
+ const askAIWidgetRef = useRef(null);
73
56
  const prevSearchContext = useRef("");
74
57
  const [searchContext, setSearchContext] = useState("");
75
58
  const prevVersionUrl = useRef(baseUrl);
@@ -197,7 +180,25 @@ export default function SearchBar({ handleSearchBarToggle, }) {
197
180
  {
198
181
  source: async (input, callback) => {
199
182
  const result = await searchByWorker(versionUrl, searchContext, input, searchResultLimits);
200
- callback(result);
183
+ if (input && askAi) {
184
+ callback([
185
+ {
186
+ document: {
187
+ i: -1,
188
+ t: "",
189
+ u: "",
190
+ },
191
+ type: SearchDocumentType.AskAI,
192
+ page: undefined,
193
+ metadata: {},
194
+ tokens: [input],
195
+ },
196
+ ...result,
197
+ ]);
198
+ }
199
+ else {
200
+ callback(result);
201
+ }
201
202
  },
202
203
  templates: {
203
204
  suggestion: SuggestionTemplate,
@@ -216,8 +217,12 @@ export default function SearchBar({ handleSearchBarToggle, }) {
216
217
  },
217
218
  },
218
219
  ])
219
- .on("autocomplete:selected", function (event, { document: { u, h }, tokens }) {
220
+ .on("autocomplete:selected", function (event, { document: { u, h }, type, tokens }) {
220
221
  searchBarRef.current?.blur();
222
+ if (type === SearchDocumentType.AskAI && askAi) {
223
+ askAIWidgetRef.current?.openWithNewSession(tokens.join(""));
224
+ return;
225
+ }
221
226
  let url = u;
222
227
  if (Mark && tokens.length > 0) {
223
228
  const params = new URLSearchParams();
@@ -354,13 +359,19 @@ export default function SearchBar({ handleSearchBarToggle, }) {
354
359
  message: "Search",
355
360
  description: "The ARIA label and placeholder for search button",
356
361
  })} aria-label="Search" className={`navbar__search-input ${styles.searchInput}`} onMouseEnter={onInputMouseEnter} onFocus={onInputFocus} onBlur={onInputBlur} onChange={onInputChange} ref={searchBarRef} value={inputValue}/>
362
+ {askAi && (<AskAIWidget ref={askAIWidgetRef} {...askAi}>
363
+ <span hidden></span>
364
+ </AskAIWidget>)}
357
365
  <LoadingRing className={styles.searchBarLoadingRing}/>
358
366
  {searchBarShortcut &&
359
367
  searchBarShortcutHint &&
360
368
  (inputValue !== "" ? (<button className={styles.searchClearButton} onClick={onClearSearch}>
361
369
 
362
- </button>) : (isBrowser && searchBarShortcutKeymap && (<div className={styles.searchHintContainer}>
363
- {getKeymapHints(searchBarShortcutKeymap, isMac).map((hint, index) => (<kbd key={index} className={styles.searchHint}>{hint}</kbd>))}
370
+ </button>) : (isBrowser &&
371
+ searchBarShortcutKeymap && (<div className={styles.searchHintContainer}>
372
+ {getKeymapHints(searchBarShortcutKeymap, isMac).map((hint, index) => (<kbd key={index} className={styles.searchHint}>
373
+ {hint}
374
+ </kbd>))}
364
375
  </div>)))}
365
376
  </div>);
366
377
  }
@@ -17,10 +17,37 @@
17
17
  }
18
18
 
19
19
  .searchInput:focus {
20
- outline: 2px solid var(--search-local-input-active-border-color, var(--ifm-color-primary));
20
+ outline: 2px solid
21
+ var(--search-local-input-active-border-color, var(--ifm-color-primary));
21
22
  outline-offset: 0px;
22
23
  }
23
24
 
25
+ html[data-theme="dark"] div:global(.ask-ai),
26
+ div:global(.ask-ai) {
27
+ --ask-ai-primary: var(--ifm-color-primary);
28
+ --ask-ai-primary-hover: var(--ifm-color-primary-light);
29
+ --ask-ai-foreground: var(--ifm-color-content);
30
+ --ask-ai-border: var(--ifm-color-emphasis-300);
31
+ --ask-ai-error: var(--ifm-color-danger);
32
+ --ask-ai-button-bg: var(--ifm-color-emphasis-200);
33
+ }
34
+
35
+ :global(.ask-ai) {
36
+ --ask-ai-background: var(--search-local-modal-background, #f5f6f7);
37
+ --ask-ai-muted: var(--search-local-muted-color, #969faf);
38
+ }
39
+
40
+ html[data-theme="dark"] :global(.ask-ai) {
41
+ --ask-ai-background: var(
42
+ --search-local-modal-background,
43
+ var(--ifm-background-color)
44
+ );
45
+ --ask-ai-muted: var(
46
+ --search-local-muted-color,
47
+ var(--ifm-color-secondary-darkest)
48
+ );
49
+ }
50
+
24
51
  @media not (max-width: 996px) {
25
52
  .searchBar.searchBarLeft .dropdownMenu {
26
53
  left: 0 !important;
@@ -1,12 +1,21 @@
1
1
  import { SearchDocumentType, } from "../../../shared/interfaces";
2
2
  import { concatDocumentPath } from "../../utils/concatDocumentPath";
3
+ import { escapeHtml } from "../../utils/escapeHtml";
3
4
  import { getStemmedPositions } from "../../utils/getStemmedPositions";
4
5
  import { highlight } from "../../utils/highlight";
5
6
  import { highlightStemmed } from "../../utils/highlightStemmed";
6
7
  import { explicitSearchResultPath } from "../../utils/proxiedGenerated";
7
- import { iconAction, iconContent, iconHeading, iconTitle, iconTreeInter, iconTreeLast, } from "./icons";
8
+ import { iconAction, iconAskAI, iconContent, iconHeading, iconTitle, iconTreeInter, iconTreeLast, } from "./icons";
8
9
  import styles from "./SearchBar.module.css";
9
10
  export function SuggestionTemplate({ document, type, page, metadata, tokens, isInterOfTree, isLastOfTree, }) {
11
+ if (type === SearchDocumentType.AskAI) {
12
+ return [
13
+ `<span class="${styles.hitIcon}">${iconAskAI}</span>`,
14
+ `<span class="${styles.hitWrapper}">`,
15
+ `<span class="${styles.hitTitle}">Ask AI: <mark>${escapeHtml(tokens.join(" "))}</mark></span>`,
16
+ `</span>`,
17
+ ].join("");
18
+ }
10
19
  const isTitle = type === SearchDocumentType.Title;
11
20
  const isKeywords = type === SearchDocumentType.Keywords;
12
21
  const isTitleRelated = isTitle || isKeywords;
@@ -5,3 +5,4 @@ export const iconAction = '<svg width="20" height="20" viewBox="0 0 20 20"><g st
5
5
  export const iconNoResults = '<svg width="40" height="40" viewBox="0 0 20 20" fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15.5 4.8c2 3 1.7 7-1 9.7h0l4.3 4.3-4.3-4.3a7.8 7.8 0 01-9.8 1m-2.2-2.2A7.8 7.8 0 0113.2 2.4M2 18L18 2"></path></svg>';
6
6
  export const iconTreeInter = '<svg viewBox="0 0 24 54"><g stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6v42M20 27H8.3"></path></g></svg>';
7
7
  export const iconTreeLast = '<svg viewBox="0 0 24 54"><g stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6v21M20 27H8.3"></path></g></svg>';
8
+ export const iconAskAI = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles-icon lucide-sparkles"><path 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.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg>';
@@ -5,4 +5,5 @@ export var SearchDocumentType;
5
5
  SearchDocumentType[SearchDocumentType["Description"] = 2] = "Description";
6
6
  SearchDocumentType[SearchDocumentType["Keywords"] = 3] = "Keywords";
7
7
  SearchDocumentType[SearchDocumentType["Content"] = 4] = "Content";
8
+ SearchDocumentType[SearchDocumentType["AskAI"] = 5] = "AskAI";
8
9
  })(SearchDocumentType || (SearchDocumentType = {}));
@@ -6,7 +6,7 @@ const fs_1 = tslib_1.__importDefault(require("fs"));
6
6
  const path_1 = tslib_1.__importDefault(require("path"));
7
7
  const getIndexHash_1 = require("./getIndexHash");
8
8
  function generate(config, dir) {
9
- const { language, removeDefaultStopWordFilter, removeDefaultStemmer, highlightSearchTermsOnTargetPage, searchResultLimits, searchResultContextMaxLength, explicitSearchResultPath, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, fuzzyMatchingDistance, } = config;
9
+ const { language, removeDefaultStopWordFilter, removeDefaultStemmer, highlightSearchTermsOnTargetPage, searchResultLimits, searchResultContextMaxLength, explicitSearchResultPath, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, fuzzyMatchingDistance, askAi, } = config;
10
10
  const indexHash = (0, getIndexHash_1.getIndexHash)(config);
11
11
  const contents = [];
12
12
  contents.push(`export const removeDefaultStemmer = ${JSON.stringify(removeDefaultStemmer)};`);
@@ -41,6 +41,7 @@ function generate(config, dir) {
41
41
  : null)};`);
42
42
  contents.push(`export const hideSearchBarWithNoSearchContext = ${JSON.stringify(!!hideSearchBarWithNoSearchContext)};`);
43
43
  contents.push(`export const useAllContextsWithNoSearchContext = ${JSON.stringify(!!useAllContextsWithNoSearchContext)};`);
44
+ contents.push(`export const askAi = ${JSON.stringify(askAi !== null && askAi !== void 0 ? askAi : null)};`);
44
45
  fs_1.default.writeFileSync(path_1.default.join(dir, "generated.js"), contents.join("\n"));
45
46
  const constantContents = [
46
47
  `import lunr from ${JSON.stringify(require.resolve("lunr"))};`,
@@ -40,6 +40,7 @@ const schema = utils_validation_1.Joi.object({
40
40
  useAllContextsWithNoSearchContext: utils_validation_1.Joi.boolean().default(false),
41
41
  forceIgnoreNoIndex: utils_validation_1.Joi.boolean().default(false),
42
42
  fuzzyMatchingDistance: utils_validation_1.Joi.number().default(1),
43
+ askAi: utils_validation_1.Joi.object().optional(),
43
44
  });
44
45
  function validateOptions({ options, validate, }) {
45
46
  return validate(schema, options || {});
@@ -8,4 +8,5 @@ var SearchDocumentType;
8
8
  SearchDocumentType[SearchDocumentType["Description"] = 2] = "Description";
9
9
  SearchDocumentType[SearchDocumentType["Keywords"] = 3] = "Keywords";
10
10
  SearchDocumentType[SearchDocumentType["Content"] = 4] = "Content";
11
+ SearchDocumentType[SearchDocumentType["AskAI"] = 5] = "AskAI";
11
12
  })(SearchDocumentType || (exports.SearchDocumentType = SearchDocumentType = {}));
@@ -0,0 +1,43 @@
1
+ declare module "ai" {
2
+ interface UIMessage {
3
+ }
4
+ interface UIMessageChunk {
5
+ }
6
+ }
7
+ declare module "@easyops-cn/autocomplete.js" {
8
+ const noConflict: () => void;
9
+ }
10
+ declare module "*/generated.js" {
11
+ const removeDefaultStemmer: string[];
12
+ class Mark {
13
+ constructor(root: HTMLElement);
14
+ mark: (terms: string[], options?: Record<string, unknown>) => void;
15
+ unmark: () => void;
16
+ }
17
+ const searchResultContextMaxLength: number;
18
+ const explicitSearchResultPath: boolean;
19
+ const searchBarShortcut: boolean;
20
+ const searchBarShortcutHint: boolean;
21
+ const searchBarShortcutKeymap: string;
22
+ const searchBarPosition: "left" | "right";
23
+ const docsPluginIdForPreferredVersion: string;
24
+ const indexDocs: boolean;
25
+ const searchContextByPaths: (string | {
26
+ label: string | Record<string, string>;
27
+ path: string;
28
+ })[];
29
+ const hideSearchBarWithNoSearchContext: boolean;
30
+ const useAllContextsWithNoSearchContext: boolean;
31
+ const forceIgnoreNoIndex: boolean;
32
+ const askAi: import("open-ask-ai").AskAIWidgetProps;
33
+ }
34
+ declare module "*/generated-constants.js" {
35
+ const removeDefaultStopWordFilter: string[];
36
+ const language: string[];
37
+ const searchIndexUrl: string;
38
+ const searchResultLimits: number;
39
+ const fuzzyMatchingDistance: number;
40
+ const __setLanguage: (value: string[]) => void;
41
+ const __setRemoveDefaultStopWordFilter: (value: string[]) => void;
42
+ }
43
+ declare module "@docusaurus/Head";
@@ -1,3 +1,4 @@
1
+ import { AskAIWidgetProps } from "open-ask-ai";
1
2
  export interface PluginOptions {
2
3
  /**
3
4
  * Whether to index docs.
@@ -193,4 +194,8 @@ export interface PluginOptions {
193
194
  * @default 1
194
195
  */
195
196
  fuzzyMatchingDistance?: number;
197
+ /**
198
+ * Configuration for Ask AI widget integration. When not set, the Ask AI feature will be disabled.
199
+ */
200
+ askAi?: AskAIWidgetProps;
196
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyops-cn/docusaurus-search-local",
3
- "version": "0.52.2",
3
+ "version": "0.53.0",
4
4
  "description": "An offline/local search plugin for Docusaurus v3",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,6 +46,7 @@
46
46
  "lunr": "^2.3.9",
47
47
  "lunr-languages": "^1.4.0",
48
48
  "mark.js": "^8.11.1",
49
+ "open-ask-ai": "^0.7.3",
49
50
  "tslib": "^2.4.0"
50
51
  },
51
52
  "devDependencies": {