DEV Community

Cover image for Gen AI for JavaScript Devs: Building a Pizza Chatbot with Node.js – Part 1
Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Gen AI for JavaScript Devs: Building a Pizza Chatbot with Node.js – Part 1

Introduction

In our ongoing series, we've explored the OpenAI SDK and dived into various LLMs (Large Language Models) and their SDKs. In the previous post we've covered prompting techniques gaining a solid foundation in generative AI. Now, it's time to put all that learning into practice.

In this post, we’ll build a simple yet effective pizza chatbot using the skills we've acquired. From utilizing LLM SDKs like OpenAI to applying advanced prompting techniques, we’ll bring all these elements together to create something tangible. While this chatbot won't have database connectivity—something we can add later—it serves as a great starting point to showcase the power of AI.

The inspiration for this chatbot comes from a course I previously mentioned, offered by DeepLearning.AI. The course demonstrated these concepts in a Jupyter Notebook, and I highly recommend it for anyone interested in prompting techniques. In this post, we’ll take that foundational knowledge and transform it into a fully-fledged chatbot.

Setting Up the Server

In this section, we'll set up a simple Node.js application using Express, with one endpoint for basic text generation with OpenAI. Open your terminal and run the following commands to initialize your Node.js app and install the necessary dependencies:

mkdir pizza-chatbot
cd pizza-chatbot
npm init -y
npm install express openai dotenv
Enter fullscreen mode Exit fullscreen mode

Next, create a file named server.js and set up a basic Express server. Here's the code:

require("dotenv/config");
const express = require("express");
const { OpenAI } = require("openai");
const path = require("path");

const app = express();
const port = 3000;

const openai = new OpenAI({
  apiKey: process.env.OPENAI_PROJECT_KEY,
});

app.use(express.static(path.join(__dirname, "public")));
app.use(express.json());

app.get("/", (req, res) => {
  return res.sendFile(path.join(__dirname, "public", "index.html"));
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode
  • app.use(express.static(...)) serves static files like HTML, CSS, and JavaScript from the public directory.
  • app.use(express.json()) allows us to parse incoming JSON requests.
  • We create an OpenAI instance using the API key stored in our .env file.
  • The app.get("/") route serves our index.html file when a user visits the root URL (/) of our application.
  • The server listens on port 3000, and when it's running, you can access the app at http://localhost:3000.

Implementing the Chat Endpoint

Let's create the chat endpoint, which will handle conversations between our pizza chatbot and the user.

app.post("/api/chat", async (req, res) => {
  const conversations = req.body.conversations;

  const response = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [...context, ...conversations],
  });

  const assistantMessage = response.choices[0].message.content;

  return res.json({ message: assistantMessage });
});
Enter fullscreen mode Exit fullscreen mode
  • The endpoint receives an array of previous conversations from the frontend via the conversations variable. This array holds the back-and-forth messages between the user and the chatbot.
  • We use OpenAI's chat.completions.create method to generate a response. The method takes in a list of messages, which includes the predefined context (the chatbot's system instructions) and the ongoing conversations.

Managing Conversations with Context
LLMs (Large Language Models) like GPT-4 are stateless, meaning they don’t remember previous interactions. To keep the conversation flowing, we need to manage the context ourselves by sending the history of the conversation with each new request. There are many advanced techniques to manage context, such as:

  • Using Vector Databases: To store and retrieve relevant conversation history efficiently.
  • Summarizing Previous Conversations: To avoid exceeding token limits by summarizing older parts of the conversation.
  • Reducing Context Length: By trimming unnecessary details from previous interactions.

These techniques can help in more complex scenarios, but for now, we’re keeping it simple.

Crafting the System Prompt

The system prompt is a crucial part of our chatbot’s functionality. Here’s the prompt we’re using:

const context = [
  {
    role: "system",
    content: `
      You are OrderBot, an automated service designed to take orders for a pizza restaurant. Start by greeting the customer warmly. Then, proceed to collect their order, ensuring you ask for all the details like toppings, sizes, and extras to accurately identify each item on the menu.
      Once the order is complete, ask whether the customer prefers pickup or delivery. If they choose delivery, be sure to collect the delivery address. Summarize the entire order afterward and confirm if the customer wants to add anything else.
      Finally, collect the payment details. Throughout the conversation, keep your responses short, friendly, and conversational to ensure a smooth ordering experience.
      The menu includes:
      Pepperoni Pizza: $12.95 (Large), $10.00 (Medium), $7.00 (Small)
      Cheese Pizza: $10.95 (Large), $9.25 (Medium), $6.50 (Small)
      Eggplant Pizza: $11.95 (Large), $9.75 (Medium), $6.75 (Small)
      Fries: $4.50 (Large), $3.50 (Small)
      Greek Salad: $7.25
      Toppings:
      Extra Cheese: $2.00
      Mushrooms: $1.50
      Sausage: $3.00
      Canadian Bacon: $3.50
      AI Sauce: $1.50
      Peppers: $1.00
      Drinks:
      Coke: $3.00 (Large), $2.00 (Medium), $1.00 (Small)
      Sprite: $3.00 (Large), $2.00 (Medium), $1.00 (Small)
      Bottled Water: $5.00
    `,
  },
];
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Prompt:

  1. Role as a System: The prompt begins by establishing the chatbot’s role as "OrderBot," which sets the expectations for how it should behave and respond.
  2. Guided Interaction: It instructs the chatbot to greet the customer warmly, take their order, and ask specific questions about the order details, such as toppings and sizes.
  3. Customer Experience: The prompt emphasizes a friendly and conversational tone, ensuring that the chatbot’s responses are not only accurate but also pleasant for the user.
  4. Menu Details: The entire menu is included in the prompt, so the chatbot has all the necessary information to assist the customer in real-time.
  5. Handling the Order Process: The prompt outlines the steps to collect the order, summarize it, ask for pickup or delivery options, and finally, request payment details.

Prompting Technique:

  • Role-based Prompting: This technique clearly defines the chatbot’s role and tasks, guiding it to perform a specific function—taking pizza orders in this case.
  • Contextual Prompting: By including the entire menu and detailed instructions, the prompt provides a rich context for the chatbot, enabling it to generate more accurate and relevant responses.

Designing the Frontend

Now that we’ve set up the backend for our chat application, let’s move on to creating the frontend. We’ll be building a simple chat interface using HTML, CSS, and JavaScript. First, create a public folder in your project directory. Inside this folder, create two separate files: index.html and styles.css. In the styles.css file, add the following CSS code:

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: #f0f0f0;
}

#chat {
  width: 80%;
  height: 90%;
  background: #ffffff;
  position: relative;
  padding: 1.25rem;
  border-radius: 0.5rem;
  box-shadow: 0 0 0.625rem rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
}

#chat-area {
  flex: 1;
  overflow-y: auto;
  padding: 2rem;
  padding-bottom: 3.5rem;
}

.chat-message {
  margin: 0.625rem 0;
}

.user {
  text-align: right;
}

.assistant {
  text-align: left;
  background-color: #f6f6f6;
  padding: 0.625rem;
  border-radius: 0.3125rem;
}

#message-input {
  flex: 1;
  padding: 0.625rem;
  border: 0.0625rem solid #ccc;
  border-radius: 0.5rem;
}

#send-button {
  padding: 0.625rem 1.25rem;
  border: none;
  background-color: #007bff;
  color: #ffffff;
  border-radius: 0.5rem;
  cursor: pointer;
}

#chat-form {
  position: absolute;
  bottom: 1.25rem;
  left: 1.25rem;
  right: 1.25rem;
  display: flex;
  gap: 1rem;
  padding-right: 2rem;
  padding-left: 2rem;
  align-items: center;
  background: #ffffff;
}
Enter fullscreen mode Exit fullscreen mode

In the index.html file, paste the following code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Chat</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="chat">
      <div id="chat-area">

      </div>
      <form id="chat-form">
        <input
          type="text"
          id="message-input"
          placeholder="Enter your message here..."
          required
        />
        <button type="submit" id="send-button">Send</button>
      </form>
    </div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Run node server.js from your terminal and open your browser and go to http://localhost:3000 to view the chat interface.

Handling Chat Actions

Now that we have our chat interface set up, let's make it functional by sending the user's messages to the server and displaying the response from our chatbot. We’ll achieve this using JavaScript to handle the form submission and update the chat UI.

To start, we’ll add a <script> tag at the end of our index.html file and include the following JavaScript code:

<script>
  document.addEventListener("DOMContentLoaded", () => {
    const conversations = [];

    // Send an initial message when the page loads
    const initialMessage =
      "Hi there! 😊 Ready to place your pizza order 🍕 or have any questions? Let me know!";
    addMessageToChat("Assistant", initialMessage, "assistant");

    // Set up the form submit event listener
    document
      .getElementById("chat-form")
      .addEventListener("submit", async (event) => {
        event.preventDefault(); // Prevent the default form submission

        const userMessage = document.getElementById("message-input").value;
        if (!userMessage) return; // Do nothing if the input is empty

        // Add user message to chat
        addMessageToChat("User", userMessage, "user");

        // Add the user's message to the conversation
        conversations.push({ role: "user", content: userMessage });

        // Send the user message to the server
        const response = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ conversations }),
        });

        const data = await response.json();
        const assistantMessage = data.message;

        // Add assistant's response to chat
        addMessageToChat("Assistant", assistantMessage, "assistant");

        // Update the conversation with the assistant's message
        conversations.push({ role: "assistant", content: assistantMessage });

        // Clear the input field
        document.getElementById("message-input").value = "";
      });
  });

  // Function to add messages to the chat
  function addMessageToChat(sender, message, className) {
    const chat = document.getElementById("chat-area");
    const messageElement = document.createElement("div");
    messageElement.className = `chat-message ${className}`;
    messageElement.innerHTML = `<strong>${sender}:</strong> <span class="message-content">${message}</span>`;
    chat.appendChild(messageElement);
    chat.scrollTop = chat.scrollHeight;
    return messageElement.querySelector(".message-content"); // Return the span where the message content is displayed
  }
</script>
Enter fullscreen mode Exit fullscreen mode
  • We are adding a dummy initial message from the assistant (chatbot) when the page loads to greet the user.
  • We are using the fetch API to send the conversation to the server. Once the server responds, we display the assistant's message in the chat and update the conversation array.
  • The addMessageToChat function takes care of adding both user and assistant messages to the chat UI. It dynamically creates a new div element for each message, appends it to the chat area, and scrolls to the latest message.

Testing the Chat Application

  • Ensure your server is running by executing node server.js in your terminal.
  • Open your browser and go to http://localhost:3000. You should see the initial greeting message. Type a message in the input field and click "Send" to see the chat in action.
  • Finish your chat with the bot by ordering a pizza and check if it follows your instructions correctly.

Conclusion

In this post, we built a basic pizza chatbot using Node.js, Express, and OpenAI. In the next post we will be:

  1. Adding Streaming Responses
    Currently, our chatbot sends a response all at once, which might not provide the best user experience. In a real-world application, streaming responses—where the chatbot sends responses incrementally—can make interactions feel more natural like the ChatGPT Interface. We'll implement streaming in an API and handle it on the frontend for a smoother chat experience.

  2. Handling Markdown Responses
    Our chatbot’s responses include markdown formatting, which can be less user-friendly. Instead of directly sending markdown, we could parse it into HTML on the frontend. This approach will make the responses more visually appealing and easier to read.

  3. Cost and Model Considerations
    As discussed in our previous posts, the choice of language model (LLM) can impact both cost and performance. While the gpt-4o-mini model is quite powerful, it might be more than necessary for a simple chatbot. We’ll explore alternatives like Mistral or other cost-effective models, which can help reduce both inference time and expenses.

Until then, keep coding 😊.

Top comments (0)