diff --git a/packages/instantsearch.js/src/connectors/chat/connectChat.ts b/packages/instantsearch.js/src/connectors/chat/connectChat.ts index 9d88d3cce5..0fda1fc297 100644 --- a/packages/instantsearch.js/src/connectors/chat/connectChat.ts +++ b/packages/instantsearch.js/src/connectors/chat/connectChat.ts @@ -159,6 +159,16 @@ export type ChatConnectorParams = ( * @default 'chat' */ type?: string; + /** + * A message to send automatically when the chat is initialized. + * + * This message is only sent when the chat has no existing messages yet. If + * messages were restored or otherwise already exist when the widget starts, + * this message is not sent. + * + * When `resume` is enabled, this message is not sent. + */ + initialUserMessage?: string; }; export type ChatWidgetDescription = { @@ -260,6 +270,7 @@ export default (function connectChat( resume = false, tools = {}, type = 'chat', + initialUserMessage, ...options } = widgetParams || {}; @@ -547,6 +558,14 @@ export default (function connectChat( _chatInstance.resumeStream(); } + if ( + initialUserMessage && + !resume && + _chatInstance.messages.length === 0 + ) { + _chatInstance.sendMessage({ text: initialUserMessage }); + } + renderFn( { ...this.getWidgetRenderState(initOptions), diff --git a/tests/common/connectors/chat/options.ts b/tests/common/connectors/chat/options.ts index f5a1fd9b53..5a95c70868 100644 --- a/tests/common/connectors/chat/options.ts +++ b/tests/common/connectors/chat/options.ts @@ -2,6 +2,7 @@ import { createSearchClient } from '@instantsearch/mocks'; import { wait } from '@instantsearch/testutils'; import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; +import { Chat } from 'instantsearch.js/src/lib/chat'; import { skippableDescribe } from '../../common'; @@ -31,6 +32,68 @@ export function createOptionsTests( `); }); + test('sends initialUserMessage on init', async () => { + const chat = new Chat({}); + const sendMessageSpy = jest + .spyOn(chat, 'sendMessage') + .mockResolvedValue(undefined); + + const options: SetupOptions = { + instantSearchOptions: { + indexName: 'indexName', + searchClient: createSearchClient(), + }, + widgetParams: { + chat, + agentId: 'agentId', + initialUserMessage: 'Hello, AI!', + } as any, + }; + + await setup(options); + + await act(async () => { + await wait(0); + }); + + expect(sendMessageSpy).toHaveBeenCalledWith({ text: 'Hello, AI!' }); + }); + + test('does not send initialUserMessage when messages already exist', async () => { + const chat = new Chat({ + messages: [ + { + id: '1', + role: 'user', + parts: [{ type: 'text', text: 'Previous message' }], + }, + ], + }); + const sendMessageSpy = jest + .spyOn(chat, 'sendMessage') + .mockResolvedValue(undefined); + + const options: SetupOptions = { + instantSearchOptions: { + indexName: 'indexName', + searchClient: createSearchClient(), + }, + widgetParams: { + chat, + agentId: 'agentId', + initialUserMessage: 'Hello, AI!', + } as any, + }; + + await setup(options); + + await act(async () => { + await wait(0); + }); + + expect(sendMessageSpy).not.toHaveBeenCalled(); + }); + test('provides `input` state to persist text input', async () => { const options: SetupOptions = { instantSearchOptions: { diff --git a/tests/common/widgets/chat/options.tsx b/tests/common/widgets/chat/options.tsx index 7e36f6a9a9..57b08c0490 100644 --- a/tests/common/widgets/chat/options.tsx +++ b/tests/common/widgets/chat/options.tsx @@ -44,6 +44,80 @@ export function createOptionsTests( expect(document.querySelector('.ais-ChatPrompt')).toBeInTheDocument(); }); + test('sends initialUserMessage on init', async () => { + const searchClient = createSearchClient(); + + const chat = new Chat({}); + const sendMessageSpy = jest + .spyOn(chat, 'sendMessage') + .mockResolvedValue(undefined); + + await setup({ + instantSearchOptions: { + indexName: 'indexName', + searchClient, + }, + widgetParams: { + javascript: { + ...createDefaultWidgetParams(chat), + initialUserMessage: 'Hello, AI!', + }, + react: { + ...createDefaultWidgetParams(chat), + initialUserMessage: 'Hello, AI!', + }, + vue: {}, + }, + }); + + await act(async () => { + await wait(0); + }); + + expect(sendMessageSpy).toHaveBeenCalledWith({ text: 'Hello, AI!' }); + }); + + test('does not send initialUserMessage when messages already exist', async () => { + const searchClient = createSearchClient(); + + const chat = new Chat({ + messages: [ + { + id: '1', + role: 'user', + parts: [{ type: 'text', text: 'Previous message' }], + }, + ], + }); + const sendMessageSpy = jest + .spyOn(chat, 'sendMessage') + .mockResolvedValue(undefined); + + await setup({ + instantSearchOptions: { + indexName: 'indexName', + searchClient, + }, + widgetParams: { + javascript: { + ...createDefaultWidgetParams(chat), + initialUserMessage: 'Hello, AI!', + }, + react: { + ...createDefaultWidgetParams(chat), + initialUserMessage: 'Hello, AI!', + }, + vue: {}, + }, + }); + + await act(async () => { + await wait(0); + }); + + expect(sendMessageSpy).not.toHaveBeenCalled(); + }); + test('sends messages when prompt is submitted', async () => { const searchClient = createSearchClient();