DEV Community

Cover image for Building an AI Assistant with Ollama and Next.js - Part 1
Abayomi Olatunji
Abayomi Olatunji

Posted on

Building an AI Assistant with Ollama and Next.js - Part 1

Introduction 🧠💬

Artificial Intelligence (AI) is reshaping how we interact with digital tools, and building your own local AI assistant has never been easier. In this guide, I’ll walk you through how I built a simple AI assistant using Next.js, TailwindCSS, and Ollama, running the Gemma 3:1B model

Note: You can run any model of your choice from the available models on https://ollama.com/models; however you should have at least 8 GB of RAM available to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models.).

Whether you're a beginner or just looking for a lightweight and privacy-friendly AI implementation, you’ll find this guide approachable and relatable. No cloud APIs. No subscriptions. Just local magic.


🧰 Tools Used

Before we dive in, here are the key tools used in this project:

  • Next.js – Our React framework of choice, with app router support.
  • TailwindCSS – For fast and beautiful styling.
  • Cursor IDE – A modern coding environment tailored for AI-assisted development. (I will create an article to help you setup your IDE, use rules and also work with MCPs)
  • Ollama – A simple way to run open-source large language models locally. Download and use directly from your terminal.
  • Gemma 3:1B model – A lightweight model great for running on most modern laptops. Using this just for testing purposes. Others includes Llama, DeepSeek, and Mistral, etc.

📦 Step 1: Set Up Your Next.js App

Let’s start by creating a new Next.js project. Open your terminal and run:

npx create-next-app@latest ollama-assistant --app
cd ollama-assistant
cursor .
Enter fullscreen mode Exit fullscreen mode

Installing Next.js now allows you to set up Tailwind, TypeScript, and other configurations from the installation process.

Then, run the app on your locals using:

npm run dev
Enter fullscreen mode Exit fullscreen mode

🤖 Step 2: Install and Run Ollama with Gemma

Ollama makes it super simple to run models locally. Head over to https://ollama.com/download and install it for your OS.

Once installed, open your terminal and run:

ollama run gemma3:1b
Enter fullscreen mode Exit fullscreen mode

This will download and start the Gemma 3:1B model locally. Once the download is complete, the model will launch, and you can start chatting with it on the terminal, as shown in the screenshot below.

Terminal Screenshot of Ollama setup


⚙️ Step 3: Connect Your App to Ollama

Next, we’ll add a simple API route that communicates with the local Ollama server. Ollama provides REST API endpoints that allow you to interact with the downloaded models. Once the terminal is running, it exposes an endpoint at http://localhost:11434/api, where you can send your HTTP requests.

app/api/chat/route.ts

import { NextResponse } from 'next/server';

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

    // Make request to Ollama API
    const response = await fetch('http://localhost:11434/api/generate', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'gemma3:1b',
        prompt: message,
        stream: false,
      }),
    });

    const data = await response.json();

    return NextResponse.json({
      response: data.response,
    });
  } catch (error) {
    console.error('Error:', error);
    return NextResponse.json(
      { error: 'Failed to process the request' },
      { status: 500 }
    );
  }
} 
Enter fullscreen mode Exit fullscreen mode

This endpoint accepts a message from the frontend and returns the model’s response.


🖼️ Step 4: Build the Chat Interface

Let’s create a simple UI where users can type and get responses from our assistant.

These files will be in a modules folder named chat. (you can use your preferred project folder structure).

ChatInput.tsx

import React, { useState } from 'react';

interface ChatInputProps {
  onSendMessage: (message: string) => void;
}

const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage }) => {
  const [message, setMessage] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim()) {
      onSendMessage(message);
      setMessage('');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="border-t border-gray-200 dark:border-gray-700 p-4">
      <div className="flex items-center gap-2">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type your message..."
          className="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 p-2 
                   bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
        />
        <button
          type="submit"
          className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 
                   transition-colors duration-200"
        >
          Send
        </button>
      </div>
    </form>
  );
};

export default ChatInput; 
Enter fullscreen mode Exit fullscreen mode
ChatMessage.tsx

import React from 'react';

interface ChatMessageProps {
  message: string;
  isUser: boolean;
}

const ChatMessage: React.FC<ChatMessageProps> = ({ message, isUser }) => {
  return (
    <div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`}>
      <div
        className={`${
          isUser
            ? 'bg-blue-600 text-white rounded-l-lg rounded-tr-lg'
            : 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-r-lg rounded-tl-lg'
        } px-4 py-2 max-w-[80%]`}
      >
        <p className="text-sm">{message}</p>
      </div>
    </div>
  );
};

export default ChatMessage; 
Enter fullscreen mode Exit fullscreen mode
ChatPage.tsx 

"use client"
import React, { useEffect, useRef, useState } from 'react';
import ChatInput from './ChatInput';
import ChatMessage from './ChatMessage';

interface Message {
  text: string;
  isUser: boolean;
}

const ChatPage: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const handleSendMessage = async (message: string) => {
    // Add user message
    setMessages(prev => [...prev, { text: message, isUser: true }]);
    setIsLoading(true);

    try {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ message }),
      });

      const data = await response.json();

      // Add AI response
      setMessages(prev => [...prev, { text: data.response, isUser: false }]);
    } catch (error) {
      console.error('Error:', error);
      setMessages(prev => [...prev, { text: 'Sorry, there was an error processing your request.', isUser: false }]);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto">
      <div className="bg-white dark:bg-gray-800 shadow-lg rounded-lg m-4 flex-1 flex flex-col overflow-hidden">
        <div className="p-4 border-b border-gray-200 dark:border-gray-700">
          <h1 className="text-xl font-semibold text-gray-800 dark:text-white">AI Assistant</h1>
        </div>

        <div className="flex-1 overflow-y-auto p-4">
          {messages.map((msg, index) => (
            <ChatMessage key={index} message={msg.text} isUser={msg.isUser} />
          ))}
          {isLoading && (
            <div className="flex justify-start mb-4">
              <div className="bg-gray-200 dark:bg-gray-700 rounded-lg px-4 py-2">
                <div className="animate-pulse flex space-x-2">
                  <div className="w-2 h-2 bg-gray-400 rounded-full"></div>
                  <div className="w-2 h-2 bg-gray-400 rounded-full"></div>
                  <div className="w-2 h-2 bg-gray-400 rounded-full"></div>
                </div>
              </div>
            </div>
          )}
          <div ref={messagesEndRef} />
        </div>

        <ChatInput onSendMessage={handleSendMessage} />
      </div>
    </div>
  );
};

export default ChatPage; 
Enter fullscreen mode Exit fullscreen mode

Then, the page/tsx in the app folder will have;

import ChatPage from '@/modules/ChatPage';

export default function Chat() {
  return <ChatPage />;
}

Enter fullscreen mode Exit fullscreen mode

Here is how the user interface look after running npm run dev in the terminal

Website UI

Viola! You can continue to build on this.


🧠 How It Works

Here’s a quick summary of what’s happening:

  • You enter a message in the text area.
  • The app sends your message to the /api/chat endpoint.
  • The endpoint forwards it to Ollama’s local API (localhost:11434).
  • Ollama responds with the model’s reply.
  • The frontend displays the AI response instantly.

💡 Why Use Ollama?

✅ Privacy – Everything runs locally. No data leaves your device.
✅ Speed – No network latency when calling the model.
✅ Cost-effective – No token limits or monthly subscriptions.


✨ What's Next?

While this method connects to the Ollama server externally (using the terminal), in the next article, I’ll show you how to build your assistant using the ollamajs package directly in your codebase for even tighter integration. Stay tuned!

📩 Feel free to drop a comment if you have any questions or need help setting things up!

Top comments (0)