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

  1. Create a MUI/React Portal.
  2. Pass the created portal into the chat’s slots (threadsList). The dialog list will be placed as children.
  3. 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;