diff --git a/examples/react/getting-started/src/App.tsx b/examples/react/getting-started/src/App.tsx index ad1b9961829..3538e5c22e7 100644 --- a/examples/react/getting-started/src/App.tsx +++ b/examples/react/getting-started/src/App.tsx @@ -72,7 +72,7 @@ export function App() { diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx index 66e2d93de83..40163171d32 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatMessage.tsx @@ -126,6 +126,10 @@ export type ChatMessageProps = ComponentProps<'article'> & { * Close the chat */ onClose: () => void; + /** + * Function to send a message + */ + sendMessage: (message: string) => void; /** * Array of tools available for the assistant (for tool messages) */ @@ -158,6 +162,7 @@ export function createChatMessageComponent({ createElement }: Renderer) { indexUiState, setIndexUiState, onClose, + sendMessage, translations: userTranslations, ...props } = userProps; @@ -216,7 +221,18 @@ export function createChatMessageComponent({ createElement }: Renderer) { toolCallId: toolMessage.toolCallId, }); - if (!ToolLayoutComponent) { + if (toolMessage.state === 'input-available' && tool.renderLast) { + boundAddToolResult({ output: { message: '' } }); + } + + if ( + !ToolLayoutComponent || + (tool.renderLast && + (!message.parts.find((p) => p.type === 'text') || + message.parts.some( + (p) => p.type === 'text' && p.state === 'streaming' + ))) + ) { return null; } @@ -230,6 +246,7 @@ export function createChatMessageComponent({ createElement }: Renderer) { indexUiState={indexUiState} setIndexUiState={setIndexUiState} addToolResult={boundAddToolResult} + sendMessage={sendMessage} onClose={onClose} /> @@ -239,6 +256,17 @@ export function createChatMessageComponent({ createElement }: Renderer) { return null; } + const toolToRenderLast = message.parts.find((part) => { + if (!startsWith(part.type, 'tool-')) { + return false; + } + + const toolName = part.type.replace('tool-', ''); + const tool = tools[toolName]; + + return tool?.renderLast; + }); + return (
- {message.parts.map(renderMessagePart)} + {message.parts + .filter((part) => part !== toolToRenderLast) + .map(renderMessagePart)} + {toolToRenderLast && + renderMessagePart(toolToRenderLast, message.parts.length - 1)}
{hasActions && ( diff --git a/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx b/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx index 2a86b962932..75a46d12ffe 100644 --- a/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx +++ b/packages/instantsearch-ui-components/src/components/chat/ChatMessages.tsx @@ -116,6 +116,7 @@ export type ChatMessagesProps< * Function to close the chat */ onClose: () => void; + sendMessage: (message: string) => void; /** * Optional class names */ @@ -194,6 +195,7 @@ function createDefaultMessageComponent< setIndexUiState, onReload, onClose, + sendMessage, actionsComponent, classNames, messageTranslations, @@ -209,6 +211,7 @@ function createDefaultMessageComponent< onReload: (messageId?: string) => void; onClose: () => void; actionsComponent?: ChatMessageProps['actionsComponent']; + sendMessage: (newMessage: string) => void; translations: ChatMessagesTranslations; classNames?: Partial; messageTranslations?: Partial; @@ -244,6 +247,7 @@ function createDefaultMessageComponent< indexUiState={indexUiState} setIndexUiState={setIndexUiState} onClose={onClose} + sendMessage={sendMessage} actions={defaultActions} actionsComponent={actionsComponent} data-role={message.role} @@ -288,6 +292,7 @@ export function createChatMessagesComponent({ hideScrollToBottom = false, onReload, onClose, + sendMessage, translations: userTranslations, userMessageProps, assistantMessageProps, @@ -361,6 +366,7 @@ export function createChatMessagesComponent({ onReload={onReload} actionsComponent={ActionsComponent} onClose={onClose} + sendMessage={sendMessage} translations={translations} classNames={messageClassNames} messageTranslations={messageTranslations} diff --git a/packages/instantsearch-ui-components/src/components/chat/types.ts b/packages/instantsearch-ui-components/src/components/chat/types.ts index 4c6164dc496..bbd319c6eba 100644 --- a/packages/instantsearch-ui-components/src/components/chat/types.ts +++ b/packages/instantsearch-ui-components/src/components/chat/types.ts @@ -24,6 +24,7 @@ export type ClientSideToolComponentProps = { setIndexUiState: (state: object) => void; onClose: () => void; addToolResult: AddToolResultWithOutput; + sendMessage: (message: string) => void; }; export type ClientSideToolComponent = ( @@ -40,6 +41,7 @@ export type ClientSideTool = { addToolResult: AddToolResultWithOutput; } ) => void; + renderLast?: boolean; }; export type ClientSideTools = Record; diff --git a/packages/instantsearch.js/src/lib/chat/index.ts b/packages/instantsearch.js/src/lib/chat/index.ts index 6480d2fc5d0..6cd19e5f9c5 100644 --- a/packages/instantsearch.js/src/lib/chat/index.ts +++ b/packages/instantsearch.js/src/lib/chat/index.ts @@ -6,3 +6,4 @@ export { Chat } from './chat'; export const SearchIndexToolType = 'algolia_search_index'; export const RecommendToolType = 'algolia_recommend'; +export const PromptSuggestionsToolType = 'algolia_prompt_suggestions'; diff --git a/packages/react-instantsearch/src/widgets/Chat.tsx b/packages/react-instantsearch/src/widgets/Chat.tsx index 0e94f2266f9..bc5451f4a91 100644 --- a/packages/react-instantsearch/src/widgets/Chat.tsx +++ b/packages/react-instantsearch/src/widgets/Chat.tsx @@ -2,12 +2,14 @@ import { createChatComponent } from 'instantsearch-ui-components'; import { SearchIndexToolType, RecommendToolType, + PromptSuggestionsToolType, } from 'instantsearch.js/es/lib/chat'; import React, { createElement, Fragment } from 'react'; import { useInstantSearch, useChat } from 'react-instantsearch-core'; import { useStickToBottom } from '../lib/useStickToBottom'; +import { createPromptSuggestionsTool } from './chat/tools/PromptSuggestionsTool'; import { createCarouselTool } from './chat/tools/SearchIndexTool'; export { SearchIndexToolType, RecommendToolType }; @@ -45,6 +47,7 @@ export function createDefaultTools( itemComponent, getSearchPageURL ), + [PromptSuggestionsToolType]: createPromptSuggestionsTool(), }; } @@ -268,6 +271,9 @@ export function Chat< }, translations: messagesTranslations, messageTranslations, + sendMessage: (newMessage: string) => { + sendMessage({ text: newMessage }); + }, ...messagesProps, }} promptProps={{ diff --git a/packages/react-instantsearch/src/widgets/chat/tools/PromptSuggestionsTool.tsx b/packages/react-instantsearch/src/widgets/chat/tools/PromptSuggestionsTool.tsx new file mode 100644 index 00000000000..52ee1bf91aa --- /dev/null +++ b/packages/react-instantsearch/src/widgets/chat/tools/PromptSuggestionsTool.tsx @@ -0,0 +1,43 @@ +import { createButtonComponent } from 'instantsearch-ui-components'; +import React, { createElement } from 'react'; + +import type { + ClientSideToolComponentProps, + Pragma, + UserClientSideTool, +} from 'instantsearch-ui-components'; + +export function createPromptSuggestionsTool(): UserClientSideTool { + const Button = createButtonComponent({ + createElement: createElement as Pragma, + }); + + function PromptSuggestionsComponent({ + message, + sendMessage, + }: ClientSideToolComponentProps) { + const input = message.input as { promptSuggestions?: string[] } | undefined; + const promptSuggestions = input?.promptSuggestions; + + if (!promptSuggestions || promptSuggestions.length === 0) { + return <>; + } + + return ( +
    + {promptSuggestions.map((suggestion, index) => ( +
  • + +
  • + ))} +
+ ); + } + + return { + layoutComponent: PromptSuggestionsComponent, + renderLast: true, + }; +}