@easyops-cn/docusaurus-search-local 0.52.3 → 0.54.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.54.0](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.53.0...v0.54.0) (2026-02-08)
6
+
7
+
8
+ ### Features
9
+
10
+ * make open-ask-ai async and optional ([4c404ab](https://github.com/easyops-cn/docusaurus-search-local/commit/4c404ab21a6ea232d82e9bdd3041d6a1c7ac73d1))
11
+ * make open-ask-ai async and optional ([e031739](https://github.com/easyops-cn/docusaurus-search-local/commit/e031739116d9cc1eee18ecb13e6752474fdd2f79)), closes [#558](https://github.com/easyops-cn/docusaurus-search-local/issues/558)
12
+
13
+ ## [0.53.0](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.52.3...v0.53.0) (2026-02-05)
14
+
15
+
16
+ ### Features
17
+
18
+ * support Ask AI ([8a5d29e](https://github.com/easyops-cn/docusaurus-search-local/commit/8a5d29e2e5b3b1cb534b1bbf13075f6824307d2e))
19
+ * support Ask AI ([3b2e339](https://github.com/easyops-cn/docusaurus-search-local/commit/3b2e339a04ac9347e73fe39d63237ffa3e63242a))
20
+
5
21
  ## [0.52.3](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.52.2...v0.52.3) (2026-01-29)
6
22
 
7
23
 
@@ -8,7 +8,8 @@ import { useActivePlugin, useActiveVersion, } from "@docusaurus/plugin-content-d
8
8
  import { fetchIndexesByWorker, searchByWorker } from "../searchByWorker";
9
9
  import { SuggestionTemplate } from "./SuggestionTemplate";
10
10
  import { EmptyTemplate } from "./EmptyTemplate";
11
- import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated";
11
+ import { SearchDocumentType } from "../../../shared/interfaces";
12
+ import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, askAi, } from "../../utils/proxiedGenerated";
12
13
  import LoadingRing from "../LoadingRing/LoadingRing";
13
14
  import { normalizeContextByPath } from "../../utils/normalizeContextByPath";
14
15
  import { searchResultLimits } from "../../utils/proxiedGeneratedConstants";
@@ -28,6 +29,19 @@ async function fetchAutoCompleteJS() {
28
29
  }
29
30
  return autoComplete;
30
31
  }
32
+ async function fetchOpenAskAI() {
33
+ try {
34
+ const openAskAIModule = await import("open-ask-ai");
35
+ await import("open-ask-ai/styles.css");
36
+ return {
37
+ AskAIWidget: openAskAIModule.AskAIWidget,
38
+ };
39
+ }
40
+ catch (error) {
41
+ // open-ask-ai is optional, return null if not available
42
+ return null;
43
+ }
44
+ }
31
45
  const SEARCH_PARAM_HIGHLIGHT = "_highlight";
32
46
  export default function SearchBar({ handleSearchBarToggle, }) {
33
47
  const isBrowser = useIsBrowser();
@@ -49,6 +63,8 @@ export default function SearchBar({ handleSearchBarToggle, }) {
49
63
  const [inputChanged, setInputChanged] = useState(false);
50
64
  const [inputValue, setInputValue] = useState("");
51
65
  const search = useRef(null);
66
+ const askAIWidgetRef = useRef(null);
67
+ const [AskAIWidgetComponent, setAskAIWidgetComponent] = useState(null);
52
68
  const prevSearchContext = useRef("");
53
69
  const [searchContext, setSearchContext] = useState("");
54
70
  const prevVersionUrl = useRef(baseUrl);
@@ -94,10 +110,14 @@ export default function SearchBar({ handleSearchBarToggle, }) {
94
110
  indexStateMap.current.set(searchContext, "loading");
95
111
  search.current?.autocomplete.destroy();
96
112
  setLoading(true);
97
- const [autoComplete] = await Promise.all([
113
+ const [autoComplete, openAskAIModule] = await Promise.all([
98
114
  fetchAutoCompleteJS(),
115
+ askAi ? fetchOpenAskAI() : Promise.resolve(null),
99
116
  fetchIndexesByWorker(versionUrl, searchContext),
100
117
  ]);
118
+ if (openAskAIModule) {
119
+ setAskAIWidgetComponent(() => openAskAIModule.AskAIWidget);
120
+ }
101
121
  const searchFooterLinkElement = ({ query, isEmpty, }) => {
102
122
  const a = document.createElement("a");
103
123
  const params = new URLSearchParams();
@@ -176,7 +196,25 @@ export default function SearchBar({ handleSearchBarToggle, }) {
176
196
  {
177
197
  source: async (input, callback) => {
178
198
  const result = await searchByWorker(versionUrl, searchContext, input, searchResultLimits);
179
- callback(result);
199
+ if (input && askAi) {
200
+ callback([
201
+ {
202
+ document: {
203
+ i: -1,
204
+ t: "",
205
+ u: "",
206
+ },
207
+ type: SearchDocumentType.AskAI,
208
+ page: undefined,
209
+ metadata: {},
210
+ tokens: [input],
211
+ },
212
+ ...result,
213
+ ]);
214
+ }
215
+ else {
216
+ callback(result);
217
+ }
180
218
  },
181
219
  templates: {
182
220
  suggestion: SuggestionTemplate,
@@ -195,8 +233,12 @@ export default function SearchBar({ handleSearchBarToggle, }) {
195
233
  },
196
234
  },
197
235
  ])
198
- .on("autocomplete:selected", function (event, { document: { u, h }, tokens }) {
236
+ .on("autocomplete:selected", function (event, { document: { u, h }, type, tokens }) {
199
237
  searchBarRef.current?.blur();
238
+ if (type === SearchDocumentType.AskAI && askAi) {
239
+ askAIWidgetRef.current?.openWithNewSession(tokens.join(""));
240
+ return;
241
+ }
200
242
  let url = u;
201
243
  if (Mark && tokens.length > 0) {
202
244
  const params = new URLSearchParams();
@@ -333,6 +375,9 @@ export default function SearchBar({ handleSearchBarToggle, }) {
333
375
  message: "Search",
334
376
  description: "The ARIA label and placeholder for search button",
335
377
  })} aria-label="Search" className={`navbar__search-input ${styles.searchInput}`} onMouseEnter={onInputMouseEnter} onFocus={onInputFocus} onBlur={onInputBlur} onChange={onInputChange} ref={searchBarRef} value={inputValue}/>
378
+ {askAi && AskAIWidgetComponent && (<AskAIWidgetComponent ref={askAIWidgetRef} {...askAi}>
379
+ <span hidden></span>
380
+ </AskAIWidgetComponent>)}
336
381
  <LoadingRing className={styles.searchBarLoadingRing}/>
337
382
  {searchBarShortcut &&
338
383
  searchBarShortcutHint &&
@@ -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,55 @@
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 "open-ask-ai" {
11
+ interface AskAIWidgetProps {
12
+ [key: string]: any;
13
+ }
14
+ interface AskAIWidgetRef {
15
+ openWithNewSession: (query: string) => void;
16
+ }
17
+ const AskAIWidget: any;
18
+ }
19
+ interface AskAIConfig {
20
+ [key: string]: any;
21
+ }
22
+ declare module "*/generated.js" {
23
+ const removeDefaultStemmer: string[];
24
+ class Mark {
25
+ constructor(root: HTMLElement);
26
+ mark: (terms: string[], options?: Record<string, unknown>) => void;
27
+ unmark: () => void;
28
+ }
29
+ const searchResultContextMaxLength: number;
30
+ const explicitSearchResultPath: boolean;
31
+ const searchBarShortcut: boolean;
32
+ const searchBarShortcutHint: boolean;
33
+ const searchBarShortcutKeymap: string;
34
+ const searchBarPosition: "left" | "right";
35
+ const docsPluginIdForPreferredVersion: string;
36
+ const indexDocs: boolean;
37
+ const searchContextByPaths: (string | {
38
+ label: string | Record<string, string>;
39
+ path: string;
40
+ })[];
41
+ const hideSearchBarWithNoSearchContext: boolean;
42
+ const useAllContextsWithNoSearchContext: boolean;
43
+ const forceIgnoreNoIndex: boolean;
44
+ const askAi: AskAIConfig | undefined;
45
+ }
46
+ declare module "*/generated-constants.js" {
47
+ const removeDefaultStopWordFilter: string[];
48
+ const language: string[];
49
+ const searchIndexUrl: string;
50
+ const searchResultLimits: number;
51
+ const fuzzyMatchingDistance: number;
52
+ const __setLanguage: (value: string[]) => void;
53
+ const __setRemoveDefaultStopWordFilter: (value: string[]) => void;
54
+ }
55
+ 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.3",
3
+ "version": "0.54.0",
4
4
  "description": "An offline/local search plugin for Docusaurus v3",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,6 +48,9 @@
48
48
  "mark.js": "^8.11.1",
49
49
  "tslib": "^2.4.0"
50
50
  },
51
+ "optionalDependencies": {
52
+ "open-ask-ai": "^0.7.3"
53
+ },
51
54
  "devDependencies": {
52
55
  "@docusaurus/module-type-aliases": "^3.0.0",
53
56
  "@docusaurus/theme-common": "^3.0.0",