Using AI chat with React
The React component for AI chat contains helpful facades to the underlying class for ease of use.
Contents
Overview
Installation
Using ChatContainer
Config object changes
Using ChatCustomElement
Accessing instance methods
User-defined responses
Overview
AI chat shows two React components that act as a facade in front of the core AI chat.
If you want to use the float
layout, use ChatContainer. Use the ChatCustomElement for custom sizes, such as a sidebar, full screen, or nested in your UI.
The React components provide helpful alternative ways to manage user_defined
responses, writeable elements, and the AI chat rendering cycle. For more information, see Using the API to fully understand the references.
Currently, this component does not support SSR, so if you are using Next.js or similar frameworks, make sure you render this component in client only modes.
For more information, see the examples page for more examples.
Installation
Install by using npm
:
npm install @carbon/ai-chat
Or using yarn
:
yarn add @carbon/ai-chat
Basic example
Render this component in your application and provide the configuration options for the AI chat as a prop. Refer to the following example.
import React from 'react';
import { ChatContainer } from '@carbon/ai-chat';
const chatOptions = {
// Your configuration object.
};
function App() {
return <ChatContainer config={chatOptions} />;
}
Using ChatContainer
The ChatContainer
is a functional component that loads and renders an instance of the AI chat when it mounts, and deletes the instance when unmounted. If the configuration for the AI chat changes, it also deletes the previous AI chat and creates a new one with the new configuration. It can also manage React portals for user-defined responses.
Note: This component calls the instance.render()
method for you. You do not need to call it yourself. You can use the onBeforeRender
or onAfterRender
properties to run operations before or after render calls. Refer to the following table.
Props
ChatContainer
has the following props.
Attribute | Required | Type | Description |
---|---|---|---|
config | Yes | object | The options object for configuring the chat. |
onBeforeRender | No | function | This callback function calls after the AI chat loads but before calling the render function. It passes a single argument, the loaded instance of the AI chat. To use the available instance methods, you can use this function to obtain a reference to the AI chat |
onAfterRender | No | function | This callback function calls after the AI chat loads but before calling the render function. It passes a single argument, the loaded instance of the AI chat. To use the available instance methods, you can use this function to obtain a reference to the AI chat instance. |
renderUserDefinedResponse | No | function | This function is a callback that the container calls to render user-defined responses. When you provide this prop, the container listens for user-defined response events from the AI chat and generates a React portal for each event. The function calls once during the component render for each user-defined response event. |
This function takes two arguments: |
- First argument: A RenderUserDefinedState interface that triggered the user-defined response. This property includes
partialItems
during a stream (if required), amessageItem
when the stream for that single item is complete, and afullMessage
when the entire message resolves (in cases where multiple items are streaming within the same message). - Second argument: A convenience argument, the instance of the AI chat. The function returns a
ReactNode
that renders the user-defined content for the response. |
| renderWriteableElement | No | Object | This takes an Object with writeable element key names and aReactNode
as the value. TheReactNode
is rendered into the writeable element slot. |
Config object changes
The chat is not capable of handling in-place changes to the config object. If any of the properties in the config object are changed, then the existing chat will be discarded and a whole new chat will be created using the new config properties. A deep equal comparison of the config object is done to detect changes. Note that DOM elements and functions are compared using a strict ===
comparison.
Of note, the above means that if you are creating a new callback function for something like customSendMessage
on each render of your component, this would cause the chat to be discarded and recreated each time your component is rendered.
Below is an example of bad code where a new customSendMessage
function causes the chat to get recreated each time App
is rendered.
function App() {
// This is bad because it creates a new customSendMessage function every time App is rendered which will cause the
// ChatContainer to create a new chat every time.
const customSendMessage = (message: MessageRequest) => {
console.log('Sending message', message);
};
const config: PublicConfig = { messaging: { customSendMessage } };
return <ChatContainer config={config} />;
}
The quickest solution to this is to move the config object outside of your render function.
const customSendMessage = (message: MessageRequest) => {
console.log('Sending message', message);
};
const config: PublicConfig = { messaging: { customSendMessage } };
function App() {
return <ChatContainer config={config} />;
}
However this does not work if your customSendMessage
function requires access to state or props from your application. If you need that, you can use useCallback
to ensure only a single instance is created from the dependencies. Note in this example that if the instance of the messageService
prop changes, the whole chat will get recreated. If you don't expect that to happen, this will work for you. Otherwise, you may need to use a ref.
function App({ messageService }: any) {
// Creates a new customSendMessage for each instance of messageService.
const customSendMessage = useCallback(
(message: MessageRequest) => {
messageService(message);
},
[messageService], // If messageService changes, the chat will be recreated.
);
const config: PublicConfig = { messaging: { customSendMessage } };
return <ChatContainer config={config} />;
}
If you have dependencies that shouldn't recreate the chat, you can store them in a ref. In this example, the chat will not be recreated.
function App({ messageService }: any) {
const messageServiceRef = useRef(messageService);
messageServiceRef.current = messageService;
const customSendMessage = useCallback((message: MessageRequest) => {
messageServiceRef.current(message);
}, []);
const config: PublicConfig = { messaging: { customSendMessage } };
return <ChatContainer config={config} />;
}
Using ChatCustomElement
This library provides the ChatCustomElement
component, which can be used to render the AI chat inside a custom element. Use it you want to change the location where the AI chat renders. This component renders an element in your React app and uses that element as the custom element for rendering the AI chat.
This component's default behavior adds and removes a class from the main window of the AI chat. It also applies the same behavior to your custom element to manage the visibility of the AI chat when it opens or closes. When the AI chat closes, it adds a classname to the AI chat main window to hide the element. Your custom element receives another classname to set its width and height to 0, so that it doesn't take up space.
Note: The use case where you are using a custom element but also by using the AI chat's native launcher, the custom element remains visible as it also contains the launcher.
If you don't want these behaviors, provide your own onViewChange
prop to ChatCustomElement
and provide your logic for controlling the visibility of the AI chat. If you want custom animations when the AI chat opens and closeds, it is the mechanism to do that. Refer to the following example.
import React from 'react';
import { ChatCustomElement } from '@carbon/ai-chat';
import './App.css';
const chatOptions = { /* AI chat options */ };
function App() {
return <ChatCustomElement className="MyCustomElement" config={chatOptions} />;
}
.MyCustomElement {
position: absolute;
left: 100px;
top: 100px;
width: 500px;
height: 500px;
}
ChatCustomElement
inherits all of the props from ChatContainer
. Refer to the following table for optional props.
Attribute | Type | Description |
---|---|---|
className | string | An optional classname that adds to the custom element. |
id | string | An optional id that adds to the custom element. |
onViewChange | function | A custom element controlling the visiblity of the AI chat's main window requires an optional listener for "view:change" events. If you do not provide a callback, the default one employs certain classnames to manage the AI chat and the custom element. You can provide a different callback if you want custom behavior, such as an animation when the main window opens or closes. |
Note: Provide this function before you load the AI chat. The event handler does not update after the AI chat loads.
Accessing instance methods
You can use the onBeforeRender
or onAfterRender
props to access the AI chat’s instance if you need to call instance methods later. This example renders a button that toggles the AI chat open and only renders after the instance becomes available. Refer to the following example.
import React, { useCallback, useState } from 'react';
import { ChatContainer } from '@carbon/ai-chat';
const chatOptions = {
// Your configuration object.
};
function App() {
const [instance, setInstance] = useState(null);
const toggleWebChat = useCallback(() => {
instance.toggleOpen();
}, [instance]);
function onBeforeRender(instance) {
// Make the instance available to the React components.
setInstance(instance);
}
return (
<>
{instance && (
<button type="button" onClick={toggleWebChat}>
Toggle AI chat
</button>
)}
<ChatContainer config={chatOptions} onBeforeRender={onBeforeRender} />
</>
);
}
User-defined responses
This component can also manage user-defined responses. You must pass a renderUserDefinedResponse
function as a render prop. This function returns a React component that renders content for the specific message that relates to that response. Keep the ReactChatContainer
component mounted during your application’s lifecycle, as it loses all user-defined responses that the AI chat previously received.
Treat the renderUserDefinedResponse
prop like any typical React render prop; it is different from the userDefinedResponse
event or a typical event handler. The event fires only once when the AI chat initially receives the response from the server. The renderUserDefinedResponse
prop calls every time the App rerenders, returning an up-to-date React component for the provided message item, just as the render function does for a typical React component. Refer to the following example.
import React from 'react';
import { ChatContainer } from '@carbon/ai-chat';
const chatOptions = {
// Your configuration object.
};
function App() {
return <ChatContainer renderUserDefinedResponse={renderUserDefinedResponse} config={chatOptions} />;
}
function renderUserDefinedResponse(state, instance) {
const { messageItem } = state;
// The event here contains details for each user defined response that needs to be rendered.
// You can also pass information from your components props or state into the component your are returning.
if (messageItem) {
switch (messageItem.user_defined?.user_defined_type) {
case 'chart':
return (
<div className="padding">
<Chart content={messageItem.user_defined.chart_data as string} />
</div>
);
case 'green':
return <UserDefinedResponseExample text={messageItem.user_defined.text as string} />;
default:
return undefined;
}
// We are just going to show a skeleton state here if we are waiting for a stream, but you can instead have another
// switch statement here that does something more specific depending on the component.
return <AISkeletonPlaceholder className="fullSkeleton" />;
}
return <ChatContainer renderUserDefinedResponse={renderUserDefinedResponse} config={chatOptions} />;
}
Testing with Jest
AI chat exports as an ES module and does not include a CJS build. Please refer to the Jest documentation for information about transforming ESM to CJS for Jest using babel-jest
or ts-jest
.
You may need to add configuration similar to the following to your Jest configuration.
"transform": {
"\\.[jt]sx?$": "babel-jest",
},
{
transformIgnorePatterns: [
'/node_modules/(?!(@?lit.*|@carbon/.+)/)'
],
}