1
0
Fork 0

feat: Add Docs MCP action, update DSxAlgolia logo

This commit is contained in:
Paul Jankowski 2026-07-29 14:05:46 -04:00
parent 01b9533632
commit 95177b1c4e
No known key found for this signature in database
23 changed files with 492 additions and 146 deletions

View file

@ -78,27 +78,6 @@ bun run pw:run:webkit
## Code Style Guidelines
### Imports
Imported modules must be ordered alphabetically with newlines between groups:
1. Built-in modules
2. External dependencies
3. Parent directory imports
4. Sibling imports
5. Index imports
Internal `@/**/*` paths go before parent imports.
```typescript
// Correct order
import type { AutocompleteOptions } from '@algolia/autocomplete-core';
import React, { type JSX } from 'react';
import { DocSearchButton } from './DocSearchButton';
import type { DocSearchHit } from './types';
```
### TypeScript
- Use `type` imports for type-only imports: `import type { Foo } from './types'`
@ -137,9 +116,8 @@ function createStorage<TItem>(key: string): StorageInterface<TItem> {
### React Component Structure
- `src/components/ui/` contains reusable rendering components.
- `src/components/ui/` contains small, composable, building block components that are used to construct larger components (Button, Popover, Menu, etc...).
- Prefer domain-light primitives in `src/components/ui/` when possible.
- Feature-scoped UI components may live in `src/components/ui/` when their feature scope is explicit in the filename, such as `RecentConversationsResults.tsx`.
- `src/components/` contains scoped composition components that own feature flow, branching, and state orchestration.
- Keep AI-specific behavior out of generic UI primitives. If a UI component is AI-specific, make that scope clear in its name.

View file

@ -87,6 +87,20 @@ describe('validateThemeConfig', () => {
});
});
it('accepts hideMCPCallout', () => {
const docsearch: DocSearchInput = {
...minimalDocSearchConfig,
hideMCPCallout: true,
};
expect(testValidateThemeConfig(docsearch)).toEqual({
docsearch: {
...DEFAULT_CONFIG,
...docsearch,
},
});
});
it('rejects missing docsearch config', () => {
expectThrowMessage(
() => testValidateThemeConfig(undefined),

View file

@ -76,6 +76,7 @@ declare module '@docsearch/docusaurus-adapter' {
| 'recentSearchesWithFavoritesLimit'
| 'resultBadgeKey'
| 'translations'
| 'hideMCPCallout'
> & {
indices: NonNullable<DocSearchProps['indices']>;
askAi?: AskAiConfig;

View file

@ -371,6 +371,13 @@ function DocSearch({
const panelOptions = getSidePanelPanelOptions(
typeof sidePanel === 'object' ? sidePanel : undefined
);
const sidepanelTranslations = {
...panelOptions.translations,
byAlgoliaAriaLabel:
panelOptions.translations?.byAlgoliaAriaLabel ??
props.translations?.modal?.footer?.byAlgoliaAriaLabel ??
translations.modal?.footer?.byAlgoliaAriaLabel ?? 'DocSearch by Algolia',
};
return (
<>
@ -420,6 +427,7 @@ function DocSearch({
DocSearchSidepanel && (
<DocSearchSidepanel
{...panelOptions}
translations={sidepanelTranslations}
variant={panelOptions.variant ?? 'inline'}
pushSelector={panelOptions.pushSelector ?? '#__docusaurus'}
assistantId={sidePanelAskAi.assistantId}

View file

@ -63,9 +63,9 @@ const translations: DocSearchTranslations & {
};
footer: {
submitQuestionText: string;
poweredByText: string;
backToSearchText: string;
searchByText: string;
addDocsMCPText: string;
byAlgoliaAriaLabel: string;
};
};
} = {
@ -393,21 +393,21 @@ const translations: DocSearchTranslations & {
message: 'Escape key',
description: 'The ARIA label for close key in footer',
}),
poweredByText: translate({
id: 'theme.SearchModal.footer.searchByText',
message: 'Powered by',
description: "The 'Powered by' text for footer",
}),
searchByText: translate({
id: 'theme.SearchModal.footer.searchByText',
message: 'Powered by',
description: "The 'Powered by' text for footer",
}),
backToSearchText: translate({
id: 'theme.SearchModal.footer.backToSearchText',
message: 'Back to search',
description: 'The back to search text for footer',
}),
addDocsMCPText: translate({
id: 'theme.SearchModal.footer.addDocsMCPText',
message: 'Add Docs MCP',
description: 'The callout text for linking to Docs MCP',
}),
byAlgoliaAriaLabel: translate({
id: 'theme.SearchModal.footer.byAlgoliaAriaLabel',
message: 'DocSearch by Algolia',
description: 'The ARIA label text for the DocSearch by Algolia lockup',
}),
},
noResultsScreen: {
noResultsText: translate({

View file

@ -205,6 +205,7 @@ const DocSearchSchema = Joi.object<ThemeConfigDocSearch>({
searchPage: SearchPageSchema,
askAi: AskAiSchema.optional(),
sidePanel: Joi.alternatives().try(Joi.boolean(), SidePanelSchema).optional(),
hideMCPCallout: Joi.boolean().optional(),
})
.label('themeConfig.docsearch')
.unknown(false);

View file

@ -664,13 +664,74 @@
font-weight: 500;
}
.DocSearch-Logo a {
display: flex;
.DocSearch-ByAlgolia {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-width: 131px;
color: var(--docsearch-text-color);
}
.DocSearch-Logo svg {
.DocSearch-ByAlgolia-mark {
color: var(--docsearch-logo-color);
margin-inline-start: 8px;
height: 1.5rem;
width: auto;
flex-shrink: 0;
}
.DocSearch-ByAlgolia-Lockup {
display: flex;
flex-direction: column;
line-height: 1;
}
.DocSearch-ByAlgolia-Lockup-title {
font-size: 0.9375em;
font-weight: 600;
letter-spacing: -0.02em;
}
.DocSearch-ByAlgolia-Lockup-by {
margin-block-start: 0.125rem;
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.625em;
font-weight: 500;
letter-spacing: 0.025em;
color: oklch(64% 0.008 75deg);
svg {
height: 0.75rem;
width: auto;
max-width: 100%;
}
}
.DocSearch-DocsMCPAction {
a {
font-size: 0.875em;
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--docsearch-text-color);
padding-inline: 0.5rem;
padding-block: 0.25rem;
border-radius: var(--docsearch-border-radius);
background-color: var(--docsearch-hit-focus-background);
min-block-size: 1.5rem;
&:hover,
&:focus-visible {
outline: 2px solid var(--docsearch-highlight-color);
}
}
svg {
height: 1rem;
width: 1rem;
color: var(--docsearch-highlight-color);
}
}
/* Hit */
@ -1039,15 +1100,30 @@
border-block-start: 1px solid var(--docsearch-subtle-color);
border-radius: 0 0 var(--docsearch-modal-radius) var(--docsearch-modal-radius);
display: flex;
flex-direction: row-reverse;
flex-shrink: 0;
height: var(--docsearch-footer-height);
justify-content: space-between;
padding: 0 var(--docsearch-spacing);
position: relative;
user-select: none;
width: 100%;
z-index: 300;
@media screen and (width >= 768px) {
justify-content: space-between;
}
}
.DocSearch-Footer-Actions {
display: flex;
align-items: center;
gap: 1em;
justify-content: space-between;
width: 100%;
@media screen and (width >= 768px) {
justify-content: unset;
width: auto;
}
}
.DocSearch-Commands {

View file

@ -552,10 +552,6 @@
padding: 0.75rem 1rem 1rem;
}
.DocSearch-Sidepanel--powered-by {
align-self: flex-end;
}
.DocSearch-Sidepanel-Screen--title {
font-size: 1.625rem;
font-weight: 600;
@ -743,6 +739,53 @@
right: -4rem;
}
.DocSearch-Sidepanel .DocSearch-ByAlgolia {
display: inline-flex;
align-items: center;
gap: 0.25rem;
min-width: 131px;
color: var(--docsearch-text-color);
.DocSearch-ByAlgolia-mark {
color: var(--docsearch-logo-color);
height: 1rem;
width: auto;
flex-shrink: 0;
}
.DocSearch-ByAlgolia-Lockup {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.25rem;
}
.DocSearch-ByAlgolia-Lockup-title {
font-size: 0.9em;
font-weight: 600;
letter-spacing: -0.02em;
}
.DocSearch-ByAlgolia-Lockup-by {
margin-block-start: 0.125rem;
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75em;
font-weight: 500;
letter-spacing: 0.025em;
color: oklch(64% 0.008 75deg);
border-inline-start: 2px solid var(--docsearch-subtle-color);
padding-inline-start: 0.25rem;
svg {
height: 0.85rem;
width: auto;
max-width: 100%;
}
}
}
.sr-only {
clip-path: inset(50%);
white-space: nowrap;

View file

@ -1,81 +0,0 @@
import React, { type JSX } from 'react';
export type AlgoliaLogoTranslations = Partial<{
poweredByText: string;
}>;
type AlgoliaLogoProps = {
translations?: AlgoliaLogoTranslations;
};
export function AlgoliaLogo({
translations = {},
}: AlgoliaLogoProps): JSX.Element {
const { poweredByText = 'Powered by' } = translations;
return (
<a
href={`https://www.algolia.com/ref/docsearch/?utm_source=${window.location.hostname}&utm_medium=referral&utm_content=powered_by&utm_campaign=docsearch`}
target="_blank"
rel="noopener noreferrer"
>
<span className="DocSearch-Label">{poweredByText}</span>
<svg
width="80"
height="24"
aria-label="Algolia"
role="img"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2196.2 500"
>
<defs>
<style>
{'.cls-1,.cls-2{fill:#003dff;}.cls-2{fill-rule:evenodd;}'}
</style>
</defs>
<path
className="cls-2"
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<rect
className="cls-1"
x="1845.88"
y="104.73"
width="62.58"
height="277.9"
rx="5.9"
ry="5.9"
/>
<path
className="cls-2"
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
/>
<path
className="cls-2"
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<path
className="cls-2"
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
/>
<path
className="cls-2"
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
className="cls-2"
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
className="cls-2"
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
/>
<path
className="cls-1"
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
/>
</svg>
</a>
);
}

View file

@ -154,6 +154,12 @@ export interface DocSearchProps {
* @default undefined
*/
resultBadgeKey?: string;
/**
* Hides the Docs MCP callout in the Footer.
*
* @default false
*/
hideMCPCallout?: boolean;
}
function DocSearchComponent(

View file

@ -101,6 +101,7 @@ export function DocSearchAskAiModal({
indices,
facets,
isHybridModeSupported = false,
hideMCPCallout,
...props
}: DocSearchAskAiModalProps): JSX.Element {
const {
@ -653,6 +654,7 @@ export function DocSearchAskAiModal({
<Footer
translations={footerTranslations}
isAskAiActive={isAskAiActive}
hideMCPCallout={hideMCPCallout}
/>
}
onClose={onClose}

View file

@ -69,6 +69,7 @@ export function DocSearchModal({
recentSearchesWithFavoritesLimit = 4,
indices,
facets,
hideMCPCallout,
...props
}: DocSearchModalProps): JSX.Element {
const {
@ -303,7 +304,12 @@ export function DocSearchModal({
}}
/>
}
footer={<Footer translations={footerTranslations} />}
footer={
<Footer
translations={footerTranslations}
hideMCPCallout={hideMCPCallout}
/>
}
onClose={onClose}
/>
);

View file

@ -1,6 +1,7 @@
import React, { type JSX } from 'react';
import { AlgoliaLogo } from './AlgoliaLogo';
import { DocSearchByAlgolia } from './components/DocSearchByAlgolia';
import { DocsMCPAction } from './components/DocsMCPAction';
export type FooterTranslations = Partial<{
selectText: string;
@ -12,12 +13,24 @@ export type FooterTranslations = Partial<{
closeText: string;
backToSearchText: string;
closeKeyAriaLabel: string;
poweredByText: string;
/**
* Text displayed for the add Docs MCP CTA.
*
* @default 'Add Docs MCP'
*/
addDocsMCPText: string;
/**
* Aria label for the DocSearch by Algolia lockup.
*
* @default 'DocSearch by Algolia'
*/
byAlgoliaAriaLabel: string;
}>;
type FooterProps = Partial<{
translations: FooterTranslations;
isAskAiActive: boolean;
hideMCPCallout: boolean;
}>;
interface CommandIconProps {
@ -50,6 +63,7 @@ function CommandIcon(props: CommandIconProps): JSX.Element {
export function Footer({
translations = {},
isAskAiActive = false,
hideMCPCallout = false,
}: FooterProps): JSX.Element {
const {
selectText = 'Select',
@ -61,14 +75,12 @@ export function Footer({
closeText = 'Close',
backToSearchText = 'Back to search',
closeKeyAriaLabel = 'Escape key',
poweredByText = 'Powered by',
addDocsMCPText = 'Add Docs MCP',
byAlgoliaAriaLabel = 'DocSearch by Algolia',
} = translations;
return (
<>
<div className="DocSearch-Logo">
<AlgoliaLogo translations={{ poweredByText }} />
</div>
<ul className="DocSearch-Commands">
<li>
<kbd className="DocSearch-Commands-Key">
@ -105,6 +117,14 @@ export function Footer({
</span>
</li>
</ul>
<div className="DocSearch-Footer-Actions">
{!hideMCPCallout && (
<div className="DocSearch-DocsMCPAction">
<DocsMCPAction addDocsMCPText={addDocsMCPText} />
</div>
)}
<DocSearchByAlgolia byAlgoliaAriaLabel={byAlgoliaAriaLabel} />
</div>
</>
);
}

View file

@ -2,7 +2,7 @@ import type { SidepanelShortcuts, InitialAskAiMessage } from '@docsearch/core';
import React, { useCallback } from 'react';
import type { JSX } from 'react';
import { AlgoliaLogo, type AlgoliaLogoTranslations } from '../AlgoliaLogo';
import { DocSearchByAlgolia } from '../components/DocSearchByAlgolia';
import type {
DocSearchSidepanelProps,
SidepanelSearchParameters,
@ -52,8 +52,12 @@ export type SidepanelTranslations = Partial<{
conversationScreen: ConversationScreenTranslations;
/** Translation texts for the new conversation/starting screen. */
newConversationScreen: NewConversationScreenTranslations;
/** Translation text for the Algolia logo. */
logo: AlgoliaLogoTranslations;
/**
* Aria label for the DocSearch by Algolia lockup.
*
* @default 'DocSearch by Algolia'
*/
byAlgoliaAriaLabel: string;
}>;
export type SidepanelProps = Pick<
@ -161,6 +165,7 @@ function SidepanelInner(
const sidepanelContainerRef = React.useRef<HTMLDivElement>(null);
const promptInputRef = React.useRef<HTMLTextAreaElement>(null);
const isMobile = useIsMobile();
const { byAlgoliaAriaLabel = 'DocSearch by Algolia' } = translations;
const expectedWidth = useSidepanelWidth({
isExpanded,
@ -438,9 +443,7 @@ function SidepanelInner(
onStopStreaming={handleStopStreaming}
/>
<footer className="DocSearch-Sidepanel-Footer">
<span className="DocSearch-Logo DocSearch-Sidepanel--powered-by">
<AlgoliaLogo translations={translations.logo} />
</span>
<DocSearchByAlgolia byAlgoliaAriaLabel={byAlgoliaAriaLabel} />
</footer>
</aside>
</div>

View file

@ -238,7 +238,7 @@ describe('api', () => {
navigateText: 'Pour naviguer',
navigateUpKeyAriaLabel: 'Flèche vers le haut',
navigateDownKeyAriaLabel: 'Flèche vers le bas',
poweredByText: 'Propulsé par',
byAlgoliaAriaLabel: 'Propulsé par',
selectText: 'Pour sélectionner',
selectKeyAriaLabel: "Touche d'entrée",
},
@ -253,7 +253,7 @@ describe('api', () => {
fireEvent.click(await screen.findByText('Search'));
});
await screen.findByText('Propulsé par');
expect(await screen.findByLabelText('Propulsé par')).toBeInTheDocument();
await screen.findByText('Pour fermer');
await screen.findByText('Pour naviguer');
await screen.findByText('Pour sélectionner');
@ -342,6 +342,85 @@ describe('api', () => {
});
});
describe('footer callout', () => {
it('renders the Docs MCP callout by default', async () => {
render(<DocSearch />);
await act(async () => {
fireEvent.click(await screen.findByText('Search'));
});
const mcpLink = await screen.findByRole('link', {
name: 'Add Docs MCP',
});
expect(mcpLink).toHaveAttribute(
'href',
expect.stringContaining('https://docsearch.algolia.com/mcp/install')
);
expect(mcpLink).toHaveAttribute('target', '_blank');
expect(mcpLink).toHaveAttribute('rel', 'noopener noreferrer');
});
it('overrides the Docs MCP callout text', async () => {
render(
<DocSearch
translations={{
modal: {
footer: {
addDocsMCPText: 'Ajouter Docs MCP',
},
},
}}
/>
);
await act(async () => {
fireEvent.click(await screen.findByText('Search'));
});
expect(
await screen.findByRole('link', { name: 'Ajouter Docs MCP' })
).toBeInTheDocument();
});
it('disables the Docs MCP callout', async () => {
render(<DocSearch hideMCPCallout={true} />);
await act(async () => {
fireEvent.click(await screen.findByText('Search'));
});
expect(
screen.queryByRole('link', { name: 'Add Docs MCP' })
).not.toBeInTheDocument();
});
it('renders the Docs MCP callout in the Ask AI modal', async () => {
render(<DocSearchAI />);
await act(async () => {
fireEvent.click(await screen.findByText('Search'));
});
expect(
await screen.findByRole('link', { name: 'Add Docs MCP' })
).toBeInTheDocument();
});
it('disables the Docs MCP callout in the Ask AI modal', async () => {
render(<DocSearchAI hideMCPCallout={true} />);
await act(async () => {
fireEvent.click(await screen.findByText('Search'));
});
expect(
screen.queryByRole('link', { name: 'Add Docs MCP' })
).not.toBeInTheDocument();
});
});
describe('ask AI integration', () => {
it('updates placeholder when ask AI is available', async () => {
render(<DocSearchAI />);

View file

@ -0,0 +1,63 @@
import React from 'react';
import type { JSX } from 'react';
export function AlgoliaWordMark(): JSX.Element {
return (
<svg
width="80"
height="24"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2196.2 500"
aria-hidden="true"
focusable="false"
>
<defs>
<style>
{'.cls-1,.cls-2{fill:#003dff;}.cls-2{fill-rule:evenodd;}'}
</style>
</defs>
<path
className="cls-2"
d="M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<rect
className="cls-1"
x="1845.88"
y="104.73"
width="62.58"
height="277.9"
rx="5.9"
ry="5.9"
/>
<path
className="cls-2"
d="M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"
/>
<path
className="cls-2"
d="M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"
/>
<path
className="cls-2"
d="M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"
/>
<path
className="cls-2"
d="M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
className="cls-2"
d="M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"
/>
<path
className="cls-2"
d="M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"
/>
<path
className="cls-1"
d="M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"
/>
</svg>
);
}

View file

@ -0,0 +1,32 @@
import React from 'react';
import type { JSX } from 'react';
import { DocSearchMarkIcon } from '../icons';
import { AlgoliaWordMark } from './AlgoliaWordMark';
interface DocSearchByAlgoliaProps {
byAlgoliaAriaLabel: string;
}
export function DocSearchByAlgolia({
byAlgoliaAriaLabel,
}: DocSearchByAlgoliaProps): JSX.Element {
return (
<a
href={`https://www.algolia.com/ref/docsearch/?utm_source=${window.location.hostname}&utm_medium=referral&utm_content=powered_by&utm_campaign=docsearch`}
target="_blank"
rel="noopener noreferrer"
aria-label={byAlgoliaAriaLabel}
className="DocSearch-ByAlgolia"
>
<DocSearchMarkIcon className="DocSearch-ByAlgolia-mark" />
<span className="DocSearch-ByAlgolia-Lockup">
<span className="DocSearch-ByAlgolia-Lockup-title">DOCSEARCH</span>
<span className="DocSearch-ByAlgolia-Lockup-by">
by <AlgoliaWordMark />
</span>
</span>
</a>
);
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import type { JSX } from 'react';
import { PlusIcon } from '../icons';
interface DocsMCPActionProps {
addDocsMCPText: string;
}
export function DocsMCPAction({
addDocsMCPText,
}: DocsMCPActionProps): JSX.Element {
return (
<a
href={`https://docsearch.algolia.com/mcp/install?utm_source=${window.location.hostname}&utm_medium=referral&utm_content=add_docs_mcp&utm_campaign=docsearch`}
target="_blank"
rel="noopener noreferrer"
>
<PlusIcon />
{addDocsMCPText}
</a>
);
}

View file

@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import React from 'react';
import { describe, expect, it } from 'vitest';
import { DocSearchByAlgolia } from '../DocSearchByAlgolia';
describe('DocSearchByAlgolia', () => {
it('uses the default Sidepanel accessible label', () => {
render(<DocSearchByAlgolia byAlgoliaAriaLabel="DocSearch by Algolia" />);
expect(
screen.getByRole('link', { name: 'DocSearch by Algolia' })
).toBeInTheDocument();
});
it('uses a localized accessible label', () => {
render(<DocSearchByAlgolia byAlgoliaAriaLabel="DocSearch par Algolia" />);
expect(
screen.getByRole('link', { name: 'DocSearch par Algolia' })
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,29 @@
import React from 'react';
import type { JSX } from 'react';
interface DocSearchMarkIconProps {
className?: string;
}
export function DocSearchMarkIcon({
className = '',
}: DocSearchMarkIconProps): JSX.Element {
return (
<svg
viewBox="0 0 76 78"
xmlns="http://www.w3.org/2000/svg"
fillRule="nonzero"
className={className}
aria-hidden="true"
>
<path
d="M36.493 77.289H3.074C1.376 77.289 0 75.919 0 74.228h36.523c12.769.062 24.6-6.663 31.038-17.642 6.438-10.979 6.504-24.543.174-35.584C61.404 9.962 49.639 3.122 36.87 3.061H0C0 1.37 1.376 0 3.074 0h33.796c10.31.01 20.193 4.1 27.473 11.369 7.279 7.268 11.36 17.12 11.341 27.384-.06 21.366-17.741 38.536-39.19 38.536z"
fill="currentcolor"
/>
<path
d="M0 69.045h23.711c2.931.01 5.761-1.071 7.933-3.031H0v3.031zM0 60.8h36.374c.734-.987 1.418-1.975 2.053-3.07H0v3.07zM0 52.546h41.025c.407-.987.774-1.975 1.091-3.06H0v3.06zM0 44.302h43.306c.149-.987.268-2.034.337-3.061H0v3.061zM0 36.058h43.633a29.95 29.95 0 0 1-.337-3.071H0v3.071zM0 27.804h42.116c-.317-1.037-.684-2.064-1.09-3.061H0v3.061zM0 19.559h38.427a32.68 32.68 0 0 1-2.053-3.06H0v3.06zM0 8.244v3.071h31.674A19.04 19.04 0 0 0 23.74 8.284L0 8.244z"
fill="currentcolor"
/>
</svg>
);
}

View file

@ -0,0 +1,22 @@
import React from 'react';
import type { JSX } from 'react';
export function PlusIcon(): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
);
}

View file

@ -1,3 +1,4 @@
export * from './DocSearchMarkIcon';
export * from './GoToExternalIcon';
export * from './LoadingIcon';
export * from './SparklesIcon';
@ -22,6 +23,7 @@ export * from './ToolIcon';
export * from './MemoryIcon';
export * from './MessageIcon';
export * from './PinIcon';
export * from './PlusIcon';
export * from './BackIcon';
export * from './ChevronIcon';
export * from './CheckIcon';

View file

@ -86,11 +86,6 @@ export default {
button: {
buttonText: 'Go on, give it a search...',
},
modal: {
footer: {
poweredByText: 'Powered by',
},
},
},
},
metadata: [