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); + }, + ); +});