Empowering Website Conversations: Part 6

Introduction

In the preceding section, we crafted a reusable REST API to communicate with our fine-tuned models. Now, to interact with this API effectively, we need a user interface. Our approach involves creating a straightforward UI using React with TypeScript, leveraging Material UI components. The primary objective is to construct an intuitive chat interface that mimics real human interaction.

This task calls for the development of three key components:

  1. TypingIndicator: A chat message window designed to resemble someone typing.
  2. ChatWindow: A comprehensive window housing user and chatbot messages, complete with a text input area and a submission button.
  3. ChatDialog: A popover that transforms into a button adorned with a message icon when closed, revealing the ChatWindow when opened.

The previous section can be found here.

Why React with Typescript

React is a popular choice for building user interfaces due to its component-based architecture and declarative syntax that simplifies development. It promotes reusability, maintains a vast and active community, and offers an extensive ecosystem of libraries and tools. React’s support for server-side rendering and mobile app development, along with performance optimization features and developer tools, makes it a versatile and powerful option for creating efficient and maintainable web and mobile applications. Its widespread adoption in the industry and strong community support further solidify its appeal.

TypeScript is a strongly typed programming language that builds on JavaScript. Using TypeScript with React offers the advantage of adding static typing, improving code quality and catching errors early. It provides robust tooling support, enhancing developer productivity, and offers better documentation through type annotations. This combination makes TypeScript a preferred choice for many, enhancing reliability and maintainability in React development.

Finally, we are also going to make use of Material UI. MUI provides a comprehensive library of pre-built, customizable UI components that follow Google’s Material Design principles. By combining MUI with React and TypeScript, you can accelerate development, maintain code consistency, and ensure type safety throughout your application.

To create a new React project run the following command:

npx create-react-app my-app --template typescript

If you created a React project without Typescript and want to convert it, this blog walks through the required changes.

The important files we will edit or add in our directory tree are the following:

src/
    components/
               ChatWindow.tsx
               ChatDialog.tsx
               TypingIndicator.tsx
    providers/
             ThemeProviders.tsx
    App.tsx
    index.css
    index.tsx

ThemeProvider

To provide a theme for our new React application, we will need to provide a MUI ThemeProvider. MUI components come with default themes that we’ll harness, but we’ll also incorporate inline adjustments within our component implementations. Our customization includes setting the font family to ‘Exo 2’ and making the background transparent. This design choice aligns with our intent for the chatbot to seamlessly overlay another website.

Add the following to src/providers/ThemeProviders.tsx:

import React, { useMemo } from 'react';

import {
  createTheme,
  ThemeProvider as MuiThemeProvider,
} from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';

export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const theme = useMemo(
    () =>
      createTheme({
        typography: {
          fontFamily: ['"Exo 2"', 'sans-serif'].join(','),
        },
        palette: {
          background: {
            default: 'transparent',
          },
        },
      }),
    [],
  );

  return (
    <MuiThemeProvider theme={theme}>
      <CssBaseline enableColorScheme />
      {children}
    </MuiThemeProvider>
  );
}

Make sure to import the Google font in our src/index.css:

@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@300&display=swap');

body {
  margin: 0;
  font-family: 'Exo 2', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
    'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

Now with our theme in place, we can start working on our components.

TypingIndicator

Our goal is to create a chatbot interaction that closely mimics a conversation with a real person. However, the delay in generating responses from the OpenAI API might give users the impression that something is amiss. To address this concern, we can incorporate a simulated typing indicator, providing users with visual feedback that progress is underway.

typing indicator

The following styles the typing indicator in the same way that messages from the bot will be styled in the ChatWindow.

Add the following code to src/components/TypingIndicator.tsx:

import React, { useState, useEffect } from 'react';

export default function TypingIndicator() {
  const [showTypingIndicator, setShowTypingIndicator] = useState(false);
  const typingDotStyle = {
    display: 'inline-block',
    width: '8px',
    height: '8px',
    borderRadius: '50%',
    backgroundColor: '#333', // Adjust the color as desired
    marginRight: '4px', // Adjust the spacing between dots
    animation: '1.2s typing-dot ease-in-out infinite',
  };

  useEffect(() => {
    const typingTimer = setTimeout(() => {
      setShowTypingIndicator(true);
    }, 500); // Adjust the duration as needed

    return () => {
      clearTimeout(typingTimer);
    };
  }, []);

  return (
    <div>
      {showTypingIndicator && (
        <div
          className="chat-bubble"
          style=
        >
          <span className="dot" style={typingDotStyle} />
          <span className="dot" style={typingDotStyle} />
          <span className="dot" style={typingDotStyle} />
        </div>
      )}
    </div>
  );
}

Also add the following to src/index.css:

@keyframes typing-dot {
  15% {
    transform: translateY(-35%);
    opacity: 0.5;
  }
  30% {
    transform: translateY(0%);
    opacity: 1;
  }
}

.dot:nth-of-type(2) {
  animation-delay: 0.15s !important;
}
.dot:nth-of-type(3) {
  animation-delay: 0.25s !important;
}

ChatWindow

The central component of our UI is the ChatWindow. It encompasses crucial elements: a title, user and bot messages, and a text area for submitting new questions.

When a question is submitted and we await a response from the API, the TypingIndicator becomes visible. To ensure a smooth user experience, a useEffect function is employed to auto-scroll to the latest message when added.

Furthermore, we’ve designed flexibility into the chatbot UI by enabling configuration via parameters. This allows for the reuse of the same chatbot UI instance across various scenarios and with different API instances.

Parameters:

  • chat_url: The URL of the chat REST API for communication (Required).
  • greeting: An optional initial message from the bot to display to the user.
  • title: An optional custom title for the ChatWindow.

For example the following comes from the following URL: http://localhost:3000/chatbot-ui?chat_url=http%3A%2F%2F127.0.0.1%3A8000%2Fchat&greeting=Hello!%20I%20am%20a%20ChatBot%20created%20by%20fine-tuning%20OpenAI%20completions%20on%20information%20about%20Embyr.%20How%20can%20I%20help%20you%3F&title=EmbyrBot

chat window

The component will take the prompt submitted in the textarea and submit it against the provided chat_url when the send button is pressed via the handleSubmit function.

Add the following to src/components/ChatWindow.tsx:

import React, { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { Alert, Card, CardHeader, Typography } from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import TypingIndicator from './TypingIndicator';

interface ChatMessage {
  sender: string;
  content: string;
  index: number;
}

type ResponseMessage = {
  data: {
    choice: string;
  };
};

type ErrorMessage = {
  status_code: number;
  message: string;
};

export default function ChatWindow() {
  const location = useLocation();
  const queryParams = new URLSearchParams(location.search);
  const chatUrl = queryParams.get('chat_url') || '';
  const openingMessage =
    queryParams.get('greeting') ||
    'Hello! I am a ChatBot created by fine-tuning OpenAI completions. How can I help you?';
  const title = queryParams.get('title') || 'ChatBot';

  const [messages, setMessages] = useState<ChatMessage[]>([
    { sender: 'Bot', content: openingMessage, index: 0 },
  ]);
  const [input, setInput] = useState('');
  const [errorMsg, setErrorMsg] = useState('');
  const [lastIndex, setLastIndex] = useState(1);
  const [waiting, setWaiting] = useState(false);

  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Scroll to the bottom after adding new content
    if (containerRef.current) {
      containerRef.current.scrollIntoView({ behavior: 'smooth' });
    }
  }, [messages, waiting]);

  if (chatUrl === '') {
    return (
      <Alert severity="error">
        No URL for the chat API (chat_url) has been set
      </Alert>
    );
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (input.trim() === '') return;
    setWaiting(true);

    const newMessage: ChatMessage = {
      sender: 'User',
      content: input,
      index: lastIndex + 1,
    };

    setMessages([...messages, newMessage]);
    setInput('');

    try {
      const response = await fetch(chatUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ prompt: input, temperature: 0.0 }),
      });

      if (!response.ok) {
        const errorMessage: ErrorMessage = await response.json();
        setErrorMsg(errorMessage.message);
        setWaiting(false);
        return;
      }

      const responseData: ResponseMessage = await response.json();
      const newResponse: ChatMessage = {
        sender: 'Bot',
        content: responseData.data.choice,
        index: lastIndex + 2,
      };
      setLastIndex(lastIndex + 2);
      setMessages([...messages, newMessage, newResponse]);
    } catch (error) {
      if (typeof error === 'string') {
        // set error message and disable the box
        setErrorMsg(error);
      } else if (error instanceof Error) {
        // set error message and disable the box
        setErrorMsg(error.message);
      }
    }

    setWaiting(false);
  };

  const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (event.key === 'Enter' && !event.shiftKey) {
      event.preventDefault(); // Prevent default Enter key behavior (e.g., line break)
      handleSubmit(event); // Call your submit function
    }
  };

  return (
    <Card
      sx=
    >
      <CardHeader title={<Typography variant="h5">{title}</Typography>} />
      {errorMsg !== '' && <Alert severity="error">{errorMsg.toString()}</Alert>}
      <div className="chat-window">
        <div
          className="messages"
          style=
        >
          {messages.map((message) => (
            <div
              key={message.index}
              className="message"
              style=
            >
              <div
                className="sender"
                style=
              >
                {message.sender}
              </div>
              <div className="content">{message.content}</div>
            </div>
          ))}
          {waiting && <TypingIndicator />}
          <div
            ref={containerRef}
            style=
          />
        </div>

        <form
          onSubmit={handleSubmit}
          style=
        >
          <div style=>
            <textarea
              id="chat-input"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="Type your message..."
              onKeyDown={handleKeyDown}
              style=
            />
            <button
              type="submit"
              style=
            >
              <SendIcon />
            </button>
          </div>
        </form>
      </div>
    </Card>
  );
}

ChatDialog

The final component in our setup is the ChatDialog, which serves as a floating action button (FAB). When users click this button, it triggers the appearance of a Popover that houses the ChatWindow discussed earlier.

chat dialog button

Add the following to src/components/ChatDialog.tsx:

import React, { useEffect } from 'react';
import { Fab, Popover } from '@mui/material';
import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
import ChatWindow from './ChatWindow';

export default function ChatDialog() {
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
    null,
  );
  const open = Boolean(anchorEl);

  const handlePopoverClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    if (open) {
      setAnchorEl(null);
    } else {
      setAnchorEl(event.currentTarget);
    }
  };

  return (
    <div
      style=
      data-testid="chat-dialog"
    >
      <Fab
        aria-owns={open ? 'chat-dialog-popover' : undefined}
        aria-haspopup="true"
        onClick={handlePopoverClick}
      >
        <ChatBubbleOutlineIcon />
      </Fab>
      <Popover
        id="chat-dialog-popover"
        open={open}
        anchorEl={anchorEl}
        anchorOrigin=
        transformOrigin=
        onClose={handlePopoverClick}
        disableRestoreFocus
      >
        <ChatWindow />
      </Popover>
    </div>
  );
}

Add the ChatDialog to a /chatbot-ui route wrapped in our ThemeProvider to src/App.tsx and update the src/index.tsx to use a BrowserRouter.

import { Routes, Route } from 'react-router-dom';
import ChatDialog from './components/ChatDialog';
import ThemeProvider from './providers/ThemeProvider';

export default function App() {
  return (
    <Routes>
      <Route
        path="/chatbot-ui"
        element={
          <ThemeProvider>
            <ChatDialog />
          </ThemeProvider>
        }
      />
    </Routes>
  );
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement,
);
root.render(
  <React.StrictMode>
    <div>
      <main>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </main>
    </div>
  </React.StrictMode>,
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

You are ready to get chatting!

Conclusion

We’ve successfully crafted a user-friendly chatbot UI, providing our users with seamless access to our REST API and finely-tuned models. Interacting with it should be akin to conversing with a knowledgeable friend well-versed in all things Embyr! In the upcoming and final installment of our series, we’ll delve into integrating this chatbot UI into our existing website, explore essential ethical considerations, and brainstorm potential enhancements for our finely-tuned models. Stay tuned for an exciting conclusion to our journey!

Part 1: What are Chatbots, and why would I want one?

Part 2: From Markdown to Training Data?

Part 3: Fine-tune a Chatbot QA model

Part 4: Safeguard your Chatbot with Discriminators

Part 5: Develop your Chatbot REST API

Part 7: Empowering Website Conversations: Conclusion

Reference

React

Typescript

Material UI

How to use TypeScript with React: A tutorial with examples

How to Set up a React Project with TypeScript

TypeScript - The Best Way to Use It with React