Constructor

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.

Collapse code
Expand code
<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.

Collapse code
Expand code
<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.

Collapse code
Expand code
  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;