Build Your Own AI Assistant with OpenAI and Next.js : A Step-by-Step Guide

In today’s digital age, where communication is the backbone of every business, staying connected with your customers is more critical and more challenging than ever.

If you’re running a small business or offering services in limited bugged, managing inquiries, providing support and engaging with users can feel very challenging at times. But what if you could build your own AI assistant to handle these tasks for you effortlessly, efficiently, and around the clock?

In this guide, I will walk you through creating an AI assistant for your domain using OpenAI Chat Completions API and Next.js.

Let's dive in!

🧠 Define Your Assistant’s Purpose

Before writing a single line of code, take a moment to clarify the "why" behind your AI assistant. A well-defined purpose helps shape the assistant’s tone, logic and responses ultimately leading to a better user experience.

Let’s walk through the essentials using a relatable example:

🎯 Assistant Goal

You're a maths teacher who wants to build an AI assistant to support students with their math questions.

🧩 Define the Core Attributes

  • πŸ“š Specialization Area: Focus on mathematics from Class 1 to Class 10. This keeps the assistant domain-specific and easy to fine-tune.

  • πŸ‘¨β€πŸ« Target Users: School students in grades 1 to 10 who need help understanding or solving math problems.

  • πŸ› οΈ Core Functionality:

    • Solve math queries with step-by-step explanations.

    • Answer in a simple, friendly and fun tone to keep students engaged.

  • 🎭 Personality: The assistant, let’s call it Rubi Madam, should be:

    • Funny and straightforward, to make learning enjoyable.

    • Conversational in Hinglish (Hindi + English) to resonate with local students.

    • Use emojis, short sentences, and easy words to avoid overwhelming the user.

By defining these attributes, we have set the stage for a more focused and effective AI assistant. This clarity will guide our development process, ensuring that every line of code or prompt aligns with your assistant's purpose.

πŸ› οΈ Step 2: Set Up Your Project

Create a Next.js Project

npx create-next-app@latest maths-assistant

Now navigate to your project directory and install all dependencies.

cd maths-assistant
npm i axios

Now start the server

npm run dev

πŸ” Step 3: Configure Environment Variables

Just create your Open AI API key from https://platform.openai.com/api-keys and save it in the .env file at the root of your project!

# .env
OPENAI_API_KEY=your_api_key_here

πŸ“ Set Up the Directory Structure

Set up your project with the following structure:

my-assistant/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ app/
β”‚ β”‚ β”œβ”€β”€ page.tsx # Main chat interface
β”‚ β”‚ β”œβ”€β”€ api/
β”‚ β”‚ β”‚ └── v1/
β”‚ β”‚ β”‚ └── chat/
β”‚ β”‚ β”‚ └── completions/
β”‚ β”‚ β”‚ └── route.ts # API endpoint
β”‚ β”œβ”€β”€ lib/
β”‚ β”‚ └── openai.ts # OpenAI API client
β”‚ β”œβ”€β”€ utils/
β”‚ β”œβ”€β”€ types/
└── ...

🧾 Define Types

// src/types/index.ts
export type OpenAIMessageType = {
  role: "system" | "user" | "assistant";
  content: string;
};

export type ChatMessageType = OpenAIMessageType & {
  id: string;
  isLoading?: boolean;
};

πŸ›  Create Utility Functions

// src/utils/index.ts
export const classNames = (...classes: string[]) => {
  return classes.filter(Boolean).join(" ");
};

export const generateMessageId = () => {
  return Date.now() + Math.random().toString(36).substring(2, 10);
};

πŸ“š Create a Constants File

// src/utils/constants.ts
export const ChatRoles = {
  SYSTEM: "system",
  USER: "user",
  ASSISTANT: "assistant",
};

export const HttpStatusCodes = {
  OK: 200,
  BAD_REQUEST: 400,
  INTERNAL_SERVER_ERROR: 500,
};

πŸ€– Create the Open AI API Client and Helper Functions

// src/lib/openai.ts
import axios from "axios";
import { OpenAIMessageType } from "@/types";
import { HttpStatusCodes } from "@/utils/constants";

export const apiClient = axios.create({
  baseURL: "https://api.openai.com/v1",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
});

export const getChatCompletion = async (messages: OpenAIMessageType[]) => {
  try {
    const response = await apiClient.post("/chat/completions", {
      model: "gpt-3.5-turbo", // Specifies the language model to use
      messages: messages, // Array of message objects (role + content) forming the conversation history
      max_tokens: 2000, // Maximum number of tokens the model can generate in the response
      temperature: 1, // Controls randomness: 0 = deterministic, 1 = more creative
    });

    if (response.status !== HttpStatusCodes.OK) {
      throw {
        statusCode: response.status,
        message: response.data.error.message || "Something went wrong",
      };
    }

    const message = response.data.choices[0].message as OpenAIMessageType;
    return message;
  } catch (error) {
    console.log("Error fetching chat completion:", error);

    if (axios.isAxiosError(error)) {
      const statusCode =
        error.response?.status || HttpStatusCodes.INTERNAL_SERVER_ERROR;
      const message =
        error.response?.data?.error?.message ||
        "Something went wrong while fetching chat completion";

      throw { statusCode, message };
    } else {
      throw {
        statusCode: HttpStatusCodes.INTERNAL_SERVER_ERROR,
        message: "An unexpected error occurred",
      };
    }
  }
};

🌐 Create API Route Handler

// src/app/api/v1/chat/completions/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getChatCompletion } from "@/lib/openai";
import { generateMessageId } from "@/utils";
import { HttpStatusCodes } from "@/utils/constants";

export async function POST(request: NextRequest) {
  try {
    const { messages } = await request.json();

    if (!messages || messages.length === 0) {
      return NextResponse.json(
        {
          statusCode: HttpStatusCodes.BAD_REQUEST,
          message: "No messages provided",
        },
        { status: HttpStatusCodes.BAD_REQUEST }
      );
    }

    const { content } = await getChatCompletion(messages);

    return NextResponse.json({
      status: HttpStatusCodes.OK,
      message: "Chat completion fetched successfully",
      data: {
        id: generateMessageId(),
        role: "assistant",
        content,
      },
    });
  } catch (error: any) {
    console.error("OpenAI API Error:", error);

    const statusCode =
      error.statusCode || HttpStatusCodes.INTERNAL_SERVER_ERROR;
    const message = error.message || "Failed to fetch chat completion";

    return NextResponse.json({ statusCode, message }, { status: statusCode });
  }
}

πŸ’¬ Build the Chat UI

// src/app/page.tsx
"use client";

import { useState } from "react";
import { classNames, generateMessageId } from "@/utils";
import axios from "axios";
import { OpenAIMessageType, ChatMessageType } from "@/types";
import { ChatRoles } from "@/utils/constants";

export default function Home() {
  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState<ChatMessageType[]>([
    {
      id: generateMessageId(),
      role: "system",
      content: `
        You are an AI Assistant name Rubi. who is specialized in maths.
        You should not answer any query that is not related to maths.

        For a given query help user to solve that along with explanation.

        Tone:
          - Funny and straightforward.
          - Use emojis to make it more engaging.

        Language:
          - Hinglish (Hindi + English) for better understanding.
          - Use simple and easy to understand language.
          - Use short sentences and avoid jargon.

        Example:
        Input: 3 + 2
        Output: 3 + 2 is 5.

        Input: 3 * 10
        Output: 3 * 10 is 30. Fun fact you can even multiply 10 * 3 which gives same result.

        Input: Why is the color of sky?
        Output: Are you alright? Is it maths query?

        Input: What is your name?
        Output: My name is Rubi. How can I assist you today?
      `,
    },
    {
      id: generateMessageId(),
      role: "assistant",
      content: "Hello my name is Rubi! How can I assist you today?",
    },
  ]);
  const [loading, setLoading] = useState(false);

  // Helper to create message objects with only necessary data
  const createMessage = (
    role: ChatMessageType["role"],
    content: string,
    isLoading = false
  ): ChatMessageType => ({
    id: generateMessageId(),
    role,
    content,
    ...(isLoading && { isLoading }),
  });

  const handleSendMessage = (e: React.FormEvent) => {
    e.preventDefault();
    if (!message.trim()) return;
    setLoading(true);

    const userMsg = createMessage("user", message);
    const latestMsgs = messages.concat(userMsg);

    const loadingMsg = createMessage("assistant", "Thinking...", true);
    // Add user message and loading message
    setMessages([...latestMsgs, loadingMsg]);
    setMessage("");

    const payload = {
      messages: latestMsgs.map(({ role, content }) => ({ role, content })),
    };
    axios
      .post("/api/v1/chat/completions", payload)
      .then((res) => {
        const replyMsg = res.data.data as OpenAIMessageType;

        if (replyMsg && replyMsg.content) {
          const assistantMessage = createMessage("assistant", replyMsg.content);

          // Replace loading message with actual response
          setMessages((prevMsgs) => {
            const updatedMessages = [...prevMsgs];
            updatedMessages.pop(); // Remove loading message
            return [...updatedMessages, assistantMessage];
          });
        } else {
          // Remove loading message if there's an error
          setMessages((prevMsgs) => {
            const updatedMessages = [...prevMsgs];
            updatedMessages.pop(); // Remove loading message
            return updatedMessages;
          });
          alert("No response from the assistant.");
        }
      })
      .catch((error) => {
        // Remove loading message on error
        setMessages((prevMsgs) => {
          const updatedMessages = [...prevMsgs];
          updatedMessages.pop();
          return updatedMessages;
        });
        alert(error.response?.data?.message || "Something went wrong");
      })
      .finally(() => setLoading(false));
  };

  return (
    <div className="flex flex-col h-screen bg-gray-100">
      {/* Header */}
      <header className="bg-white shadow-sm p-4 flex items-center">
        <h1 className="text-xl font-semibold text-gray-800">
          Rubi Your Maths Assistant
        </h1>
      </header>

      {/* Chat area */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((msg, index) => {
          const isUserMsg = msg.role === ChatRoles.USER;

          if (msg.role === "system") return null; // Skip system messages

          return (
            <div
              key={index}
              className={classNames(
                "flex items-start space-x-2",
                isUserMsg ? "justify-end" : "justify-start"
              )}
            >
              <div
                className={classNames(
                  "max-w-[70%] p-3 rounded-lg shadow",
                  isUserMsg
                    ? "bg-blue-500 text-white"
                    : "bg-white text-gray-800"
                )}
              >
                {msg.content}
              </div>
            </div>
          );
        })}
      </div>

      {/* Message input */}
      <form
        onSubmit={handleSendMessage}
        className="bg-white p-4 shadow-lg border-t"
      >
        <div className="flex space-x-2">
          <input
            type="text"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            placeholder="Type your message..."
            className="flex-1 border rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
            disabled={loading}
          />

          <button
            type="submit"
            className="bg-blue-500 text-white rounded-full p-2 w-10 h-10 flex items-center justify-center hover:bg-blue-600"
            disabled={loading}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
              className="h-5 w-5 rotate-90"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
              />
            </svg>
          </button>
        </div>
      </form>
    </div>
  );
}

If you have followed all the steps correctly, you should now have a fully functional AI assistant that can handle math queries in a fun and engaging way.

Congratulations GIFs - The Best GIF Collections Are On GIFSEC

Repository link: https://github.com/OmGameHub/math-ai-assistant

βœ… Best Practices for Implementing AI Assistants

  1. 🎯 Clearly Define the Assistant’s Scope

    To avoid confusion and prevent the assistant from providing irrelevant or misleading responses.

     You are an AI Assistant named Rubi, specialized in maths.
     You should not answer any query that is not related to maths.
    
  2. πŸ—£οΈ Design a Consistent Personality & Tone

    To make the assistant feel more human and relatable.

     Tone: 
     - Funny and straightforward. 
     - Use emojis to make it more engaging.
    
     Language:
     - Hinglish (Hindi + English) for better understanding. 
     - Use simple and easy-to-understand language. 
     - Use short sentences and avoid jargon.
    
  3. 🧱 Use System Prompts with examples to Control Behaviour

    Set system prompts to guide the assistant's responses and help create clear boundaries.

     You are an AI Assistant name Rubi. who is specialized in maths.
     You should not answer any query that is not related to maths.
    
     For a given query help user to solve that along with explanation.
    
     Tone:
    
     - Funny and straightforward.
     - Use emojis to make it more engaging.
    
     Language:
    
     - Hinglish (Hindi + English) for better understanding.
     - Use simple and easy-to-understand language.
     - Use short sentences and avoid jargon.
    
     Example:
     Input: 3 + 2
     Output: 3 + 2 is 5.
    
     Input: 3 _ 10
     Output: 3 _ 10 is 30. Fun fact you can even multiply 10 * 3 which gives same result.
    
     Input: Why is the color of sky?
     Output: Are you alright? Is it maths query?
    
     Input: What is your name?
     Output: My name is Rubi. How can I assist you today?
    
  4. πŸ”’ Never Expose Sensitive Keys on the Client Side

    To avoid misuse or abuse of your OpenAI API key.

    Example: We store the OPENAI_API_KEY securely in the .env file and use it server-side only via the API route at /api/v1/chat/completions.

  5. πŸ§ͺ Test for Edge Cases and Non-domain Questions

To ensure that the assistant stays on-topic and doesn't hallucinate answers.

Example:

  • βœ… β€œWhat is 2 power 100?” β†’ correct answer

  • ❌ β€œWhat is amazon.com?” β†’ reject with a fun message

  1. πŸ” Continuously Improve Through Real User Feedback

    • To ensure that the assistant is always improving and adapting to user needs.

    • Example:

      • Take feedback from users and improve the assistant's responses.

      • You can use tools like LangGraph etc.

Thank you for reading! πŸš€

Happy learning! 😊

0
Subscribe to my newsletter

Read articles from Om Prakash Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Om Prakash Pandey
Om Prakash Pandey