avatarMatija Žiberna

Summary

The provided content is a comprehensive guide on building an AI chatbot using OpenAI's Assistant API and deploying it on a website with Next.js 14.

Abstract

This guide outlines the process of creating an intelligent AI chatbot that can be embedded on any website for free. It details how to utilize OpenAI's latest Assistant API with Next.js 14 to build a chatbot that can remember conversations, learn from data, and be extended with additional knowledge. The tutorial covers setting up the development environment, coding the front-end and back-end components, integrating with OpenAI's services, and finally, testing and embedding the chatbot on a website using an iframe. It emphasizes the ease of development due to advancements in the Assistant API and the cost-effectiveness of the implementation. The guide also touches on managing the chatbot through OpenAI's dashboard and the potential for further customization.

Opinions

  • The author conveys that building a chatbot has become significantly easier and more efficient with the new Assistant API and Next.js 14.
  • There is an opinion that the advancements in AI and web development tools have made complex chatbot functionalities, such as context retention and knowledge retrieval, more accessible to developers.
  • The guide suggests that using OpenAI's chatbot technology can provide a more effective solution than previously possible, given the same amount of development time.
  • The author expresses the importance of efficient ID management and context persistence for maintaining conversation continuity in the chatbot.
  • There is a positive view on the use of local storage and context providers in Next.js to manage chatbot state and data across sessions.
  • The guide promotes the use of SWR for data fetching due to its performance and ease of use, particularly in the context of real-time applications like chatbots.
  • The author emphasizes the user-friendliness of OpenAI's dashboard for managing and editing chatbot assistants, suggesting it enhances the developer experience.
  • The tutorial concludes with the ease of embedding the chatbot on any website, highlighting the flexibility and reach of the developed solution.

How to Build AI Chatbot In 30 Minutes With OpenAI & Next.js 14

Create an AI Chatbot with Any Knowledge and Embed It on Any Website for Free.

Building an AI chatbot in under 30 minutes that remembers conversations and learns from your data? And one that can be embedded on any site? That’s our project today.

This tutorial guides you step-by-step in building an intelligent free ai chatbot.

We’re utilizing OpenAI’s latest Assistant API.

By the end of this tutorial, you’ll know how to create a chatbot from scratch, extend its knowledge base and embedd it to any site. Additionally, you’ll learn to add, modify, or remove knowledge. And you’ll be able to customize how it interacts with customers.

If you’re interested in building your own chatbot, or just curious about the latest Assistant API, you’re going to enjoy this. It’s a great way to see what modern chatbot technology can do.

You can view the live working demo here.

Chatbot built with this tutorial

This used to be a lot more difficult

Over 91 hours.

That’s the amount of time I spent building a chatbot this summer.

The sad part? Despite the effort, it wasn’t nearly as effective as the chatbot we are about to build in less than 30 minutes today.

Before the latest OpenAI Developer Days, creating a chatbot involved navigating through a maze of complexities.

Tasks like managing chat history, ensuring context windows remained manageable, conducting vector searches for knowledge retrieval, understanding embedding, learning about language chains and vector databases, and implementing PDF loaders were just the tip of the iceberg.

It’s a lot easier now

Thanks to the advancements in the new Assistant API, enhanced threading capabilities, and a suite of sophisticated tools, building a chatbot has become almost effortlessly simple.

A foundational knowledge of JavaScript and NextJs will be beneficial for a smooth follow-along experience. So, let’s dive in and start this exciting journey of chatbot creation!

Let’s dive in!

Requirements

This implementation is remarkably lightweight and cost-effective. The primary expense you'll encounter are the API calls to OpenAI. However, to get started, you'll need to set up accounts with a few services.

Essential Accounts and Tools:

  • An OpenAI Developer Account.
  • A Vercel Account for serverless infrastructure to host your NextJS application.
  • A GitHub Account to manage your code.
  • NextJS version 14 or higher.

Set-up NextJs

We will be using Next.js a JavaScript framework based on React. It will handle the application’s front-end and back-end logic.

If you would like to read more about NextJs click here.

Begin by creating a new project using Next.js’s CLI. This step will generate a dedicated folder named after the app you choose.

npx create-next-app@latest

We’re going to utilize Tailwind CSS for our styling needs. Alongside Tailwind CSS, we will be incorporating the latest App Router.

While ESLint is a widely used tool for maintaining code quality, I recommend avoiding its installation for this particular project.

Next’s CLI config options

Settings the environmental variables

In your project’s parent directory, open the .env file. If it doesn't already exist, create a new one. Once opened or created, paste the following variable into the file:

OPENAI_API_KEY=YOUR_OPEN_API_KEY
NEXT_PUBLIC_CHATBOT_NAME=ChattyMan // Name of your chat displayed in UI
NEXT_PUBLIC_SERVER_ADDRESS=http://localhost:3000

You can get the API keys here (you need a developer account).

Frontend

Now that we’ve set up the essential files, it’s time to delve into the actual coding. Let’s begin by focusing on setting up the front end of our application.

Configuring initial page

To start, run the npm run dev command in your terminal. This will spin up your Next.js application. Once it's running, navigate to localhost:3000 in your browser. You should be greeted with the default starting screen.

Editing the Home Page in Next.js

The content you see above is located in src/page.tsx. Feel free to edit this file to observe real-time changes.

Important Convention Note: In Next.js 13.5 and above, src/page.tsx corresponds to the root of your website (i.e., https://yourwebsite.com). This is a crucial convention to follow in this version of Next.js.

Setting Up the Chatbot’s Home

The src/page.tsx file, which acts as the home page, initially contains placeholder content. Replace the existing boilerplate with the following components, which we'll create shortly.

For now, simply paste the code and don't worry about any import errors.

// src/page.tsx
"use client";
// import ChatHeader from "./_components/ChatHeader";
// import MessageWindows from "./_components/MessageWindow";
// import InputArea from "./_components/InputArea";
import { useEffect } from "react";
import { mutate } from "swr";

export default function Home() {
  
  return (
    <main
      id="chatbot-openai"
      className="w-screen h-screen bg-white shadow-lg overflow-hidden flex flex-col"
    >
      {/* <ChatHeader></ChatHeader>
      <MessageWindows></MessageWindows>
      {isLoading && <LoadingSpinner></LoadingSpinner>}
      <InputArea></InputArea> */}
    </main>
  );
}

const LoadingSpinner = () => {
  return (
    <div className="flex justify-center items-center">
      <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900"></div>
    </div>
  );
};

Don’t worry if you’re not familiar with the components ChatHeader, MessageWindow, and InputArea yet. For the moment, these are commented out in the code since they don't exist. We'll create them soon.

Creating the Components Folder

In the app folder, create a new folder named _components. This will be the dedicated space for all the components that make up our chatbot. It's here that we'll create the components we just mentioned.

Understanding the Underscore Convention

Notice the leading underscore in _components. This isn't just a naming choice; it's a meaningful convention in Next.js. The underscore signifies to Next.js that this is a private folder and its contents should not be directly accessible to visitors.

You can learn more about this convention and its importance in Next.js's documentation on routing and colocation: Next.js Routing Documentation.

Folder structure for components

Component #1 — ChatHeader

Let’s begin our component creation process by starting with ChatHeader.

This component will play a key role in defining the visual aspects of our chatbot. It will be responsible for displaying the bot's icon and name.

import Image from "next/image";
import React from "react";
import BotImage from "@/assets/chatbot-icon.svg";

function ChatHeader() {
  return (
    <div className="sticky top-0 w-full bg-white border-b border-gray-200">
      <div className="flex justify-between py-1 mb-4">
        <div className="flex items-center  px-2 gap-2 pt-2">
          <Image className="w-10 h-10" src={BotImage} alt="Chatbot"></Image>
          <span className="text-lg font-bold text-zinc-700">
            {process.env.NEXT_PUBLIC_CHATBOT_NAME}
          </span>
        </div>
      </div>
    </div>
  );
}

export default ChatHeader;

You can personalize the chatbot’s icon by adding your chosen image to src/assets/image_name. Simply import this image into the ChatHeader component. Remember, the image file should be placed in the src/assets folder for easy access and organization.

To customize the chatbot’s name, you can use an environmental variable named NEXT_PUBLIC_CHATBOT_NAME. This variable should be edited in the .env file we defined earlier.

Component #2 — Message

Next, we’ll focus on the MessageWindow component. This component is crucial as it handles the display of both user and bot messages.

To start, navigate to src/app/_components in your project directory. Here, create a new file named MessageWindow.tsx and paste the following code into it:

// src/app/_component/MessageWindows.tsx
import React from "react";

function MessageWindows() {
  const sortedMessagesByDate = []
  return (
    <div className="flex-grow overflow-y-auto p-4">
      <Messages messages={sortedMessagesByDate}></Messages>
    </div>
  );
}

export default MessageWindows;

The MessageWindow component, which we just set up, will eventually be used to fetch the current messages. These messages will then be passed down to another key component: Messages.

Creating the Messages Component: For now, let’s focus on creating the Messages.tsx file. Navigate to the same directory where you created MessageWindow.tsx and create a new file named Messages.tsx. Then, paste the following code into it:

// src/app/_components/Messages.tsx
import { ThreadMessage } from "openai/resources/beta/threads/messages/messages.mjs";
import React, { useEffect, useRef } from "react";
import Message from "./Message";

function Messages({ messages }: { messages: ThreadMessage[] }) {
  const lastMessageRef = useRef<any>(null);

  // Scrolls to the last message whenever the messages update
  useEffect(() => {
    if (lastMessageRef.current) {
      lastMessageRef.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages]);

  return (
    <div>
      {messages.map((m, index) => (
        <Message
          key={index}
          message={m}
          ref={index === messages.length - 1 ? lastMessageRef : null}
        />
      ))}
    </div>
  );
}

export default Messages;

The useEffect function, as shown above, plays a crucial role. It ensures that whenever the message window loads, it automatically scrolls to display the latest message.

Finally, to complete the messaging functionality, let’s create the Message.tsx file. This component is responsible for displaying individual messages, both from the user and the bot. Navigate to the same directory and create a new file named Message.tsx. Then, paste the following code into this file:

// src/app/_components/Message.tsx
import { ThreadMessage } from "openai/resources/beta/threads/messages/messages.mjs";
import React from "react";

const Message = React.forwardRef(
  ({ message }: { message: ThreadMessage }, ref) => (
    <div
      ref={ref}
      className={`clear-both relative overflow-hidden ${
        message.role === "assistant" ? "float-left" : "float-right"
      }`}
    >
      <div
        className={`rounded-lg py-3 px-4 mb-3 ${
          message.role === "assistant"
            ? "bg-gray-200 text-black"
            : "bg-blue-500 text-white"
        }`}
      >
        <p>{message.content[0]?.text?.value)</p>
      </div>
    </div>
  )
);

export default Message;

Component #3— Input Area

A chatbot without a means for user input isn’t very useful. So, before we dive into the back-end logic, let’s create the final front-end component: InputArea.tsx.

This component will be where users type their messages to interact with the chatbot.

React Hook Form

To handle message processing efficiently, we’ll be using react-hook-form. This library is known for its performance, flexibility, and easy-to-use validation capabilities, making it an ideal choice for form handling in React applications.

First, install react-hook-form using the following command:

npm install react-hook-form

Once the installation is complete, create the InputArea.tsx file and paste the following code into it. Although this code is a bit longer, it's quite straightforward and self-explanatory.

// src/app/_components/InputArea.tsx
import React from "react";
import { useForm } from "react-hook-form";
import Link from "next/link";
import { ThreadMessagesPage } from "openai/resources/beta/threads/messages/messages.mjs";

interface FormFields {
  question: string;
}

function InputArea() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormFields>();

  const onSubmit = async (data: FormFields) => {
     // To add logic
  };

  return (
    <div className="sticky bottom-0 bg-white p-2">
      <form
        onSubmit={handleSubmit(onSubmit)}
        className="flex items-center border rounded"
      >
        <input
          className="flex-grow p-2 border-0"
          {...register("question", { required: true })}
          placeholder="Type your message"
        />
        {errors.question && (
          <span className="text-red-500">This field is required</span>
        )}
        <button
          disabled={isLoading ? true : false}
          type="submit"
          className="p-2 text-white bg-blue-500 rounded disabled:bg-gray-200"
        >
          <svg
            width="16"
            height="16"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              fill-rule="evenodd"
              clip-rule="evenodd"
              d="M4.394 14.7L13.75 9.3c1-.577 1-2.02 0-2.598L4.394 1.299a1.5 1.5 0 00-2.25 1.3v3.438l4.059 1.088c.494.132.494.833 0 .966l-4.06 1.087v4.224a1.5 1.5 0 002.25 1.299z"
              fill="#fff"
            ></path>
          </svg>
        </button>
      </form>
      <p className="text-center text-xs py-2 text-gray-700">
        Powered By{" "}
        <Link
          target="_blank"
          className="font-semibold"
          href="https://www.we-hate-copy-pasting.com/"
        >
          WHCP
        </Link>
      </p>
    </div>
  );
}

export default InputArea;

The onSubmit function is a critical part of our chatbot. It's designed to send messages from the user interface to the server, and then to the backend.

For now, it’s just a placeholder function.

With the front end now almost fully prepared, it’s time to head to the backend.

Take a quick break if you want. It’s important to return refreshed and ready for the next steps in our development journey.

Backend

As we transition to back-end development, you’ll start to appreciate the ease provided by the Assistant API. Much of the hard work is already behind us, thanks to this powerful tool.

Our immediate goal is to set up a few key API endpoints for:

  1. Fetching all messages (thread).
  2. Posting new messages from a user (message).
  3. Checking the completion status of a task (run).

The first two tasks are straightforward, but the third involves a bit more complexity. It’s essentially about determining whether OpenAI has finished processing our request.

Not An Ideal Implementation

Currently, after sending a message to OpenAI, there’s a wait time before receiving a response.

This is similar to the ongoing text stream in ChatGPT. According to OpenAI’s keynote, a more direct method via API is in development, but for now, we’re left with periodically checking the task’s status.

The run endpoint will indicate whether the task is in progress or completed.

You can learn more about this process in the OpenAI Platform Documentation.

How to create API endpoints in NextJs

To create an API route in Next.js 13.5+, you can start by creating a folder within src/api, like src/api/hello, and then create a file named route.ts within.

This is a key convention in Next.js: only the code in route.ts is considered relevant for the API. In this file, you can define handlers for each HTTP method. For example...

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  return NextResponse.json({
      world:"hello"
  });
}

export async function GET(req: NextRequest) {
  return NextResponse.json({
      hello:"World"
  });
}

// Others methods like PUT, DELETE, OPTIONS

Endpoint #1 — Messages

Let’s proceed by setting up the API endpoint. Start by creating a new file within your project: src/app/api/messages/route.ts. This file will correspond to the endpoint https://yourwebsite.com/api/messages.

As we go through the code, you’ll notice imports from @/lib/openai. This library includes functions essential for communicating with OpenAI services, allowing our chatbot to interact with the OpenAI API effectively.

Now, let’s begin by pasting the following code into route.ts:

// src/app/api/messages/route.ts
import { getMessagesForThread, createThreadAndRun } from "@/lib/openai";
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  const searchParams = req.nextUrl.searchParams;
  const threadId = searchParams.get("threadId");
  if (!threadId) {
    return NextResponse.json({ noMessages: true });
  }
  const messages = await getMessagesForThread(threadId);
  return NextResponse.json(messages);
}

export async function POST(req: NextRequest) {
  const data = await req.json();
  const { question, threadId } = data;
  const { run, thread } = await createThreadAndRun(threadId, question);
  const runId = run.id;
  const newThreadId = thread.id;
  return NextResponse.json({ runId, threadId: newThreadId });
}

The GET function in our API is designated for fetching all messages associated with a specific thread or conversation. This is achieved through the getMessagesForThread function.

When a request is made to https://yourwebsite.com/api/messages, it needs to include a threadId parameter, like this: https://yourwebsite.com/api/messages?threadId=XXXXXXXX.

Each browser session will generate a unique thread ID. This approach allows us to maintain a separate conversation history for each user (or browser session). In essence, every user has their own individual conversation thread with the bot.

The POST request within our API is responsible for initiating a new conversation whenever a new user connects. Think of this as creating a new conversation thread for each new user interaction.

Now, our next step is to develop the getMessagesForThread and other necessary functions to make our GET and POST requests operational.

Endpoint #1 — Status

Before we move on to configuring OpenAI, we need to set up another endpoint, api/status. This endpoint will be used to check whether a certain task run has been completed.

Create a new folder under the API directory named api/status, and within it, create a route.ts file.

Paste the following code into it.

import { statusOfRun } from "@/lib/openai";
import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  const searchParams = req.nextUrl.searchParams;
  const threadId = searchParams.get("threadId");
  const runId = searchParams.get("runId");
  if (!threadId || !runId) throw Error("No threadId or runId");
  const status = await statusOfRun(threadId, runId);
  return NextResponse.json({ status });
}

Create OpenaAI interface

It’s time to set up a code interface that allows our Next.js app to communicate with OpenAI’s servers via their API. For this, create a new folder named lib under src. Inside this lib folder, create a file named openai.ts. This file will centralize all the code related to interactions with OpenAI.

Create assistant

The assistant, essentially our chatbot, is a crucial component. We define its behaviour, impart knowledge to it, and it’s essential that this assistant be reused across all conversations for consistency.

That’s why we need to create it and store its configuration, allowing us to retrieve and utilize it in ongoing conversations.

In openai.ts, you'll specify the assistant's instructions, name, the tools it should use, and the model it will operate on.

// src/lib/openai.ts
import OpenAI from "openai";
import fs from "fs";
const secretKey = process.env.OPENAI_API_KEY;

export const openai = new OpenAI({
  apiKey: secretKey,
});

// Database for storing assistant ID.
const assistantIdFilePath = "assistantId.txt";

export const createAssistant = async () => {
  // Try to read the assistant ID from the file
  let assistantId;
  try {
    assistantId = fs.readFileSync(assistantIdFilePath, "utf8");
  } catch (error) {
    console.log("Assistant ID file not found, creating a new assistant");
  }

  if (!assistantId) {
    const assistantInstance = await openai.beta.assistants.create({
      instructions:
        "You are customer support agent trying to help the customer",
      name: "Customer Support Bot",
      tools: [{ type: "retrieval" }],
      model: "gpt-3.5-turbo-1106",
    });

    // Save the assistant ID to a file
    fs.writeFileSync(assistantIdFilePath, assistantInstance.id);

    return assistantInstance;
  } else {
    // If the assistant ID is already stored, return the ID
    return { id: assistantId };
  }
};

Our first step involves initializing OpenAI within our application.

Next, we specify a file name where the assistant’s ID will be stored. This step is crucial as it helps in persistently identifying and accessing our assistant across different sessions and interactions.

The createAssistant function in the provided JavaScript code manages the creation and retrieval of an OpenAI assistant.

  1. Check Existing Assistant ID: The function attempts to read an assistant ID from a local file (assistantId.txt). If this file exists and contains an ID, it skips creating a new assistant.
  2. Create New Assistant: If the file doesn’t exist or is empty (indicating no previous assistant was created), the function proceeds to create a new assistant using OpenAI’s API.
  3. Save New Assistant ID: After creating a new assistant, its ID is saved to assistantId.txt.
  4. Return Assistant Information: Finally, the function returns the assistant’s instance if it’s new, or just its ID if it was already stored in the file.

This setup allows for efficient use of the OpenAI assistant by reusing the same instance across multiple sessions, reducing the need for creating new assistants each time.

Create a thread & add messages

We need to create three functions responsible for managing threads and messages.

  • createThreadIn the openai.ts file, add the following function. It plays a dual role: if a thread ID is not provided, it will initiate a new conversation thread; if a thread ID is already present, it will continue using the existing thread.
  • getMessagesForThreadAdditionally, include the getMessagesForThread function. This function is dedicated to retrieving all messages from a specific thread, ensuring a continuous and organized chat history.
  • addMessageToThread Alongside this, add another function responsible for adding new messages to a thread. These functions work together to seamlessly manage conversation flows in your chatbot.
// src/lib/openai.ts
...

export const createThread = async (threadId: string | undefined) => {
  try {
    if (threadId) {
      return await openai.beta.threads.retrieve(threadId);
    }
    return await openai.beta.threads.create();
  } catch (error: any) {
    throw error;
  }
};

export const getMessagesForThread = async (threadId: string) => {
  const messages = await openai.beta.threads.messages.list(threadId);
  return messages;
};

export const addMessageToThread = async (
  threadId: string,
  question: string
) => {
  await openai.beta.threads.messages.create(threadId, {
    role: "user",
    content: question,
  });
};

...

Run

In OpenAI’s framework, the ‘run’ function serves a critical role in checking the completion status of a task. Similar to how you experience a brief wait time in ChatGPT after sending a message, the ‘run’ function in our application performs the same check.

A ‘run’ is considered completed once the response from ChatGPT is final and received.

This will enable our chatbot to check for and handle the completion of tasks as needed.

// src/lib/openai.ts
...

export const createRun = async (assistantId: string, threadId: string) => {
  const run = await openai.beta.threads.runs.create(threadId, {
    assistant_id: assistantId,
  });
  return run;
};

export const statusOfRun = async (threadId: string, runId: string) => {
  let runStatus = await openai.beta.threads.runs.retrieve(threadId, runId);
  return runStatus.status;
};

...

Tying everything together on the back end

The last function we’ll add to the openai.ts. This function manages the assistant, handles thread operations, adds messages, and initiates tasks.

It is the primary point of interaction between our front-end and the OpenAI services.

// src/lib/openai.ts
...
export const createThreadAndRun = async (
  threadId: string | undefined,
  question: string
) => {
  const myAssistant = await createAssistant();
  const thread = await createThread(threadId);
  await addMessageToThread(thread.id, question);
  const run = await createRun(myAssistant.id, thread.id);
  return { run, thread };
};
...

Remember, the comprehensive function we added is pivotal in our /api/messages endpoint. With this in place, our back-end setup is now complete.

Frontend — Part II

With our back end and /api/messages endpoint in place, it's time to set up the front end to communicate with these services. This involves both fetching messages from and sending messages to the back end.

First, let’s focus on displaying messages, even if there are none to show yet.

How it works

Our backend relies on several IDs to identify the conversation and participants:

  1. threadId to track the conversation.
  2. assistantId to identify which assistant is speaking.
  3. runId for each specific message, determining its completion status.

We need to effectively manage these variables across our application.

To handle these IDs, we’ll use a combination of local storage and context providers. This approach ensures these variables are stored efficiently and made accessible throughout the application.

Creating Chatbot context

The context we create will encompass all information related to the chatbot. This includes tracking IDs and managing conversation data.

Creating the ChatbotProvider Context

  1. Create a new folder named _contexts within src/app.
  2. Inside this folder, create a file named ChatbotProvider.tsx.
  3. Paste the following code into ChatbotProvider.tsx:
// src/app/_contexts/ChatbotProvider.tsx
import { ReactNode, createContext, useContext, useState } from "react";
import { useLocalStorage } from "usehooks-ts";

const ChatbotContext = createContext<any>(undefined);

export function ChatbotProvider({ children }: { children: ReactNode }) {
  const [threadId, setThreadId] = useLocalStorage("threadId", undefined);
  const [runId, setRunId] = useLocalStorage<string | undefined>(
    "runId",
    undefined
  );
  const [isLoading, setIsLoading] = useState(false);

  const value = {
    isLoading,
    setIsLoading,
    setRunId,
    runId,
    setThreadId,
    threadId,
  };
  return (
    <ChatbotContext.Provider value={value}>{children}</ChatbotContext.Provider>
  );
}

export const useChatbotContext = () => {
  return useContext(ChatbotContext);
};

Please install the package that will handle local storage for us.

npm i usehooks-ts

Our ChatbotProvider is designed to store data in the client's browser, ensuring persistence across sessions. It manages key variables such as threadId, runId, and isLoading.

Now, create a new file:

  1. Go to the src directory.
  2. Create a file named providers.tsx.
"use client";

import { ReactNode } from "react";
import { ChatbotProvider } from "./_contexts/ChatbotProvider";

export function Providers({ children }: { children: ReactNode }) {
  return <ChatbotProvider>{children}</ChatbotProvider>;
}

And lastly, edit src/layout.tsx.

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Chatbot Name",
  description: "Generated by Tutorial",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>{children}</Providers> // Added here
      </body>
    </html>
  );
}

The approach to setting up context in the latest version of Next.js differs from older versions. If you’re curious about these changes and want a deeper understanding, you can refer to my previous article on this topic:

Data-fetching

Before we start retrieving messages on the front-end, we need to install a data-fetching library. We’ll be using SWR, developed by Vercel, for this purpose.

To install SWR, type the following command:

npm i swr

For it to work we need to create fetched create it src/_utils/fetcher.ts

export const fetcher = (url:string) => fetch(url).then(r => r.json())

Revisiting Messages components

It’s time to enhance MessageWindow.tsx by adding the necessary code for handling message retrieval.

  1. Determining the Thread ID: First, we’ll incorporate logic to determine the threadId. This helps us check if there's an ongoing conversation in the current browser session.
  2. Fetching Messages with SWR: We’ll use the SWR library for data fetching. The fetch operation is conditionally executed if there’s an existing conversation.
  3. Making a GET Request: To retrieve messages, we make a GET request to /api/messages?threadId=${threadId}.
import React from "react";
import useSWR from "swr";
import { fetcher } from "../_utils/fetcher";
import { ThreadMessagesPage } from "openai/resources/beta/threads/messages/messages.mjs";

import Messages from "./Messages";
import { useChatbotContext } from "../_contexts/ChatbotProvider";

function MessageWindows() {
  const { threadId } = useChatbotContext();
  const { data: messages, error } = useSWR<ThreadMessagesPage | any>(() => {
    if (threadId) {
      return `${process.env.NEXT_PUBLIC_SERVER_ADDRESS}/api/messages?threadId=${threadId}`;
    }
    return;
  }, fetcher);

  if (error)
    return (
      <div className="p-4 bg-red-500 text-white">Error loading messages...</div>
    );
  if (!messages) return; // It's still loading messages
  if (messages?.noMessages)
    return (
      <p className="bg-red-400 p-4 text-white">
        Sorry there was an error fetching your messages.
      </p>
    );

  const sortedMessagesByDate = messages.data.sort(
    (a: any, b: any) => a.created_at - b.created_at
  );
  return (
    <div className="flex-grow overflow-y-auto p-4">
      <Messages messages={sortedMessagesByDate}></Messages>
    </div>
  );
}

export default MessageWindows;

In MessageWindow.tsx, we'll also address two key aspects:

  1. Basic Error Handling: We’ll implement a simple error-handling mechanism to manage any issues that arise during message retrieval.
  2. Sorting Messages: The function sortedMessagesByDate will be used to reorder the messages chronologically. This ensures that messages appear in a natural, conversation-like sequence in the chatbot interface.

Small formatting upgrade

To improve the presentation of messages in the chatbot interface, let’s create a formatting utility:

  1. Create a new file named formatMessage.tsx in the src/utils directory.
  2. Paste the following code into this file to format the messages for a better user interface experience:
export const formatMessage = (text: string) => {
  const parts = text.split(/(".*?")/);
  return parts.map((part, index) => {
    // Check if the part is within quotes
    if (part.startsWith('"') && part.endsWith('"')) {
      return (
        <span key={index} className="italic">
          {part}
        </span>
      );
    } else {
      return <span key={index}>{part}</span>;
    }
  });
};

Edit Message.tsxto add formatMessage

// src/app/_components/Message.tsx
import { ThreadMessage } from "openai/resources/beta/threads/messages/messages.mjs";
import React from "react";

const Message = React.forwardRef(
  ({ message }: { message: ThreadMessage | any }, ref: any) => (
    <div
      ref={ref}
      className={`clear-both relative overflow-hidden ${
        message.role === "assistant" ? "float-left" : "float-right"
      }`}
    >
      <div
        className={`rounded-lg py-3 px-4 mb-3 ${
          message.role === "assistant"
            ? "bg-gray-200 text-black"
            : "bg-blue-500 text-white"
        }`}
      >
        <p>{formatMessage(message.content[0]?.text?.value)}</p> // Wrap here
      </div>
    </div>
  )
);

export default Message;

With the message formatting in place, we’ve now completed setting up the display of messages in our chatbot.

Next, let’s shift our focus to the logic for sending messages from the chatbot.

InputMessage revisited

Recall the onSubmit function we outlined earlier? It's time to complete it. Replace its current content with the following code:

// src/app/_components/InputArea.tsx
...

function InputArea() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormFields>();

// ADD THIS FUNCTION
 const onSubmit = async (data: FormFields) => {
    mutate(
      `${process.env.NEXT_PUBLIC_SERVER_ADDRESS}/api/messages?threadId=${threadId}`,
      async (currentData: any) => {
        // This is part of an optimistic loading that shows user question immideatly.
        const newMessage = {
          id: "temp-id",
          thread_id: threadId,
          content: [{ text: { value: data.question } }],
          role: "user",
        };
        return {
          ...currentData,
          data: [...(currentData?.data ?? []), newMessage],
        };
      },
      false
    );
    setIsLoading(true);
    reset();

    const config = {
      method: "POST",
      url: `${process.env.NEXT_PUBLIC_SERVER_ADDRESS}/api/messages`,
      data: { ...data, threadId },
    };
    const response = await axios<{ runId: string; threadId: string }>(config);
    setThreadId(response.data.threadId);
    setRunId(response.data.runId);
  };
...

To wrap up our Next.js setup, let’s add the context variables we defined earlier into the same file.

// src/app/_components/InputArea.tsx

function InputArea() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormFields>();

// ADD THIS
const { threadId, setThreadId, setRunId, setIsLoading, isLoading } = useChatbotContext();

// Existing onSubmit function from above
...

Testing the Chatbot

It’s time to test the chatbot and see our work.

If you haven’t already you can start the chatbot with.

npm run dev

Now, visit http://localhost:3000 to see your chatbot in action. Give it a try and start chatting with it!

Editing the Chatbot

OpenAI provides a user-friendly interface for managing your chatbot assistants. Configurations and edits can be made directly through their dashboard.

  1. Accessing the Dashboard: To modify your chatbot, visit the OpenAI Dashboard at OpenAI Assistants.
  2. Locating Your Assistant: Once there, you should see the new assistant listed with the name you’ve defined in your code

By clicking on your assistant in the dashboard, you can access its settings. Here, you have the option to modify various aspects of your chatbot.

Modifications You Can Make:

  • Change its name and description
  • Add context files from which the chatbot can retrieve additional knowledge from.

To test the changes, click on the ‘Test’ button located in the top right corner of the dashboard.

Remember to save your modifications. Changes are typically applied immediately, ensuring your chatbot is up-to-date.

Installing a chatbot on any website

What good is a chatbot if it’s not shared on your website to showcase your achievements?

As mentioned in the beginning, we can use a simple iframe to embed the Next.js application we’ve built into any website.

To include the chatbot, you can easily add a bit of CSS and HTML to the page where you’d like to display the chatbot. In the <head> section, add this stylesheet.

<style>
      /* Chatbot */
      #chat-container {
          position: fixed;
          right: 20px;
          bottom: 20px;
          z-index: 1000;
      }

      #chat-button {
          background-color: #007bff;
          color: white;
          border: none;
          border-radius: 50%;
          width: 60px;
          height: 60px;
          font-size: 30px;
          cursor: pointer;
          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
      }

      #close-button {
          color: black;
          border: none;
          border-radius: 50%;
          width: 30px;
          height: 30px;
          font-size: 20px;
          cursor: pointer;
          display: none; /* Initially hidden */
          position: absolute;
          right: 15px;
          top: 10px;
      }

      #chat-iframe {
          display: none;
          width: 40vh; /* Full width on small screens */
          height: 80vh; /* 80% of the screen height */
          border: none;
          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
          border-radius: 10px;
      }

      @media (min-width: 768px) {
          /* For larger screens, we use fixed dimensions */
          #chat-iframe {
              width: 400px;
              height: 600px;
          }
      }
  </style>

Finally, at the bottom of the body of the page where you’d like to display the bot, paste this final piece of code.

<body>
... rest of HTML
  <div id="chat-container">
      <button id="chat-button">💬</button>
      <iframe id="chat-iframe" src="http://localhost:3000/" frameborder="0"></iframe>
      <button id="close-button">X</button>
  </div>
  
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script>
      $(document).ready(function() {
          $('#chat-button').click(function() {
              $('#chat-iframe').show();
              $('#close-button').show();
              $(this).hide();
          });
  
          $('#close-button').click(function() {
              $('#chat-iframe').hide();
              $('#chat-button').show();
              $(this).hide();
          });
      });
  </script>
</body>

I’m using jQuery to manage the interactivity of the Chatbot. Feel free to adjust it in any way you’d like.

Conclusion

Here it is! The fully functioning chatbot with memory, the ability to retrieve knowledge from files, and full personalization.

To recap, we used the latest Next.js 14 to create the chatbot interface UI and make API calls to OpenAI. We also edited the chatbot settings in the OpenAI dashboard. Then, we embedded that chatbot onto our website with the help of an <iframe>.

Thanks to OpenAI, it has never been simpler to create your very own chatbots.

I hope you enjoyed this tutorial. If you’d like to read more content like this, be sure to follow!

In the next one, I’ll cover publishing the bot into production.

Thanks, Matija

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.
Programming
Web Development
Nextjs
Artificial Intelligence
Recommended from ReadMedium