Constructor
Using the main components (Chat, History, ChatPage) and their slots, you can flexibly control the appearance and behavior logic of your chat.
Custom drawer
By default, History is displayed in the left container in desktop mode, while in the mobile version, the component is wrapped in Drawer. We can change this behavior using the desktop version of History.
In this example, the History component is used in the Drawer component, AppBar for display the title of the active thread.
<AppBar>
<Toolbar sx={{ gap: 6, justifyContent: 'space-between' }}>
<Typography noWrap variant="h6" component="div">
{activeThreadName}
</Typography>
<IconButton color='inherit' onClick={toggleDrawerOpen}>
<MenuIcon />
</IconButton>
</Toolbar>
</AppBar>
<Drawer
keepMounted={true}
open={drawerOpen}
anchor="right"
sx={{
[`& .${historyClassname}`]: {
width: isMobile ? 200 : 400,
maxWidth: 'none',
}
}}
onClose={handleDrawerClose}
>
<History apiRef={apiRef} className={historyClassname} />
</Drawer>import * as React from "react";
import {
useAssistantAnswerMock,
Thread,
Chat,
History,
useChatApiRef,
} from "@plteam/chat-ui";
import Box from '@mui/material/Box';
import { useTheme } from "@mui/material/styles";
import Typography from '@mui/material/Typography';
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import useMediaQuery from "@mui/material/useMediaQuery";
import Drawer from "@mui/material/Drawer";
const historyClassname = 'example-chat-history';
const date = (new Date()).toISOString();
const threads: Thread[] = [
{
id: "1",
title: "Traveling to Japan",
date: date,
messages: [
{
id: "1",
content: "Hi! Do you know anything about traveling to Japan?",
role: "user",
},
{
id: "2",
content: "Hi! Yes, I know a bit. What specifically do you want to know? Transportation, culture, or something else?",
role: "assistant",
},
{
id: "3",
content: "I'm curious about transportation. How does the train system work?",
role: "user",
},
{
id: "4",
content: "Japan has an excellent train system. There are high-speed trains called Shinkansen connecting major cities, and regional lines are great for shorter trips.",
role: "assistant",
},
],
},
{
id: "2",
title: "Small talk",
date: date,
messages: [
{
role: "user",
content: "Hello, how are you today?",
},
{
role: "assistant",
content: "Hi there! I'm doing great, thanks for asking. What can I help you with this morning?",
},
],
},
];
const App: React.FC = () => {
const [drawerOpen, setDrawerOpen] = React.useState<boolean>(false);
const [activeThreadName, setActiveThreadName] = React.useState<string>(threads[0].title);
const apiRef = useChatApiRef();
const { onUserMessageSent, handleStopMessageStreaming } =
useAssistantAnswerMock();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const toggleDrawerOpen = () => {
setDrawerOpen(!drawerOpen);
};
const handleDrawerClose = () => {
setDrawerOpen(false);
};
const onChangeCurrentThread = (params: { thread: Thread | undefined }) => {
setActiveThreadName(params.thread?.title || '');
handleDrawerClose();
};
return (
<Box
height="100vh"
width="100vw"
>
<AppBar>
<Toolbar sx={{ gap: 6, justifyContent: 'space-between' }}>
<Typography noWrap variant="h6" component="div">
{activeThreadName}
</Typography>
<IconButton color='inherit' onClick={toggleDrawerOpen}>
<MenuIcon />
</IconButton>
</Toolbar>
</AppBar>
<Drawer
keepMounted={true}
open={drawerOpen}
anchor="right"
sx={{
[`& .${historyClassname}`]: {
width: isMobile ? 200 : 400,
maxWidth: 'none',
}
}}
onClose={handleDrawerClose}
>
<History apiRef={apiRef} className={historyClassname} />
</Drawer>
<Box
width="100%"
height='calc(100vh - 64px)'
overflow="auto"
paddingTop={8}
>
<Chat
initialThread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
apiRef={apiRef}
onUserMessageSent={onUserMessageSent}
onChangeCurrentThread={onChangeCurrentThread}
/>
</Box>
</Box>
);
};
export default App;Chat children context
You can embed a component in Chat’s children to give it access to the internal context, which will enhance your application’s capabilities.
<Chat
initialThread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
>
<ChildrenComponent />
</Chat>import * as React from "react";
import {
useAssistantAnswerMock,
Thread,
Chat,
useChatContext,
} from "@plteam/chat-ui";
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from "@mui/material/IconButton";
import InfoIcon from '@mui/icons-material/Info';
import RefreshIcon from '@mui/icons-material/Refresh';
import Modal from "@mui/material/Modal";
import Stack from "@mui/material/Stack";
import { styled } from '@mui/material/styles';
import { AppBar, Toolbar } from "@mui/material";
const date = (new Date()).toISOString();
const threads: Thread[] = [
{
id: "1",
title: "Traveling to Japan",
date: date,
messages: [
{
id: "1",
content: "Hi! Do you know anything about traveling to Japan?",
role: "user",
},
{
id: "2",
content: "Hi! Yes, I know a bit. What specifically do you want to know? Transportation, culture, or something else?",
role: "assistant",
},
{
id: "3",
content: "I'm curious about transportation. How does the train system work?",
role: "user",
},
{
id: "4",
content: "Japan has an excellent train system. There are high-speed trains called Shinkansen connecting major cities, and regional lines are great for shorter trips.",
role: "assistant",
},
],
},
];
const StackStyled = styled(Stack)(({ theme }) => ({
position: 'absolute',
top: '50%',
left: '50%',
backgroundColor: theme.palette.background.paper,
transform: 'translate(-50%, -50%)',
width: 400,
border: '2px solid #000',
padding: theme.spacing(2),
}));
type InfoModalProps = {
open: boolean,
onClose: () => void,
};
const ParamsRow: React.FC<{ label: string, value: string }> = ({ label, value }) => (
<Stack flexDirection='row' gap={1}>
<Typography fontWeight={600}>
{label + ':'}
</Typography>
<Typography>
{value}
</Typography>
</Stack>
);
const InfoModal: React.FC<InfoModalProps> = ({ open, onClose }) => {
const { model } = useChatContext();
const thread = model.currentThread.value;
return (
<Modal open={open} onClose={onClose}>
<StackStyled gap={1}>
<ParamsRow label="Thread title" value={`${thread?.title}`} />
<ParamsRow label="Thread id" value={`${thread?.id}`} />
<ParamsRow label="Total number of messages" value={`${thread?.messages.length}`} />
</StackStyled>
</Modal>
);
};
const ChildrenComponent = () => {
const [modalOpen, setModalOpen] = React.useState<boolean>(false);
const { apiRef, model } = useChatContext();
const handleModalOpen = () => {
setModalOpen(true);
};
const handleModalClose = () => {
setModalOpen(false);
};
const handleClearChat = () => {
const currentThread = model.currentThread.value;
if (currentThread) model.delete(currentThread.id);
const newThread = apiRef.current?.handleCreateNewThread?.();
if (newThread) apiRef.current?.openNewThread(newThread);
};
return (
<>
<AppBar>
<Toolbar>
<IconButton color='inherit' onClick={handleModalOpen}>
<InfoIcon />
</IconButton>
<IconButton color='inherit' onClick={handleClearChat}>
<RefreshIcon />
</IconButton>
</Toolbar>
</AppBar>
<InfoModal open={modalOpen} onClose={handleModalClose} />
</>
);
};
const App: React.FC = () => {
const { onUserMessageSent, handleStopMessageStreaming } =
useAssistantAnswerMock();
return (
<Box
height="100dvh"
width="100dvw"
>
<Box
width="100%"
height='calc(100vh - 64px)'
overflow="auto"
paddingTop={8}
>
<Chat
initialThread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
>
<ChildrenComponent />
</Chat>
</Box>
</Box>
);
};
export default App;Custom scroll container
By default, CUI Kit uses the window for auto-scrolling; if you want to embed the chat in your own container, pass its React Reference through the scrollerRef prop.
const scrollRef = React.useRef<HTMLDivElement | null>(null);
...
<MainBoxStyled
component="main"
ref={scrollRef}
>
<Chat
initialThread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
apiRef={apiRef}
scrollerRef={scrollRef}
/>
</MainBoxStyled>import * as React from "react";
import {
useAssistantAnswerMock,
Thread,
Chat,
useChatApiRef,
chatClassNames,
} from "@plteam/chat-ui";
import Box from '@mui/material/Box';
import { styled } from "@mui/material/styles";
import Typography from '@mui/material/Typography';
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
const assisatntMessageContent = `
I’m designed to understand and generate human-like text based on the input I receive. Here are some of my key capabilities:
1. Conversation and Q&A: I can answer questions across a wide range of topics, explain complex concepts, and engage in back-and-forth dialogue on nearly any subject.
2. Creative and Technical Writing: Whether you need help drafting an email, writing a story, or even composing code, I can generate text in various styles and formats. I can also help with editing and refining your text.
3. Problem Solving: I can assist with analyzing problems, brainstorming solutions, summarizing information, and even tackling mathematical or logical puzzles.
4. Multilingual Support: I’m capable of working in several languages, translating text, or helping you learn about language nuances.
5. Learning and Information: I draw from a vast pool of generalized knowledge, which means I can provide context, historical background, technical details, and more on many topics. (That said, while I strive for accuracy, it’s good to verify specific details if they’re critical.)
6. Adaptability: I can adjust the tone and style of my responses based on your needs, whether you’d prefer a formal explanation, a casual conversation, or something creative.
While I have these versatile capabilities, I also have limitations. I don’t have real-time access to current events or internet browsing capabilities, and my knowledge is up-to-date only until a specific cutoff. Additionally, although I try to provide accurate and helpful information, I might not always fully capture the nuances of highly specialized or rapidly changing fields.
If you have any more questions or need help with something specific, feel free to ask!
`;
const MainBoxStyled = styled(Box)(({ theme }) => ({
width: `100%`,
height: `calc(100% - 64px)`,
position: 'absolute',
display: 'flex',
top: 64,
overflow: 'auto',
[theme.breakpoints.down('sm')]: {
left: 0,
width: '100%',
},
[`& .${chatClassNames.threadRoot}`]: {
height: '100%',
},
}));
const App: React.FC = () => {
const [threads] = React.useState<Thread[]>([
{
id: "test-thread",
title: "Welcome message",
messages: [
{
role: "user",
content: "Describe your capabilities",
},
{
role: "assistant",
content: assisatntMessageContent,
},
],
},
]);
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const { onUserMessageSent, handleStopMessageStreaming } =
useAssistantAnswerMock({ loremIpsumSize: 'large' });
const apiRef = useChatApiRef();
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: '100%',
alignItems: 'center',
}}
>
<Toolbar sx={{ maxWidth: 700, width: '100%' }}>
<Typography variant="h6" noWrap component="div">
CUI Kit
</Typography>
</Toolbar>
</AppBar>
<MainBoxStyled
component="main"
ref={scrollRef}
>
<Chat
initialThread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
apiRef={apiRef}
scrollerRef={scrollRef}
/>
</MainBoxStyled>
</Box>
);
}
export default App;