Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/instantsearch.js/src/connectors/chat/connectChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ export type ChatConnectorParams<TUiMessage extends UIMessage = UIMessage> = (
* @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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you also considered passing messages or initialMessages? that would be a bit more flexible, but I'm not sure if it's useful for anything

};

export type ChatWidgetDescription<TUiMessage extends UIMessage = UIMessage> = {
Expand Down Expand Up @@ -260,6 +270,7 @@ export default (function connectChat<TWidgetParams extends UnknownWidgetParams>(
resume = false,
tools = {},
type = 'chat',
initialUserMessage,
...options
} = widgetParams || {};

Expand Down Expand Up @@ -547,6 +558,14 @@ export default (function connectChat<TWidgetParams extends UnknownWidgetParams>(
_chatInstance.resumeStream();
}

if (
initialUserMessage &&
!resume &&
_chatInstance.messages.length === 0
) {
_chatInstance.sendMessage({ text: initialUserMessage });
}

renderFn(
{
...this.getWidgetRenderState(initOptions),
Expand Down
63 changes: 63 additions & 0 deletions tests/common/connectors/chat/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<ChatConnectorSetup> = {
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<ChatConnectorSetup> = {
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<ChatConnectorSetup> = {
instantSearchOptions: {
Expand Down
74 changes: 74 additions & 0 deletions tests/common/widgets/chat/options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading