-
Notifications
You must be signed in to change notification settings - Fork 554
feat(chat): add greeting screen component #6965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e96eec7
00f78dc
c700e90
af1bb91
f240720
7031cdb
a14de97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| <p className={cx('ais-ChatGreeting-subheading', classNames.subheading)}> | ||
| {translations.greetingSubheading} | ||
| </p> | ||
| </div> | ||
| ); | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ import { | |
| } from './icons'; | ||
|
|
||
| import type { ComponentProps, MutableRef, Renderer, VNode } from '../../types'; | ||
| import type { ChatGreetingProps } from './ChatGreeting'; | ||
| import type { | ||
| ChatMessageProps, | ||
| ChatMessageActionProps, | ||
|
|
@@ -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 = { | ||
| /** | ||
|
|
@@ -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 | ||
| */ | ||
|
|
@@ -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 | ||
| */ | ||
|
|
@@ -361,6 +374,7 @@ export function createChatMessagesComponent({ | |
| messageComponent: MessageComponent, | ||
| loaderComponent: LoaderComponent, | ||
| errorComponent: ErrorComponent, | ||
| greetingComponent: GreetingComponent, | ||
| actionsComponent: ActionsComponent, | ||
| tools, | ||
| indexUiState, | ||
|
|
@@ -369,6 +383,8 @@ export function createChatMessagesComponent({ | |
| hideScrollToBottom = false, | ||
| onReload, | ||
| onClose, | ||
| sendMessage, | ||
| setInput, | ||
| translations: userTranslations, | ||
| userMessageProps, | ||
| assistantMessageProps, | ||
|
|
@@ -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; | ||
|
|
@@ -449,6 +468,15 @@ export function createChatMessagesComponent({ | |
| } | ||
| }} | ||
| > | ||
| {showGreeting && GreetingComponent && ( | ||
| <GreetingComponent | ||
| sendMessage={sendMessage} | ||
| setInput={setInput} | ||
| status={status} | ||
| onClose={onClose} | ||
| /> | ||
| )} | ||
|
Comment on lines
440
to
+478
|
||
|
|
||
| {messages.map((message, index) => ( | ||
| <DefaultMessage | ||
| key={message.id} | ||
|
|
||
| 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; | ||
| } |
| 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} />; | ||
| }; | ||
| } |
| 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, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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']; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| promptComponent?: ChatUiProps['promptComponent']; | ||
| promptHeaderComponent?: ChatUiProps['promptProps']['headerComponent']; | ||
| promptFooterComponent?: ChatUiProps['promptProps']['footerComponent']; | ||
|
|
@@ -186,6 +187,7 @@ function ChatInner< | |
| headerMaximizeIconComponent, | ||
| messagesLoaderComponent, | ||
| messagesErrorComponent, | ||
| messagesGreetingComponent, | ||
| promptComponent, | ||
| promptHeaderComponent, | ||
| promptFooterComponent, | ||
|
|
@@ -313,6 +315,8 @@ function ChatInner< | |
| status, | ||
| onReload: (messageId) => regenerate({ messageId }), | ||
| onClose: () => setOpen(false), | ||
| sendMessage: sendMessage as ChatUiProps['sendMessage'], | ||
| setInput, | ||
| onFeedback, | ||
| feedbackState, | ||
| messages, | ||
|
|
@@ -327,6 +331,7 @@ function ChatInner< | |
| onScrollToBottom: scrollToBottom, | ||
| loaderComponent: messagesLoaderComponent, | ||
| errorComponent: messagesErrorComponent, | ||
| greetingComponent: messagesGreetingComponent, | ||
| actionsComponent, | ||
| assistantMessageProps: { | ||
| leadingComponent: assistantMessageLeadingComponent, | ||
|
|
||
There was a problem hiding this comment.
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 settingclassName. If the caller passesclassName, it will override the computedais-ChatGreeting/classNames.rootclasses and break default styling. Consider mergingprops.classNameinto the computed class name and/or spreading props before settingclassName(while avoiding passing the rawclassNamethrough the spread).