VasilisKarantousis

How I build a chatbot for my portfolio website with Next.js 15, Vercel AI SDK, and GPT-4o-mini

Publication date: March 20, 2025

  • #NextJS
  • #VercelAI
  • #ChatbotTutorial
  • #GPT4oMini

Building a chatbot for my portfolio website, vks.gr, was one of the most exciting projects I’ve tackled as a web developer. I wanted something interactive, user-friendly, and modern to showcase my skills while helping visitors explore my work. In this tutorial, I’ll walk you through how I created "Vai"—my AI assistant—using Next.js 15, the Vercel AI SDK, Tailwind CSS, and OpenAI’s GPT-4o-mini model. Whether you’re a beginner or a seasoned dev, this step-by-step guide is designed to be easy to follow, with code snippets you can adapt for your own projects.

By the end, you’ll have a sleek, functional chatbot embedded in your site—perfect for portfolios, e-commerce, or any web app. Let’s dive in!

Why I Built This Chatbot

As a freelance developer, I wanted vks.gr to stand out. A chatbot seemed like the perfect way to:

  • Engage visitors with real-time answers about my portfolio, services, and blog.
  • Showcase my skills with cutting-edge tech like AI and Next.js.
  • Make the site more interactive and fun.

I chose Next.js 15 for its App Router and server-side capabilities, Vercel AI SDK for seamless AI integration, Tailwind CSS for fast styling, and GPT-4o-mini for its efficiency and cost-effectiveness. Ready to build your own? Let’s get started.

Prerequisites

Before we begin, make sure you have:

  • Node.js (v18 or later) installed.
  • A basic understanding of React and TypeScript.
  • An OpenAI API key (get one from platform.openai.com).
  • A Next.js project set up (npx create-next-app@latest).

Step 1: Set Up Your Next.js Project

If you don’t already have a Next.js app, create one:

npx create-next-app@15 my-chatbot-app
cd my-chatbot-app
npm install

I used TypeScript for type safety, so select that option when prompted. Next, install the key dependencies:

npm install @ai-sdk/openai ai tailwindcss framer-motion @heroicons/react firebase-admin
  • @ai-sdk/openai: Connects to OpenAI’s API via Vercel AI SDK.
  • ai: Vercel AI SDK core package.
  • tailwindcss: For styling the chatbot UI.
  • framer-motion: For smooth animations.
  • @heroicons/react: Icons for the UI.
  • firebase-admin: Optional, for storing chat sessions (I used Firebase, but you can skip this).

Configure Tailwind CSS by running npx tailwindcss init and updating your tailwind.config.ts—check the Tailwind docs for details.

Step 2: Create the Chat API Route

The chatbot needs a backend to process messages and talk to GPT-4o-mini. I created an API route at app/api/chat/route.ts. Here’s a simplified version:

// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

export const maxDuration = 60; // Vercel timeout limit

export async function POST(req: Request) {
  const { messages } = await req.json();

  const systemPrompt = `
    You are Vai, an AI assistant for a portfolio website (vks.gr).
    Be friendly, concise, and helpful. Answer questions about my work, services, or anything related to the site!
  `;

  const result = await streamText({
    model: openai("gpt-4o-mini"),
    system: systemPrompt,
    messages,
  });

  return result.toDataStreamResponse();
}

What’s Happening Here?

  • Imports: We bring in OpenAI and streaming utilities from Vercel AI SDK.
  • POST Handler: Accepts user messages via a JSON request.
  • System Prompt: Tells GPT-4o-mini it’s "Vai" and sets its tone.
  • Streaming: Uses streamText to send responses in real-time, improving UX.
  • Environment: Add your OpenAI API key to a .env file: OPENAI_API_KEY=your-key-here.

This API streams responses from GPT-4o-mini, keeping the chatbot fast and responsive. In my full version, I also pulled in portfolio data from JSON and Firebase—feel free to customize this with your own content!

Step 3: Build the Chat UI Component

Now, let’s create the front-end. I placed my chatbot in app/components/chat/Chat.tsx. Here’s a streamlined version:

// app/components/chat/Chat.tsx
"use client";

import { useChat } from "@ai-sdk/react";
import React, { useState } from "react";
import { XMarkIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { motion } from "framer-motion";

export default function Chat() {
  const [isOpen, setIsOpen] = useState(false);
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: "/api/chat",
    initialMessages: [
      {
        id: "welcome",
        role: "assistant",
        content: "I’m Vai — your AI assistant! How can I help you today?",
      },
    ],
  });

  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    handleSubmit(e);
  };

  return (
    <>
      {!isOpen && (
        <motion.button
          onClick={() => setIsOpen(true)}
          className="fixed bottom-4 right-4 px-4 py-2 bg-yellow-400 text-gray-900 rounded-full shadow-lg"
          whileHover={{ scale: 1.1 }}
          whileTap={{ scale: 0.9 }}
        >
          Chat with Vai
        </motion.button>
      )}
      {isOpen && (
        <section className="fixed bottom-4 right-4 w-[90vw] md:w-[400px] h-[500px] bg-gray-800 text-white rounded-xl shadow-2xl flex flex-col">
          <header className="p-4 border-b border-gray-700 flex justify-between">
            <h2 className="text-lg font-semibold">Vai — AI Assistant</h2>
            <button onClick={() => setIsOpen(false)}>
              <XMarkIcon className="w-5 h-5" />
            </button>
          </header>
          <div className="flex-1 p-4 overflow-y-auto">
            {messages.map((m) => (
              <div
                key={m.id}
                className={`p-3 rounded-lg my-2 ${
                  m.role === "user" ? "bg-yellow-400 text-black ml-auto" : "bg-gray-700"
                }`}
              >
                {m.content}
              </div>
            ))}
          </div>
          <form onSubmit={onSubmit} className="p-4 border-t border-gray-700 flex gap-2">
            <textarea
              value={input}
              onChange={handleInputChange}
              placeholder="Ask anything"
              className="flex-1 p-2 bg-gray-700 rounded-xl text-white"
              rows={2}
            />
            <button type="submit" className="p-2 bg-yellow-400 rounded-full">
              <PaperAirplaneIcon className="w-5 h-5 text-gray-900" />
            </button>
          </form>
        </section>
      )}
    </>
  );
}

Key Features:

  • useChat Hook: Manages chat state and API calls effortlessly.
  • Modal Design: A floating button opens the chat window.
  • Tailwind CSS: Styles like bg-yellow-400 and rounded-xl keep it clean and modern.
  • Framer Motion: Adds subtle animations to the button (whileHover, whileTap).
  • Accessibility: Basic ARIA labels and focus management (my full version has more).

In my real app, I added keyboard navigation, session persistence with Firebase, and Markdown parsing as well.

Step 4: Integrate the Chatbot into Your Layout

To make the chatbot accessible site-wide, I added it to app/layout.tsx:

// app/layout.tsx
import Chat from "@/app/components/chat/Chat";
import "./globals.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="min-h-screen flex flex-col">
        {children}
        <div className="fixed bottom-10 right-10">
          <Chat />
        </div>
      </body>
    </html>
  );

This positions the chatbot in the bottom-right corner, floating above the content. I also used custom fonts (Comfortaa, Montserrat) and a progress bar via nextjs-toploader—small touches to polish the experience.

Step 5: Test and Deploy

Run your app locally:

npm run dev

Click the "Chat with Vai" button, type a message, and watch Vai respond! If it works, deploy to Vercel:

vercel

Vercel’s serverless functions handle the API route perfectly, and the streaming response keeps it snappy.

Bonus: Customization Tips

  • Styling: Tweak Tailwind classes to match your brand (I used yellow accents for vks.gr).
  • AI Model: Swap GPT-4o-mini for another model via @ai-sdk/openai.
  • Data: Feed your chatbot custom content (I used JSON and Firebase for portfolio data).
  • Animations: Add more Framer Motion effects to the chat window.

Conclusion

Building Vai for vks.gr was a blast, and I hope this tutorial inspires you to add a chatbot to your own site. With Next.js 15, Vercel AI SDK, and Tailwind CSS, it’s easier than ever. Have questions? Drop a comment or reach out—I’d love to hear how you tweak this for your projects!

Do you like what you see?

It's crafted with Next.js, TailwindCSS, Framer Motion, and a mix of dangerous magic and chemical reactions.

Download