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.
Repository link: https://github.com/OmGameHub/math-ai-assistant
β Best Practices for Implementing AI Assistants
π― 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.
π£οΈ 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.
π§± 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?
π 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
.π§ͺ 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
π 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! π
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
