1
0
Fork 0
docsearch/packages/website/docs/hybrid-mode.mdx

434 lines
14 KiB
Text

---
title: Hybrid mode
description: Connect DocSearch keyword search and Ask AI Sidepanel experiences in React, JavaScript, and Docusaurus.
---
import TabItem from '@theme/TabItem';
import Tabs from '@theme/Tabs';
Hybrid mode starts in the DocSearch modal, then moves an Ask AI request into the Sidepanel. Users can scan keyword results in a focused overlay and continue an AI conversation beside the page.
The handoff carries the prompt and its identifiers. It doesn't submit the same prompt in both interfaces.
## Prerequisites
Before you add hybrid mode:
- Use matching v5 DocSearch package versions.
- Configure a searchable Algolia index and a public search-only API key.
- Create an assistant in [Agent Studio](/docs/agent-studio/getting-started).
- Decide whether you need React, JavaScript, or the DocSearch Docusaurus adapter.
Read the package references for [core](/docs/packages/core/overview), [modal](/docs/packages/modal/overview), [Sidepanel](/docs/packages/sidepanel/getting-started), [JavaScript](/docs/packages/js/getting-started), [Sidepanel JavaScript](/docs/packages/sidepanel-js/getting-started), and the [Docusaurus adapter](/docs/packages/docusaurus-adapter/getting-started) when you need options beyond this workflow.
## Understand the handoff
An Ask AI action can come from a typed prompt, a suggested question, a prompt suggestion in keyword results, or a saved conversation.
DocSearch represents the action as an `InitialAskAiMessage`:
```ts
interface InitialAskAiMessage {
query: string;
messageId?: string;
suggestedQuestionId?: string;
}
```
Preserve the complete object during a manual handoff:
- `query` becomes the first prompt when the Sidepanel starts a conversation.
- `suggestedQuestionId` is sent with the prompt so Agent Studio can identify the selected suggestion.
- `messageId` lets the Sidepanel restore a locally stored conversation. If it can't find that conversation, it starts a new one with `query`.
## Add hybrid mode
<Tabs groupId="language" aria-label="Programming language">
<TabItem value="react" label="React">
### React
Install the connected React packages and CSS:
<Tabs groupId="package-manager" aria-label="Package manager">
<TabItem value="npm" label="npm">
```bash
npm install @docsearch/core@^5 @docsearch/modal@^5 @docsearch/sidepanel@^5 @docsearch/css@^5
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @docsearch/core@^5 @docsearch/modal@^5 @docsearch/sidepanel@^5 @docsearch/css@^5
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @docsearch/core@^5 @docsearch/modal@^5 @docsearch/sidepanel@^5 @docsearch/css@^5
```
</TabItem>
<TabItem value="bun" label="Bun">
```bash
bun add @docsearch/core@^5 @docsearch/modal@^5 @docsearch/sidepanel@^5 @docsearch/css@^5
```
</TabItem>
</Tabs>
Render `DocSearchAskAiModal` and `Sidepanel` under the same `DocSearch` provider. The Sidepanel registers itself with the provider after mount. From then on, desktop Ask AI actions move to the Sidepanel automatically.
```tsx title="HybridSearch.tsx"
import { DocSearch } from '@docsearch/core';
import { DocSearchAskAiModal, DocSearchButton } from '@docsearch/modal';
import { Sidepanel, SidepanelButton } from '@docsearch/sidepanel';
import type { JSX } from 'react';
import '@docsearch/css/dist/style.css';
import '@docsearch/css/dist/sidepanel.css';
interface HybridSearchProps {
appId: string;
apiKey: string;
keywordIndexName: string;
agentId: string;
onReady?: () => void;
onOpen?: () => void;
onClose?: () => void;
onSidepanelOpen?: () => void;
onSidepanelClose?: () => void;
}
export function HybridSearch({
appId,
apiKey,
keywordIndexName,
agentId,
onReady,
onOpen,
onClose,
onSidepanelOpen,
onSidepanelClose,
}: HybridSearchProps): JSX.Element {
return (
<DocSearch
onReady={onReady}
onOpen={onOpen}
onClose={onClose}
onSidepanelOpen={onSidepanelOpen}
onSidepanelClose={onSidepanelClose}
>
<DocSearchButton />
<DocSearchAskAiModal
appId={appId}
apiKey={apiKey}
indices={[keywordIndexName]}
askAi={{ agentId }}
/>
<SidepanelButton />
<Sidepanel
appId={appId}
apiKey={apiKey}
agentId={agentId}
/>
</DocSearch>
);
}
```
`SidepanelButton` gives users a direct Ask AI entry point. You can omit it without disabling hybrid handoff. Keep `Sidepanel` mounted so it can register before a user submits an Ask AI request.
#### React lifecycle callbacks
Put lifecycle callbacks on `DocSearch` so they observe the shared state.
During the automatic React handoff, the state changes directly from `modal-askai` to `sidepanel`. That transition calls `onSidepanelOpen`; it doesn't call `onClose`.
##### `onReady`
> `type: () => void` | **optional**
Runs when the provider mounts.
##### `onOpen`
> `type: () => void` | **optional**
Runs when a modal opens from `ready` or `sidepanel`.
##### `onClose`
> `type: () => void` | **optional**
Runs when a modal changes directly to `ready`.
##### `onSidepanelOpen`
> `type: () => void` | **optional**
Runs when the state changes to `sidepanel`.
##### `onSidepanelClose`
> `type: () => void` | **optional**
Runs when the state leaves `sidepanel`.
</TabItem>
<TabItem value="js" label="JavaScript">
### JavaScript
The JavaScript packages create independent DocSearch and Sidepanel instances. Connect them with `interceptAskAiEvent`.
<Tabs groupId="package-manager" aria-label="Package manager">
<TabItem value="npm" label="npm">
```bash
npm install @docsearch/js@^5 @docsearch/sidepanel-js@^5 @docsearch/css@^5
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @docsearch/js@^5 @docsearch/sidepanel-js@^5 @docsearch/css@^5
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @docsearch/js@^5 @docsearch/sidepanel-js@^5 @docsearch/css@^5
```
</TabItem>
<TabItem value="bun" label="Bun">
```bash
bun add @docsearch/js@^5 @docsearch/sidepanel-js@^5 @docsearch/css@^5
```
</TabItem>
</Tabs>
Add one mount element for each instance:
```html title="index.html"
<div id="docsearch"></div>
<div id="docsearch-sidepanel"></div>
```
Create the Sidepanel first so the interceptor always has a target. The mobile check in this example matches the React integration: mobile users stay in the Ask AI modal, while desktop users move to the Sidepanel.
```ts title="search.ts"
import docsearch, { type DocSearchInstance } from '@docsearch/js';
import sidepanel, { type SidepanelInstance } from '@docsearch/sidepanel-js';
import '@docsearch/css/dist/style.css';
import '@docsearch/css/dist/sidepanel.css';
const appId = 'YOUR_APP_ID';
const apiKey = 'YOUR_SEARCH_API_KEY';
const keywordIndexName = 'YOUR_KEYWORD_INDEX_NAME';
const agentId = 'YOUR_AGENT_ID';
const mobileQuery = window.matchMedia('(max-width: 768px)');
let search: DocSearchInstance | undefined;
let panel: SidepanelInstance | undefined;
panel = sidepanel({
container: '#docsearch-sidepanel',
appId,
apiKey,
agentId,
onReady: () => {
document.body.setAttribute('data-sidepanel-ready', '');
},
onOpen: () => {
search?.close();
document.body.setAttribute('data-sidepanel-open', '');
},
onClose: () => {
document.body.removeAttribute('data-sidepanel-open');
},
});
search = docsearch({
container: '#docsearch',
appId,
apiKey,
indices: [keywordIndexName],
askAi: agentId,
interceptAskAiEvent: (initialMessage) => {
if (mobileQuery.matches) {
return false;
}
search?.close();
panel?.open(initialMessage);
return true;
},
onReady: () => {
document.body.setAttribute('data-docsearch-ready', '');
},
onOpen: () => {
panel?.close();
document.body.setAttribute('data-search-open', '');
},
onClose: () => {
document.body.removeAttribute('data-search-open');
},
});
```
The root `@docsearch/js` export includes Ask AI. The keyword-only JavaScript entry point is `@docsearch/js/docsearch`; don't use that entry point for hybrid mode because it has no Ask AI action to intercept.
#### How `interceptAskAiEvent` works
`interceptAskAiEvent` runs before the modal changes to Ask AI or sends a message.
- Return `true` after another interface accepts the request. DocSearch clears the keyword query and skips all default Ask AI behavior.
- Return `false` or `undefined` to keep the default Ask AI modal flow.
- Pass `initialMessage` unchanged to `sidepanelInstance.open(initialMessage)` so conversation and suggestion IDs survive.
Keep the `onOpen` callbacks that close the other instance. They prevent two interfaces from staying open when application code calls `search.open()`, `search.openAskAi()`, or `panel.open()` directly.
The JavaScript instances expose these lifecycle callbacks and controls:
- The DocSearch instance accepts the `onReady`, `onOpen`, and `onClose` callbacks, and exposes `open()`, `close()`, `openAskAi()`, `destroy()`, `isReady`, and `isOpen`.
- The Sidepanel instance accepts the `onReady`, `onOpen`, and `onClose` callbacks, and exposes `open()`, `close()`, `destroy()`, `isReady`, and `isOpen`.
Call both `destroy()` methods if your application removes the mount elements or tears down the page without a full navigation.
</TabItem>
</Tabs>
## Add hybrid mode to Docusaurus
Hybrid mode is supported through `@docsearch/docusaurus-adapter`. The adapter loads the v5 modal and Sidepanel, connects their state, and applies the mobile behavior described in this guide.
Install the adapter and keep `@docusaurus/preset-classic`:
<Tabs groupId="package-manager" aria-label="Package manager">
<TabItem value="npm" label="npm">
```bash
npm install @docsearch/docusaurus-adapter@^5
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @docsearch/docusaurus-adapter@^5
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @docsearch/docusaurus-adapter@^5
```
</TabItem>
<TabItem value="bun" label="Bun">
```bash
bun add @docsearch/docusaurus-adapter@^5
```
</TabItem>
</Tabs>
Configure DocSearch under `themeConfig.docsearch`. Set top-level `sidePanel` to `true` or to a Sidepanel options object.
```js title="docusaurus.config.mjs"
export default {
plugins: ['@docsearch/docusaurus-adapter'],
themeConfig: {
docsearch: {
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_SEARCH_API_KEY',
indices: [{ name: 'YOUR_INDEX_NAME' }],
askAi: {
agentId: 'YOUR_AGENT_ID',
},
sidePanel: true,
contextualSearch: true,
searchPage: { path: 'search' },
},
},
};
```
The adapter requires `askAi` to be an object with `agentId`; it doesn't accept the string shorthand in Docusaurus configuration. It also requires `askAi` whenever `sidePanel` is set.
Don't put `sidePanel` inside `askAi`. Don't configure this adapter under `themeConfig.algolia`. If you need function-valued custom tools, pass them through a swizzled `@theme/SearchBar` component because Docusaurus removes functions while serializing theme configuration.
The built-in `@docusaurus/theme-search-algolia` integration doesn't provide this v5 Sidepanel configuration. Use the DocSearch adapter for this workflow.
## Mobile behavior
The React provider treats viewports that match `(max-width: 768px)` as mobile. On those viewports, an Ask AI action stays in the modal even when a Sidepanel is registered. This avoids moving the conversation into the desktop Sidepanel layout.
The provider updates this decision on window resize. The Sidepanel also avoids focusing its prompt automatically on mobile because opening the virtual keyboard can disrupt the layout.
The JavaScript packages don't share a provider, so they don't apply the hybrid mobile decision to your interceptor. Add the `matchMedia` branch shown in the JavaScript example when you want the same behavior.
## Load the styles
Hybrid mode needs both style bundles:
```ts title="search.ts"
import '@docsearch/css/dist/style.css';
import '@docsearch/css/dist/sidepanel.css';
```
`style.css` includes shared variables, the button, keyword modal, and Ask AI modal. `sidepanel.css` adds the Sidepanel layout and imports the shared variables it needs.
If your bundler resolves the React style entry points, `@docsearch/react/style` and `@docsearch/react/style/sidepanel` load the same bundles. Import each bundle once.
## Troubleshoot hybrid mode
### Ask AI stays in the modal on desktop
Confirm that `Sidepanel` is mounted under the same `DocSearch` provider before the Ask AI action. The provider enables automatic handoff only after the Sidepanel registers. Also confirm that the viewport is wider than 768 pixels.
### Both the modal and Sidepanel submit the prompt
Return `true` from `interceptAskAiEvent` after calling `panel.open(initialMessage)`. Returning `false` or no value tells the modal to continue its default flow.
### Mobile users open the Sidepanel
In JavaScript integrations, check `(max-width: 768px)` in the interceptor and return `false` on a match. React and the Docusaurus adapter already make this decision.
### The Sidepanel loses a suggestion or saved conversation
Pass the complete `initialMessage` object to `open()`. Rebuilding it with only `query` discards `suggestedQuestionId` and `messageId`.
### A callback doesn't run during React handoff
Expect `onSidepanelOpen`, not `onClose`, when React changes directly from the modal to the Sidepanel. Use the shared provider callbacks rather than treating modal close as the handoff signal.
### Two JavaScript interfaces remain open
Close the search instance in the Sidepanel's `onOpen`, and close the Sidepanel in the search instance's `onOpen`. This also covers programmatic calls that bypass the interceptor.
### The interface is unstyled
Import both `style.css` and `sidepanel.css`. Check that your bundler includes CSS imports from dependencies.
### Docusaurus rejects the configuration
Use `themeConfig.docsearch`, pass `askAi` as an object, and put `sidePanel` beside `askAi`. Install `@docsearch/docusaurus-adapter@^5` instead of configuring hybrid mode through the built-in Algolia theme.
### Server rendering fails with `window` or `document` errors
Mount the composable React components only in the browser. The Docusaurus adapter handles this boundary and lazy-loads the modal and Sidepanel client-side.