Building a Web Component-Based Chatbot with GPT-3.5-turbo

Creating a custom chatbot interface using web components allows for modular and reusable design. In this tutorial, I'll guide you through building a chatbot leveraging OpenAI's powerful GPT-3.5-turbo model. All of the code for this blog post is available here.

Prerequisites

  • Familiarity with JavaScript and the Shadow DOM

  • Basic knowledge of OpenAI's API

Overview

We'll start by defining SVG icons, setting up token limitations, and building the main ChatBot class. The ChatBot class will have helper methods and event listeners to manage chat operations.

1. Defining SVG Icons

Before diving into the main component, we first define SVG elements for our chat and close icons. These icons enhance user interaction:

const chatIcon = `...`
const closeIcon = `...`

2. Setting Up Token Limitations

Next, for effective management of tokens and API calls, we'll define some constants:

const TOKEN_LIMIT = 1000
const SPECIAL_TOKEN_BUFFER = 10
  • TOKEN_LIMIT: The maximum number of tokens we want to handle.

  • SPECIAL_TOKEN_BUFFER: A small buffer to account for any special tokens.

3. Building the ChatBot Class

Our main component is the ChatBot class. It manages the chat's state, UI, and interactions:

class ChatBot extends HTMLElement {
    ...
}

3.1. Initializing Component State

Inside the constructor, we initialize our chatbot's state. This includes the chat window's visibility (isOpen), the chat messages (messages), the chatbot's thinking status, and more.

constructor() { ... }

3.2. Styling the Component

The get styles() method returns a template literal containing the CSS to style our chatbot:

get styles() { ... }

3.3. Estimating Tokens

We use a simple method to estimate the number of tokens a message might consume:

estimateTokens(message) { ... }

By using a rough estimation instead of a precise token count, this ChatBot becomes more flexible to integrate with a wider variety of LLMs, since all LLMs' token creation is directly positively correlated to the number of words in a message passed to the given LLM.

3.4. Component Lifecycle Callbacks

connectedCallback and disconnectedCallback handle rendering and cleanup operations when the component gets attached or detached from the DOM:

connectedCallback() { ... }
disconnectedCallback() { ... }

3.5. Chat Operations

Several methods manage chat operations, from toggling the chat window to sending messages and rendering the UI:

  • toggleChat(): Opens or closes the chat window.

  • sendMessage(message): Sends a message and handles the API response.

  • render(): Updates the component's UI.

3.6. Event Handlers

Event listeners and corresponding handlers ensure the chatbot responds to user interactions:

  • handleToggleChatClick(): Toggles the chat window.

  • handleCloseButtonClick(): Closes the chat.

  • handleSendButtonClick(): Handles the send button click.

  • handleInputKeydown(): Sends a message when the Enter key is pressed.

4. Registering the Web Component

Finally, we define and register our web component:

if (!customElements.get("chat-bot")) {
  customElements.define("chat-bot", ChatBot)
}

Example Integration

With our ChatBot defined, here's a simple HTML file demonstrating how to use it, assuming ChatBot.js is located next to this html file, inside the public folder:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat Bot Example</title>
  </head>
  <body>
    <chat-bot></chat-bot>
    <script type="module">
      import "./ChatBot.js"
    </script>
  </body>
</html>

And a simple Express server to illustrate what a backend could look like:

import express, { Request, Response } from "express"
import OpenAI from "openai"
import dotenv from "dotenv"
dotenv.config() // Load environment variables from .env file

const app = express()
app.use(express.json())

const OPENAI_API_KEY = process.env.OPENAI_API_KEY

const openai = new OpenAI({
  apiKey: OPENAI_API_KEY,
})

app.use(express.static("public"))

app.post("/api/chat", async (req: Request, res: Response) => {
  try {
    const gptResponse = await openai.chat.completions.create({
      // OpenAI offers more models, and choosing a different model is as easy as
      // changing this string specifier
      model: "gpt-3.5-turbo",
      messages: req.body.messages,
      // example number of tokens, you can play with this number for your needs.
      max_tokens: 150,
    })

    if (gptResponse && gptResponse.choices && gptResponse.choices.length > 0) {
      res.status(200).json({ content: gptResponse.choices[0].message.content })
    } else {
      res
        .status(500)
        .json({ content: "Failed to get a response from the model." })
    }
  } catch (error) {
    console.error("Error calling OpenAI:", error)
    res.status(500).json({ content: "Internal server error." })
  }
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`)
})

Conclusion

With this structure, we've built a modular chatbot component using web components. This chatbot is easily integrated into any web application and offers a customizable interface to interact with the GPT-3.5-turbo model or any LLM you have available through an API. The full code of this blog post is available at my github repository.