Chat Constructor
Chat Constructor
Overview
Chat UI consists of two main components: a thread block (message list and text input field) and a list of dialogs.
With the Slots, React Portals or MUI Portal functionality, you can position the components anywhere.
ℹ️
ADVANCED: Components that you pass via
slots
or as Chat UI children can use the internal chat context.Constructor
How to move threads list to your block
- Create a MUI/React Portal.
- Pass the created portal into the chat’s
slots
(threadsList
). The dialog list will be placed as children. - Position the container for the dialog list anywhere.
Example:
Collapse code
Expand code
<Chat
thread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
apiRef={apiRef}
scrollerRef={scrollRef}
onChangeCurrentThread={handleDrawerClose}
slots={{
threadsList: ToolsPanelPortal,
}}
slotProps={{
threadsList: {
handleDrawerClose,
containerRef: toolsContainerRef,
},
}}
/>
import * as React from "react";
import {
useAssistantAnswerMock,
Thread, Chat, useChatApiRef, chatClassNames, useChatContext,
} from "@plteam/chat-ui";
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import { styled, useTheme } from "@mui/material/styles";
import Typography from '@mui/material/Typography';
import Drawer from "@mui/material/Drawer";
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 Divider from "@mui/material/Divider";
import Button from '@mui/material/Button';
import AddIcon from '@mui/icons-material/Add';
import { Portal } from '@mui/base/Portal';
import useMediaQuery from "@mui/material/useMediaQuery";
import CloseIcon from '@mui/icons-material/Close';
const drawerWidth = 240;
const MainBoxStyled = styled(Box)(({ theme }) => ({
width: `calc(100% - ${drawerWidth}px)`,
height: `calc(100% - 64px)`,
position: 'absolute',
display: 'flex',
top: 64,
left: drawerWidth,
overflow: 'auto',
[theme.breakpoints.down('sm')]: {
left: 0,
width: '100%',
},
[`& .${chatClassNames.threadRoot}`]: {
height: '100%',
},
}));
type ToolsPanelProps = React.PropsWithChildren<{
handleDrawerClose: () => void;
containerRef: React.MutableRefObject<HTMLDivElement | null>;
}>;
const ToolsPanelPortal: React.FC<ToolsPanelProps> = ({ handleDrawerClose, children, containerRef }) => {
const { apiRef } = useChatContext();
const onOpenNew = React.useCallback(() => {
apiRef.current?.openNewThread();
handleDrawerClose();
}, [apiRef.current]);
return (
<Portal
container={() => containerRef.current!}
>
<Toolbar sx={{ justifyContent: 'flex-end' }}>
<IconButton onClick={handleDrawerClose}>
<CloseIcon />
</IconButton>
</Toolbar>
<Divider />
<Stack
gap={2}
>
<Box px={1.5} mt={2}>
<Button
startIcon={<AddIcon />}
onClick={onOpenNew}
fullWidth
variant="contained"
>
Open new thread
</Button>
</Box>
<Typography sx={{ pl: 1 }} fontWeight={'bold'}>
Threads list
</Typography>
{children}
</Stack>
</Portal>
);
};
const App: React.FC = () => {
const [threads] = React.useState<Thread[]>([
{
id: "test-thread",
title: "Welcome message",
messages: [
{
role: "user",
content: "Hello!",
},
{
role: "assistant",
content: "Hello there! How can I assist you today?",
},
],
},
]);
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const toolsContainerRef = React.useRef<HTMLDivElement | null>(null);
const { onUserMessageSent, handleStopMessageStreaming } =
useAssistantAnswerMock({
delayTimeout: 5000
});
const apiRef = useChatApiRef();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [mobileOpen, setMobileOpen] = React.useState(true);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
Chat UI
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
>
<Drawer
variant={isMobile ? 'persistent' : 'permanent'}
open={isMobile ? mobileOpen : true}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
<Box ref={toolsContainerRef} />
</Drawer>
</Box>
<MainBoxStyled
component="main"
ref={scrollRef}
>
<Chat
thread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
apiRef={apiRef}
scrollerRef={scrollRef}
onChangeCurrentThread={handleDrawerClose}
slots={{
threadsList: ToolsPanelPortal,
}}
slotProps={{
threadsList: {
handleDrawerClose,
containerRef: toolsContainerRef,
},
}}
/>
</MainBoxStyled>
</Box>
);
}
export default App;
Context within custom components
You can embed a component in Chat’s children to give it access to the internal context, which will enhance your application’s capabilities.
In this example, we use a custom AppBar for dynamically displaying headers.
Collapse code
Expand code
const { thread } = useChatContext();
const title = thread?.title || 'Chat UI';
return (
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{title}
</Typography>
</Toolbar>
</AppBar>
);
import * as React from "react";
import {
useAssistantAnswerMock,
Thread, Chat, useChatApiRef, chatClassNames, useChatContext,
} from "@plteam/chat-ui";
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import { styled, useTheme } from "@mui/material/styles";
import Typography from '@mui/material/Typography';
import Drawer from "@mui/material/Drawer";
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 Divider from "@mui/material/Divider";
import Button from '@mui/material/Button';
import AddIcon from '@mui/icons-material/Add';
import { Portal } from '@mui/base/Portal';
import useMediaQuery from "@mui/material/useMediaQuery";
import CloseIcon from '@mui/icons-material/Close';
import moment from 'moment';
const drawerWidth = 240;
const date = moment().utc().format('HH:mm:ss [as of] MMMM D, YYYY');
const threadsDataArray = [
{
id: "test-thread",
title: "Welcome message",
messages: [
{
role: "user",
content: "Hello!",
},
{
role: "assistant",
content: "Hello there! How can I assist you today?",
},
],
},
{
id: "test-thread-1",
title: "Conversation with assistant",
messages: [
{
role: "user",
content: "Hi, how are you?!",
},
{
role: "assistant",
content: "Hi there! I'm here and ready to help. How can I assist you today?",
},
],
},
{
id: "test-thread-2",
title: "User's question",
messages: [
{
role: "user",
content: "What time is it?",
},
{
role: "assistant",
content: `I don't have direct access to your local clock or time zone. However, the system’s timestamp (in UTC) shows ${date}. If you're in a different time zone, you'll need to adjust accordingly. Would you like help converting this to your local time?`,
},
],
},
];
const MainBoxStyled = styled(Box)(({ theme }) => ({
width: `calc(100% - ${drawerWidth}px)`,
height: `calc(100% - 64px)`,
position: 'absolute',
display: 'flex',
top: 64,
left: drawerWidth,
overflow: 'auto',
[theme.breakpoints.down('sm')]: {
left: 0,
width: '100%',
},
[`& .${chatClassNames.threadRoot}`]: {
height: '100%',
},
}));
type ToolsPanelProps = React.PropsWithChildren<{
handleDrawerClose: () => void;
containerRef: React.MutableRefObject<HTMLDivElement | null>;
}>;
const ToolsPanelPortal: React.FC<ToolsPanelProps> = ({ handleDrawerClose, children, containerRef }) => {
const { apiRef } = useChatContext();
const onOpenNew = React.useCallback(() => {
apiRef.current?.openNewThread();
handleDrawerClose();
}, [apiRef.current]);
return (
<Portal
container={() => containerRef.current!}
>
<Toolbar sx={{ justifyContent: 'flex-end' }}>
<IconButton
onClick={handleDrawerClose}
sx={{
display: { sm: 'none' },
}}
>
<CloseIcon />
</IconButton>
</Toolbar>
<Divider />
<Stack
gap={2}
>
<Box px={1.5} mt={2}>
<Button
startIcon={<AddIcon />}
onClick={onOpenNew}
fullWidth
variant="contained"
>
Open new thread
</Button>
</Box>
<Typography sx={{ pl: 1 }} fontWeight={'bold'}>
Threads list
</Typography>
{children}
</Stack>
</Portal>
);
};
const ChatAppBar: React.FC<{ handleDrawerToggle: () => void }> = ({ handleDrawerToggle }) => {
const { thread } = useChatContext();
const title = thread?.title || 'Chat UI';
return (
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{title}
</Typography>
</Toolbar>
</AppBar>
);
};
const App: React.FC = () => {
const [threads] = React.useState<Thread[]>(threadsDataArray);
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const toolsContainerRef = React.useRef<HTMLDivElement | null>(null);
const { onUserMessageSent, handleStopMessageStreaming } =
useAssistantAnswerMock({
delayTimeout: 5000
});
const apiRef = useChatApiRef();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [mobileOpen, setMobileOpen] = React.useState(true);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
return (
<Box sx={{ display: 'flex' }}>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
>
<Drawer
variant={isMobile ? 'persistent' : 'permanent'}
open={isMobile ? mobileOpen : true}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
backgroundColor: '#F1F4F9',
},
}}
>
<Box ref={toolsContainerRef} />
</Drawer>
</Box>
<MainBoxStyled
component="main"
ref={scrollRef}
>
<Chat
thread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
apiRef={apiRef}
scrollerRef={scrollRef}
onChangeCurrentThread={handleDrawerClose}
slots={{
threadsList: ToolsPanelPortal,
}}
slotProps={{
threadsList: {
handleDrawerClose,
containerRef: toolsContainerRef,
},
}}
>
<ChatAppBar handleDrawerToggle={handleDrawerToggle} />
</Chat>
</MainBoxStyled>
</Box>
);
}
export default App;
Custom scroll container
By default, Chat UI 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.
Collapse code
Expand code
const scrollRef = React.useRef<HTMLDivElement | null>(null);
...
<MainBoxStyled
component="main"
ref={scrollRef}
>
<Chat
thread={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">
Chat UI
</Typography>
</Toolbar>
</AppBar>
<MainBoxStyled
component="main"
ref={scrollRef}
>
<Chat
thread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
apiRef={apiRef}
scrollerRef={scrollRef}
/>
</MainBoxStyled>
</Box>
);
}
export default App;
Ready-made template
Chat UI provides a ready-made ChatPage
template that allows you to embed the chat into a new page:
Collapse code
Expand code
<ChatPage
thread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
/>
import * as React from "react";
import {
ChatPage,
useAssistantAnswerMock,
Thread,
} from "@plteam/chat-ui";
import Box from "@mui/material/Box";
const App: React.FC = () => {
const [threads] = React.useState<Thread[]>([
{
id: "test-thread",
title: "Welcome message",
messages: [
{
role: "user",
content: "Hello!",
},
{
role: "assistant",
content: "Hello there! How can I assist you today?",
},
],
},
]);
const { onUserMessageSent, handleStopMessageStreaming } =
useAssistantAnswerMock();
return (
<Box height={"100dvh"} width={"100%"}>
<ChatPage
thread={threads[0]}
threads={threads}
handleStopMessageStreaming={handleStopMessageStreaming}
onUserMessageSent={onUserMessageSent}
/>
</Box>
);
}
export default App;