Skip to content

Commit 8a5d29e

Browse files
authored
Merge pull request #556 from easyops-cn/steve/ask-ai
feat: support Ask AI
2 parents 22a4c17 + 3b2e339 commit 8a5d29e

File tree

17 files changed

+809
-13
lines changed

17 files changed

+809
-13
lines changed

README.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,35 @@ https://easyops-cn.github.io/docusaurus-search-local/
2626

2727
## Screen Shots
2828

29-
![Screen Shot EN](screen-shots/screen-shot-en.png)
29+
<p align="center">
30+
<img src="screen-shots/screen-shot-en.png" alt="Screen Shot EN" width="591" />
31+
</p>
3032

31-
![Screen Shot ZH](screen-shots/screen-shot-zh.png)
33+
<p align="center">
34+
<img src="screen-shots/screen-shot-zh.png" alt="Screen Shot ZH" width="602" />
35+
</p>
36+
37+
## ✨ Ask AI Support
38+
39+
This plugin now supports **Ask AI** integration! Enable AI-powered assistance directly in your documentation site to help users get instant, context-aware answers.
40+
41+
NOTE: Ask AI feature requires an external AI service. Please refer to the [Open Ask AI Server](https://github.com/easyops-cn/open-ask-ai-server), which is a serverless solution, and can be used **for free** using Vercel Hobby Plan!
42+
43+
<p align="center">
44+
<img src="screen-shots/ask-ai-01.png" alt="Ask AI Screenshot 1" width="602" />
45+
</p>
46+
47+
<p align="center">
48+
<img src="screen-shots/ask-ai-02.png" alt="Ask AI Screenshot 2" width="631" />
49+
</p>
50+
51+
With Ask AI enabled, users can:
52+
53+
- Get intelligent answers based on your documentation
54+
- Access AI assistance with a simple keyboard shortcut
55+
- Receive context-aware responses tailored to your content
56+
57+
See the [askAi option](#theme-options) in Theme Options below for configuration details.
3258

3359
## Installation
3460

@@ -65,6 +91,13 @@ module.exports = {
6591

6692
// If you're using `noIndex: true`, set `forceIgnoreNoIndex` to enable local index:
6793
// forceIgnoreNoIndex: true,
94+
95+
// Enable Ask AI integration:
96+
// askAi: {
97+
// project: "your-project-name",
98+
// apiUrl: "https://your-api-url.com/api/stream",
99+
// hotkey: "cmd+I", // Optional: keyboard shortcut to trigger Ask AI
100+
// },
68101
}),
69102
],
70103
],
@@ -96,7 +129,7 @@ module.exports = {
96129
| ignoreCssSelectors | string \| string[] | `[]` | A list of css selectors to ignore when indexing each page. |
97130
| searchBarShortcut | boolean | `true` | Whether to enable keyboard shortcut to focus in search bar. |
98131
| searchBarShortcutHint | boolean | `true` | Whether to show keyboard shortcut hint in search bar. Disable it if you need to hide the hint while shortcut is still enabled. |
99-
| searchBarShortcutKeymap | string | `"mod+k"` | Custom keyboard shortcut to focus the search bar. Supports formats like: `"s"` for single key, `"ctrl+k"` for key combinations, `"mod+k"` for Command+K (Mac) / Ctrl+K (others) - recommended cross-platform option, `"ctrl+shift+k"` for multiple modifiers. |
132+
| searchBarShortcutKeymap | string | `"mod+k"` | Custom keyboard shortcut to focus the search bar. Supports formats like: `"s"` for single key, `"ctrl+k"` for key combinations, `"mod+k"` for Command+K (Mac) / Ctrl+K (others) - recommended cross-platform option, `"ctrl+shift+k"` for multiple modifiers. |
100133
| searchBarPosition | `"auto"` \| `"left"` \| `"right"` | `"auto"` | The side of the navbar the search bar should appear on. By default, it will try to autodetect based on your docusaurus config according to [the docs](https://docusaurus.io/docs/api/themes/configuration#navbar-search). |
101134
| docsPluginIdForPreferredVersion | string | | When you're using multi-instance of docs, set the docs plugin id which you'd like to check the preferred version with, for the search index. |
102135
| zhUserDict | string | | Provide your custom dict for language of zh, [see here](https://github.com/fxsjy/jieba#%E8%BD%BD%E5%85%A5%E8%AF%8D%E5%85%B8) |
@@ -106,6 +139,7 @@ module.exports = {
106139
| useAllContextsWithNoSearchContext | boolean | `false` | Whether to show results from all the contexts if no context is provided. This option should not be used with `hideSearchBarWithNoSearchContext: true` as this would show results when there is no search context. This will duplicate indexes and might have a performance cost depending on the index sizes. |
107140
| `forceIgnoreNoIndex` | boolean | `false` | Force enable search index even if `noIndex: true` is set, this also affects unlisted articles. |
108141
| `fuzzyMatchingDistance` | number | `1` | Set the edit distance for fuzzy matching during searches. |
142+
| `askAi` | object | | Configuration for Ask AI widget integration. When not set, the Ask AI feature will be disabled. Required properties: `project` (string), `apiUrl` (string). Optional: `hotkey` (string, e.g., "cmd+I"). See [Ask AI Support](#-ask-ai-support) for more details. |
109143

110144
### I18N
111145

docusaurus-search-local/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"lunr": "^2.3.9",
4747
"lunr-languages": "^1.4.0",
4848
"mark.js": "^8.11.1",
49+
"open-ask-ai": "^0.7.3",
4950
"tslib": "^2.4.0"
5051
},
5152
"devDependencies": {

docusaurus-search-local/src/client/theme/SearchBar/SearchBar.module.css

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,37 @@
1717
}
1818

1919
.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));
2122
outline-offset: 0px;
2223
}
2324

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+
2451
@media not (max-width: 996px) {
2552
.searchBar.searchBarLeft .dropdownMenu {
2653
left: 0 !important;

docusaurus-search-local/src/client/theme/SearchBar/SearchBar.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import {
1414
useActivePlugin,
1515
useActiveVersion,
1616
} from "@docusaurus/plugin-content-docs/client";
17+
import { AskAIWidget, AskAIWidgetRef } from "open-ask-ai";
18+
import "open-ask-ai/styles.css";
1719

1820
import { fetchIndexesByWorker, searchByWorker } from "../searchByWorker";
1921
import { SuggestionTemplate } from "./SuggestionTemplate";
2022
import { EmptyTemplate } from "./EmptyTemplate";
21-
import { SearchResult } from "../../../shared/interfaces";
23+
import { SearchDocumentType, SearchResult } from "../../../shared/interfaces";
2224
import {
2325
Mark,
2426
searchBarShortcut,
@@ -29,6 +31,7 @@ import {
2931
searchContextByPaths,
3032
hideSearchBarWithNoSearchContext,
3133
useAllContextsWithNoSearchContext,
34+
askAi,
3235
} from "../../utils/proxiedGenerated";
3336
import LoadingRing from "../LoadingRing/LoadingRing";
3437
import { normalizeContextByPath } from "../../utils/normalizeContextByPath";
@@ -58,6 +61,11 @@ interface SearchBarProps {
5861
handleSearchBarToggle?: (expanded: boolean) => void;
5962
}
6063

64+
interface TemplateProps {
65+
query: string;
66+
isEmpty: boolean;
67+
}
68+
6169
export default function SearchBar({
6270
handleSearchBarToggle,
6371
}: SearchBarProps): ReactElement {
@@ -88,6 +96,7 @@ export default function SearchBar({
8896
const [inputChanged, setInputChanged] = useState(false);
8997
const [inputValue, setInputValue] = useState("");
9098
const search = useRef<any>(null);
99+
const askAIWidgetRef = useRef<AskAIWidgetRef>(null);
91100

92101
const prevSearchContext = useRef<string>("");
93102
const [searchContext, setSearchContext] = useState<string>("");
@@ -146,10 +155,7 @@ export default function SearchBar({
146155
const searchFooterLinkElement = ({
147156
query,
148157
isEmpty,
149-
}: {
150-
query: string;
151-
isEmpty: boolean;
152-
}): HTMLAnchorElement => {
158+
}: TemplateProps): HTMLAnchorElement => {
153159
const a = document.createElement("a");
154160
const params = new URLSearchParams();
155161

@@ -255,12 +261,29 @@ export default function SearchBar({
255261
input,
256262
searchResultLimits
257263
);
258-
callback(result);
264+
if (input && askAi) {
265+
callback([
266+
{
267+
document: {
268+
i: -1,
269+
t: "",
270+
u: "",
271+
},
272+
type: SearchDocumentType.AskAI,
273+
page: undefined,
274+
metadata: {},
275+
tokens: [input],
276+
} as Partial<SearchResult> as SearchResult,
277+
...result,
278+
]);
279+
} else {
280+
callback(result);
281+
}
259282
},
260283
templates: {
261284
suggestion: SuggestionTemplate,
262285
empty: EmptyTemplate,
263-
footer: ({ query, isEmpty }: any) => {
286+
footer: ({ query, isEmpty }: TemplateProps) => {
264287
if (
265288
isEmpty &&
266289
(!searchContext || !useAllContextsWithNoSearchContext)
@@ -279,9 +302,17 @@ export default function SearchBar({
279302
)
280303
.on(
281304
"autocomplete:selected",
282-
function (event: any, { document: { u, h }, tokens }: SearchResult) {
305+
function (
306+
event: any,
307+
{ document: { u, h }, type, tokens }: SearchResult
308+
) {
283309
searchBarRef.current?.blur();
284310

311+
if (type === SearchDocumentType.AskAI && askAi) {
312+
askAIWidgetRef.current?.openWithNewSession(tokens.join(""));
313+
return;
314+
}
315+
285316
let url = u;
286317
if (Mark && tokens.length > 0) {
287318
const params = new URLSearchParams();
@@ -457,6 +488,11 @@ export default function SearchBar({
457488
ref={searchBarRef}
458489
value={inputValue}
459490
/>
491+
{askAi && (
492+
<AskAIWidget ref={askAIWidgetRef} {...askAi}>
493+
<span hidden></span>
494+
</AskAIWidget>
495+
)}
460496
<LoadingRing className={styles.searchBarLoadingRing} />
461497
{searchBarShortcut &&
462498
searchBarShortcutHint &&

docusaurus-search-local/src/client/theme/SearchBar/SuggestionTemplate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import {
44
SearchResult,
55
} from "../../../shared/interfaces";
66
import { concatDocumentPath } from "../../utils/concatDocumentPath";
7+
import { escapeHtml } from "../../utils/escapeHtml";
78
import { getStemmedPositions } from "../../utils/getStemmedPositions";
89
import { highlight } from "../../utils/highlight";
910
import { highlightStemmed } from "../../utils/highlightStemmed";
1011
import { explicitSearchResultPath } from "../../utils/proxiedGenerated";
1112
import {
1213
iconAction,
14+
iconAskAI,
1315
iconContent,
1416
iconHeading,
1517
iconTitle,
@@ -27,6 +29,17 @@ export function SuggestionTemplate({
2729
isInterOfTree,
2830
isLastOfTree,
2931
}: Omit<SearchResult, "score" | "index">): string {
32+
if (type === SearchDocumentType.AskAI) {
33+
return [
34+
`<span class="${styles.hitIcon}">${iconAskAI}</span>`,
35+
`<span class="${styles.hitWrapper}">`,
36+
`<span class="${styles.hitTitle}">Ask AI: <mark>${escapeHtml(
37+
tokens.join(" ")
38+
)}</mark></span>`,
39+
`</span>`,
40+
].join("");
41+
}
42+
3043
const isTitle = type === SearchDocumentType.Title;
3144
const isKeywords = type === SearchDocumentType.Keywords;
3245
const isTitleRelated = isTitle || isKeywords;

docusaurus-search-local/src/client/theme/SearchBar/icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export const iconTreeInter =
1212
'<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>';
1313
export const iconTreeLast =
1414
'<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>';
15+
export const iconAskAI =
16+
'<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>';

docusaurus-search-local/src/declarations.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
declare module "ai" {
2+
export interface UIMessage {}
3+
export interface UIMessageChunk {}
4+
}
5+
16
declare module "@easyops-cn/autocomplete.js" {
27
export const noConflict: () => void;
38
}
@@ -24,6 +29,7 @@ declare module "*/generated.js" {
2429
export const hideSearchBarWithNoSearchContext: boolean;
2530
export const useAllContextsWithNoSearchContext: boolean;
2631
export const forceIgnoreNoIndex: boolean;
32+
export const askAi: import("open-ask-ai").AskAIWidgetProps;
2733
}
2834

2935
declare module "*/generated-constants.js" {

docusaurus-search-local/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { AskAIWidgetProps } from "open-ask-ai";
2+
13
export interface PluginOptions {
24
/**
35
* Whether to index docs.
@@ -221,4 +223,9 @@ export interface PluginOptions {
221223
* @default 1
222224
*/
223225
fuzzyMatchingDistance?: number;
226+
227+
/**
228+
* Configuration for Ask AI widget integration. When not set, the Ask AI feature will be disabled.
229+
*/
230+
askAi?: AskAIWidgetProps;
224231
}

0 commit comments

Comments
 (0)