diff --git a/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx b/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx
index 4dc7db40..3a18589e 100644
--- a/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx
+++ b/packages/docsearch-react/src/Sidepanel/ConversationScreen.tsx
@@ -8,7 +8,7 @@ import { MemoizedMarkdown } from '../MemoizedMarkdown';
import type { StoredSearchPlugin } from '../stored-searches';
import { ToolCall, type ToolCallTranslations } from '../ToolCall';
import type { StoredAskAiState } from '../types';
-import type { AIMessage } from '../types/AskiAi';
+import { isAIToolPart, type AIMessage } from '../types/AskiAi';
import { extractLinksFromMessage, getMessageContent } from '../utils/ai';
import { groupConsecutiveToolResults } from '../utils/groupConsecutiveToolResults';
@@ -159,7 +159,7 @@ const ConversationExchange = React.forwardRef;
}
- if (part.type === 'tool-searchIndex' || part.type === 'tool-algolia_search_index') {
+ if (isAIToolPart(part)) {
return (
@@ -73,7 +73,7 @@ export function ToolCall({ part, translations, onSearchQueryClick }: ToolCallPro
) : (
"{query || ''}"
)}{' '}
- found {numberOfHits || 0} results
+ found {numberOfHits} results
);
diff --git a/packages/docsearch-react/src/__tests__/ToolCall.test.tsx b/packages/docsearch-react/src/__tests__/ToolCall.test.tsx
new file mode 100644
index 00000000..2d6278a5
--- /dev/null
+++ b/packages/docsearch-react/src/__tests__/ToolCall.test.tsx
@@ -0,0 +1,71 @@
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import React from 'react';
+import { describe, it, expect } from 'vitest';
+
+import { ToolCall, type ToolCallTranslations } from '../ToolCall';
+import type { AIToolPart } from '../types/AskiAi';
+
+const TRANSLATIONS: ToolCallTranslations = {
+ preToolCallText: 'Searching for',
+ searchingText: 'Searching...',
+ toolCallResultText: 'Searched',
+};
+
+describe('ToolCall', () => {
+ describe('number of hits rendering', () => {
+ it.each([
+ {
+ description: 'tool-searchIndex with hits array',
+ part: {
+ type: 'tool-searchIndex',
+ toolCallId: 'id-1',
+ state: 'output-available',
+ input: { query: 'test' },
+ output: { query: 'test', hits: [{}, {}, {}] },
+ },
+ expectedHits: 3,
+ },
+ {
+ description: 'tool-searchIndex with empty hits',
+ part: {
+ type: 'tool-searchIndex',
+ toolCallId: 'id-2',
+ state: 'output-available',
+ input: { query: 'test' },
+ output: { query: 'test', hits: [] },
+ },
+ expectedHits: 0,
+ },
+ {
+ description: 'tool-algolia_search_index with nbHits',
+ part: {
+ type: 'tool-algolia_search_index',
+ toolCallId: 'id-3',
+ state: 'output-available',
+ input: { index: 'docs', query: 'test', number_of_results: 10, facet_filters: null },
+ output: { hits: [{}, {}] },
+ },
+ expectedHits: 2,
+ },
+ {
+ description: 'tool-algolia_search_index_custom with results array',
+ part: {
+ type: 'tool-algolia_search_index_custom',
+ toolCallId: 'id-4',
+ state: 'output-available',
+ input: { query: 'test' },
+ output: { hits: [{}] },
+ },
+ expectedHits: 1,
+ },
+ ] satisfies Array<{ description: string; part: AIToolPart; expectedHits: number }>)(
+ 'displays $expectedHits results for $description',
+ ({ part, expectedHits }) => {
+ render();
+
+ expect(screen.getByText(`found ${expectedHits} results`, { exact: false })).toBeInTheDocument();
+ },
+ );
+ });
+});
diff --git a/packages/docsearch-react/src/types/AskiAi.ts b/packages/docsearch-react/src/types/AskiAi.ts
index 53a497a2..48e29dc7 100644
--- a/packages/docsearch-react/src/types/AskiAi.ts
+++ b/packages/docsearch-react/src/types/AskiAi.ts
@@ -27,7 +27,19 @@ export interface AgentStudioSearchTool {
};
}
+export interface AlgoliaMCPSearchTool {
+ input: {
+ query: string;
+ };
+ output: {
+ hits?: any[];
+ nbHits?: number;
+ };
+}
+
type Tools = {
+ [K in `algolia_search_index_${string}`]: AlgoliaMCPSearchTool;
+} & {
searchIndex: SearchIndexTool;
algolia_search_index: AgentStudioSearchTool;
};
@@ -37,3 +49,7 @@ export type AIMessage = UIMessage<{ stopped?: boolean }, UIDataTypes, Tools>;
export type AIMessagePart = UIMessagePart;
export type AIToolPart = ToolUIPart;
+
+export function isAIToolPart(part: AIMessagePart): part is AIToolPart {
+ return part.type.startsWith('tool-');
+}
diff --git a/packages/docsearch-react/src/types/__tests__/AskiAi.test.ts b/packages/docsearch-react/src/types/__tests__/AskiAi.test.ts
new file mode 100644
index 00000000..99bc0396
--- /dev/null
+++ b/packages/docsearch-react/src/types/__tests__/AskiAi.test.ts
@@ -0,0 +1,41 @@
+import { describe, it, expect } from 'vitest';
+
+import type { AIMessagePart } from '../AskiAi';
+import { isAIToolPart } from '../AskiAi';
+
+describe('isAIToolPart', () => {
+ it.each([
+ {
+ part: {
+ type: 'tool-searchIndex',
+ toolCallId: 'id-1',
+ state: 'output-available',
+ input: { query: 'test' },
+ output: { hits: [] },
+ },
+ expected: true,
+ },
+ {
+ part: {
+ type: 'tool-algolia_search_index',
+ toolCallId: 'id-2',
+ state: 'input-streaming',
+ input: {},
+ },
+ expected: true,
+ },
+ {
+ part: { type: 'text', text: 'Hello' },
+ expected: false,
+ },
+ {
+ part: { type: 'reasoning', text: 'Thinking...' },
+ expected: false,
+ },
+ ] satisfies Array<{ part: AIMessagePart; expected: boolean }>)(
+ 'returns $expected for $part.type',
+ ({ part, expected }) => {
+ expect(isAIToolPart(part)).toBe(expected);
+ },
+ );
+});