NextJS Course (6/9) – Taking our Chat to the Next Level… with Memory!

Welcome back to part 6 of the tutorial series. In this part, we’ll be leveling up our chat app by making it into a real conversation with memory and making it look a lot better at the same time.

We’ll also be using a library called daisyUI to help us style our chat a lot faster. This is basically a Tailwind CSS plugin that gives you a lot of components out of the box, so you don’t have to reinvent the wheel.

Installing daisyUI

So first let’s install the daisyUI library by running the following command in the terminal (make sure you are in the root of the project):

npm i -D daisyui@latest

Now we need to let Tailwind know that we are using this library. Open the tailwind.config.ts file located in your main project folder (outside of πŸ“ app) and change the settings inside as follows:

// This is all the same as before
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      backgroundImage: {
        "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
        "gradient-conic":
          "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
      },
      // From here on down is new
      colors: {
        'custom-grey':'#2b3440',
      }
    },
  },
  plugins: [
    require('daisyui'),
  ],
  daisyui: {
    darkTheme: "light",
  },
};
export default config;

I’m just going to ignore the gradient-radial and gradient-conic for now even though we’re not using them. I added a custom color to the theme object called custom-grey which we’ll be using later on. Most importantly, I added the daisyui plugin to the plugins array. This is what tells Tailwind to use the Daisy UI library.

I also set the darkTheme to light in the daisyui settings object. What this will do is use the light theme of Daisy UI even if the user has dark mode enabled. You can change this and use your own styling if you want, but for the purposes of this tutorial, I want to make sure everybody sees the same thing.

Go ahead and save your tailwind.config.ts file.

Adding a new menu item

Next we’ll go to our navbar and make a small edit. I want to keep the old version of blockbuster chat in the navbar on a separate page, and just start with a fresh page for this one. So open the navbar.tsx file located in the app folder:

πŸ“ app
    πŸ“ api
        πŸ“ blockbuster
            πŸ“„ route.tsx
    πŸ“ blockbuster_chat
        πŸ“„ page.tsx
    πŸ“ counter
        πŸ“„ page.tsx
    πŸ“„ favicon.ico
    πŸ“„ globals.css
    πŸ“„ layout.tsx
    πŸ“„ navbar.tsx πŸ› οΈ We'll be working on this file
    πŸ“„ page.tsx

You can just leave everything as is except for the NavItems inside of the Navbar component. Change the NavItems to look like this:

const Navbar = () => {
  return (
    <nav className="bg-emerald-700 text-white p-4">
      <ul className="flex justify-between items-center">
        <NavItem href="/">Home</NavItem> 
        <NavItem href="/blockbuster_chat">Blockbuster Chat V1</NavItem>
        <NavItem href="/blockbuster_chat_2">Blockbuster Chat V2</NavItem>
        <NavItem href="/tbd2">TBD2</NavItem>
      </ul>
    </nav>
  );
}

You can see I just renamed the menu item to Blockbuster Chat V1 and added a new menu item for Blockbuster Chat V2, so we can work on our new version of the chat app separately. Save our changes and close the file.

If you run (npm run dev) and open your project in the browser, you should see the new menu item in the navbar:

Creating an interface for chat messages

Awesome! Now in order to make our new page work, we will need to create two things: a new page at /blockbuster_chat_2 and a new API route at /api/blockbuster_chat_2 for this new page to call. As we want this new version of the chat to be able to have a chat history, we want to keep track of the chat messages in a list that we can store and also send to our API endpoint.

We can use TypeScript to create a ChatMessage interface (type) for this. Since we will want both our client-side page and our server-side API to be able to work with this type, we will create a new folder called types in the app folder of our project, and then create a new file called chatMessage.ts inside of it:

πŸ“ app
    πŸ“ api
        πŸ“ blockbuster
            πŸ“„ route.tsx
    πŸ“ blockbuster_chat
        πŸ“„ page.tsx
    πŸ“ counter
        πŸ“„ page.tsx
    πŸ“ types
        πŸ“„ chatMessage.ts   ✨ New file
    πŸ“„ favicon.ico
    πŸ“„ globals.css
    πŸ“„ layout.tsx
    πŸ“„ navbar.tsx
    πŸ“„ page.tsx

Now you might be concerned about what happens if people go to website.com/types since we have created a folder called types. Don’t worry, Next.js will not create a new page on your website, since we don’t have any page.tsx file inside of it.

So open up the chatMessage.ts file and add the following interface inside of it:

export interface ChatMessage {
  text: string;
  sender: string;
  isAnswer?: boolean;
  time_sent: Date;
}

So we have a simple interface to define what each individual ChatMessage object should look like. It has a text property which is a string, a sender property which is also a string, an optional (as indicated by the ?) isAnswer property which is a boolean, and a time_sent property which is a Date object.

The isAnswer property exists as we will have to store not just the messages that ChatGPT sends to us, but also the messages that the user sends to ChatGPT.

Creating the new API route

So let’s first create our new API route, and after that, we’ll work on the page for Blockbuster Chat V2. Inside the api folder, create a new folder called blockbuster_v2 and inside of it create a new file called route.tsx:

πŸ“ app
    πŸ“ api
        πŸ“ blockbuster
            πŸ“„ route.tsx
        πŸ“ blockbuster_v2
            πŸ“„ route.tsx    ✨ New file
    πŸ“ blockbuster_chat
        πŸ“„ page.tsx
    πŸ“ counter
        πŸ“„ page.tsx
    πŸ“ types
        πŸ“„ chatMessage.ts
    πŸ“„ favicon.ico
    πŸ“„ globals.css
    πŸ“„ layout.tsx
    πŸ“„ navbar.tsx
    πŸ“„ page.tsx

Now open up this new file and let’s get to coding up our new API route. Start with the imports and setup:

import { NextRequest, NextResponse } from "next/server";
import { ChatMessage } from "../../types/chatMessage";
import OpenAI from "openai";
import { ChatCompletionMessageParam } from "openai/resources/index.mjs";

const openAIConfig = { apiKey: process.env.OPENAI_API_KEY };
const openai = new OpenAI(openAIConfig);

This is largely the same as the previous API route, but we also import the ChatMessage type we created earlier, and something new called ChatCompletionMessageParam from the openai library, which is nothing but a type definition that the OpenAI library uses so we’ll need it later on.

We have the openAIConfig object where we load our API key once more and then create our openai object.

The only thing we need for this particular API is a POST route, so let’s create that:

export async function POST(request: NextRequest) {
  // All the following code blocks will go inside here.
}

As we will have a whole bunch of code blocks inside of the POST function, we’ll go over each one step by step. After we’ve looked at all of them, I’ll provide the full function for you once more just for clarity. Keep in mind all the following code blocks are to be put inside the POST function.

First of all, this API route will need to receive a bunch of things so that it can do its work. So let’s think of the things we want to receive in our request object when a call is made to this API route. We want to receive the following:

  • Both the name of the movie and the name of the movie character, so we can put these in ChatGPT’s prompt.
  • The question that the user would like to ask.
  • The chatHistory so far which should be an array of ChatMessage objects. ChatGPT will need to know what has been said before so it can continue the conversation. As this could obviously be the first message this might be an empty array.

So, let’s assume that all of these will be sent into the