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
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "122 kB"
"maxSize": "122.25 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "253.4 kB"
"maxSize": "254 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/** @jsx createElement */

import { cx } from '../../lib';

import type { ComponentProps, Renderer } from '../../types';
import type { ChatLayoutOwnProps, ChatStatus } from './types';

export type ChatGreetingTranslations = {
/**
* Heading text for the greeting
*/
greetingHeading: string;
/**
* Subheading text for the greeting
*/
greetingSubheading: string;
};

export type ChatGreetingClassNames = {
/**
* Class names to apply to the root element
*/
root?: string | string[];
/**
* Class names to apply to the heading element
*/
heading?: string | string[];
/**
* Class names to apply to the subheading element
*/
subheading?: string | string[];
};

export type ChatGreetingProps = ComponentProps<'div'> & {
/**
* Translations for greeting component texts
*/
translations?: Partial<ChatGreetingTranslations>;
/**
* Optional class names
*/
classNames?: Partial<ChatGreetingClassNames>;
/**
* Function to send a message to the chat
*/
sendMessage?: ChatLayoutOwnProps['sendMessage'];
/**
* Current chat status
*/
status?: ChatStatus;
/**
* Callback to close the chat
*/
onClose?: () => void;
/**
* Function to set the prompt input value
*/
setInput?: (input: string) => void;
};

export function createChatGreetingComponent({
createElement,
}: Pick<Renderer, 'createElement'>) {
return function ChatGreeting(userProps: ChatGreetingProps) {
const {
translations: userTranslations,
classNames = {},
sendMessage: _sendMessage,
status: _status,
onClose: _onClose,
setInput: _setInput,
...props
} = userProps;
const translations: Required<ChatGreetingTranslations> = {
greetingHeading:
userTranslations?.greetingHeading ??
'How can I help you today?',
greetingSubheading:
userTranslations?.greetingSubheading ??
"Ask me anything about our products, and I'll do my best to assist you.",
};

return (
<div
className={cx('ais-ChatGreeting', classNames.root)}
{...props}
>
<h2 className={cx('ais-ChatGreeting-heading', classNames.heading)}>
{translations.greetingHeading}
</h2>
Comment on lines +83 to +90
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The root <div> spreads {...props} after setting className. If the caller passes className, it will override the computed ais-ChatGreeting/classNames.root classes and break default styling. Consider merging props.className into the computed class name and/or spreading props before setting className (while avoiding passing the raw className through the spread).

Copilot uses AI. Check for mistakes.
<p className={cx('ais-ChatGreeting-subheading', classNames.subheading)}>
{translations.greetingSubheading}
</p>
</div>
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from './icons';

import type { ComponentProps, MutableRef, Renderer, VNode } from '../../types';
import type { ChatGreetingProps } from './ChatGreeting';
import type {
ChatMessageProps,
ChatMessageActionProps,
Expand All @@ -30,7 +31,7 @@ import type {
} from './ChatMessage';
import type { ChatMessageErrorProps } from './ChatMessageError';
import type { ChatMessageLoaderProps } from './ChatMessageLoader';
import type { ChatMessageBase, ChatStatus, ClientSideTools } from './types';
import type { ChatLayoutOwnProps, ChatMessageBase, ChatStatus, ClientSideTools } from './types';

export type ChatMessagesTranslations = {
/**
Expand Down Expand Up @@ -109,6 +110,10 @@ export type ChatMessagesProps<
* Custom error component
*/
errorComponent?: (props: ChatMessageErrorProps) => JSX.Element;
/**
* Custom greeting component shown when there are no messages
*/
greetingComponent?: (props: ChatGreetingProps) => JSX.Element;
/**
* Custom actions component
*/
Expand Down Expand Up @@ -141,6 +146,14 @@ export type ChatMessagesProps<
* Function to close the chat
*/
onClose: () => void;
/**
* Function to send a message to the chat
*/
sendMessage?: ChatLayoutOwnProps['sendMessage'];
/**
* Function to set the prompt input value
*/
setInput?: (input: string) => void;
/**
* Optional class names
*/
Expand Down Expand Up @@ -361,6 +374,7 @@ export function createChatMessagesComponent({
messageComponent: MessageComponent,
loaderComponent: LoaderComponent,
errorComponent: ErrorComponent,
greetingComponent: GreetingComponent,
actionsComponent: ActionsComponent,
tools,
indexUiState,
Expand All @@ -369,6 +383,8 @@ export function createChatMessagesComponent({
hideScrollToBottom = false,
onReload,
onClose,
sendMessage,
setInput,
translations: userTranslations,
userMessageProps,
assistantMessageProps,
Expand Down Expand Up @@ -421,6 +437,9 @@ export function createChatMessagesComponent({
isStreamingWithNoContent ||
isStreamingNonTextContent;

const showGreeting =
messages.length === 0 && !showLoader && !isClearing;

const DefaultMessage = MessageComponent || DefaultMessageComponent;
const DefaultLoader = LoaderComponent || DefaultLoaderComponent;
const DefaultError = ErrorComponent || DefaultErrorComponent;
Expand Down Expand Up @@ -449,6 +468,15 @@ export function createChatMessagesComponent({
}
}}
>
{showGreeting && GreetingComponent && (
<GreetingComponent
sendMessage={sendMessage}
setInput={setInput}
status={status}
onClose={onClose}
/>
)}
Comment on lines 440 to +478
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

showGreeting becomes true when status === 'error' and there are no messages (showLoader is false), which will render the greeting and the error state at the same time. This looks unintended; the greeting should likely be hidden when the chat is in an error state (and possibly other non-ready states).

Copilot uses AI. Check for mistakes.

{messages.map((message, index) => (
<DefaultMessage
key={message.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './chat/ChatMessage';
export * from './chat/ChatMessages';
export * from './chat/ChatMessageLoader';
export * from './chat/ChatMessageError';
export * from './chat/ChatGreeting';
export * from './chat/ChatPrompt';
export * from './chat/ChatPromptSuggestions';
export * from './chat/ChatToggleButton';
Expand Down
1 change: 1 addition & 0 deletions packages/instantsearch.css/src/components/chat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@use 'chat/chat-messages';
@use 'chat/chat-message';
@use 'chat/chat-message-loader';
@use 'chat/chat-greeting';
@use 'chat/chat-prompt';
@use 'chat/chat-carousel';
@use 'chat/chat-suggestions';
23 changes: 23 additions & 0 deletions packages/instantsearch.css/src/components/chat/_chat-greeting.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.ais-ChatGreeting {
--ais-chat-greeting-padding: 0.5rem;

display: flex;
flex-direction: column;
justify-content: center;
padding: var(--ais-chat-greeting-padding);
flex: 1;
gap: calc(var(--ais-spacing) * 0.5);
}

.ais-ChatGreeting-heading {
font-size: 1.25em;
font-weight: 700;
margin: 0;
}

.ais-ChatGreeting-subheading {
font-size: 0.875em;
opacity: 0.7;
margin: 0;
line-height: 1.5;
}
16 changes: 16 additions & 0 deletions packages/instantsearch.js/src/templates/chat-greeting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @jsx h */

import { createChatGreetingComponent } from 'instantsearch-ui-components';
import { h } from 'preact';

import type { ChatGreetingProps } from 'instantsearch-ui-components';

const ChatGreetingComponent = createChatGreetingComponent({
createElement: h,
});

export function chatGreeting() {
return function ChatGreetingTemplate(props: ChatGreetingProps) {
return <ChatGreetingComponent {...props} />;
};
}
1 change: 1 addition & 0 deletions packages/instantsearch.js/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './carousel/carousel';
export * from './chat-layout/chat-overlay-layout';
export * from './chat-layout/chat-inline-layout';
export * from './chat-layout/chat-sidepanel-layout';
export * from './chat-greeting';
28 changes: 28 additions & 0 deletions packages/instantsearch.js/src/widgets/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type {
ChatMessageActionProps,
ChatMessageBase,
ChatMessageErrorProps,
ChatGreetingProps,
ChatMessageLoaderProps,
ChatMessageProps,
ChatMessagesTranslations,
Expand Down Expand Up @@ -319,6 +320,9 @@ type ChatWrapperProps = {
| ((props: ChatMessageLoaderProps) => JSX.Element)
| undefined;
errorComponent: ((props: ChatMessageErrorProps) => JSX.Element) | undefined;
greetingComponent:
| ((props: ChatGreetingProps) => JSX.Element)
| undefined;
actionsComponent:
| ((props: { actions: ChatMessageActionProps[] }) => JSX.Element)
| undefined;
Expand All @@ -332,6 +336,8 @@ type ChatWrapperProps = {
};
translations: Partial<ChatMessagesTranslations>;
messageTranslations: Partial<ChatMessageProps['translations']>;
sendMessage: ChatLayoutOwnProps['sendMessage'];
setInput: (input: string) => void;
};
promptProps: {
layoutComponent: ComponentProps<typeof Chat>['promptComponent'];
Expand Down Expand Up @@ -435,11 +441,14 @@ function ChatWrapper({
tools: toolsForUi,
loaderComponent: messagesProps.loaderComponent,
errorComponent: messagesProps.errorComponent,
greetingComponent: messagesProps.greetingComponent,
actionsComponent: messagesProps.actionsComponent,
assistantMessageProps: messagesProps.assistantMessageProps,
userMessageProps: messagesProps.userMessageProps,
translations: messagesProps.translations,
messageTranslations: messagesProps.messageTranslations,
sendMessage: messagesProps.sendMessage,
setInput: messagesProps.setInput,
}}
promptProps={{
promptRef: promptProps.promptRef,
Expand Down Expand Up @@ -656,6 +665,18 @@ const createRenderer = <THit extends RecordWithObjectID = RecordWithObjectID>({
);
}
: undefined;
const messagesGreetingComponent = templates.messages?.greeting
? (greetingProps: ChatGreetingProps) => {
return (
<TemplateComponent
{...messagesTemplateProps}
templateKey="greeting"
rootTagName="div"
data={greetingProps}
/>
);
}
: undefined;
const messagesTranslations: Partial<ChatMessagesTranslations> =
getDefinedProperties({
scrollToBottomLabel: templates.messages?.scrollToBottomLabelText,
Expand Down Expand Up @@ -916,6 +937,7 @@ const createRenderer = <THit extends RecordWithObjectID = RecordWithObjectID>({
messagesProps={{
loaderComponent: messagesLoaderComponent,
errorComponent: messagesErrorComponent,
greetingComponent: messagesGreetingComponent,
actionsComponent,
assistantMessageProps: {
leadingComponent: assistantMessageLeadingComponent,
Expand All @@ -927,6 +949,8 @@ const createRenderer = <THit extends RecordWithObjectID = RecordWithObjectID>({
},
translations: messagesTranslations,
messageTranslations,
sendMessage,
setInput,
}}
promptProps={{
layoutComponent: promptLayoutComponent,
Expand Down Expand Up @@ -1083,6 +1107,10 @@ export type ChatTemplates<THit extends NonNullable<object> = BaseHit> =
* Label for the regenerate action
*/
regenerateLabelText?: string;
/**
* Template to use for the greeting shown when there are no messages
*/
greeting?: Template<ChatGreetingProps>;
}>;

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/react-instantsearch/src/components/ChatGreeting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createChatGreetingComponent } from 'instantsearch-ui-components';
import { createElement } from 'react';

import type { Pragma } from 'instantsearch-ui-components';

export const ChatGreeting = createChatGreetingComponent({
createElement: createElement as Pragma,
});
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './Carousel';
export * from './ChatOverlayLayout';
export * from './ChatInlineLayout';
export * from './ChatSidePanelLayout';
export * from './ChatGreeting';
5 changes: 5 additions & 0 deletions packages/react-instantsearch/src/widgets/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export type ChatProps<TObject, TUiMessage extends UIMessage = UIMessage> = Omit<
headerMaximizeIconComponent?: ChatUiProps['headerProps']['maximizeIconComponent'];
messagesLoaderComponent?: ChatUiProps['messagesProps']['loaderComponent'];
messagesErrorComponent?: ChatUiProps['messagesProps']['errorComponent'];
messagesGreetingComponent?: ChatUiProps['messagesProps']['greetingComponent'];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've added it as messages component for now since the greeting component would have to be rendered inside the messages container.

Can be renamed if or when we rename the other messages components to remove the messages prefix (messagesLoaderComponent, messagesErrorComponent).

promptComponent?: ChatUiProps['promptComponent'];
promptHeaderComponent?: ChatUiProps['promptProps']['headerComponent'];
promptFooterComponent?: ChatUiProps['promptProps']['footerComponent'];
Expand Down Expand Up @@ -186,6 +187,7 @@ function ChatInner<
headerMaximizeIconComponent,
messagesLoaderComponent,
messagesErrorComponent,
messagesGreetingComponent,
promptComponent,
promptHeaderComponent,
promptFooterComponent,
Expand Down Expand Up @@ -313,6 +315,8 @@ function ChatInner<
status,
onReload: (messageId) => regenerate({ messageId }),
onClose: () => setOpen(false),
sendMessage: sendMessage as ChatUiProps['sendMessage'],
setInput,
onFeedback,
feedbackState,
messages,
Expand All @@ -327,6 +331,7 @@ function ChatInner<
onScrollToBottom: scrollToBottom,
loaderComponent: messagesLoaderComponent,
errorComponent: messagesErrorComponent,
greetingComponent: messagesGreetingComponent,
actionsComponent,
assistantMessageProps: {
leadingComponent: assistantMessageLeadingComponent,
Expand Down
Loading
Loading