DEV Community

Cover image for Creating an AI Chatbot
Ahmed Boabae
Ahmed Boabae

Posted on

Creating an AI Chatbot

Building an AI Support Assistant: A Complete Step-by-Step Guide

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Setup
  4. Installing Dependencies
  5. Environment Configuration
  6. Project Structure
  7. Application Code
  8. Running and Testing
  9. Deployment

Introduction

In this tutorial, we'll build a modern AI Support Assistant using Next.js 14 and Material-UI (MUI). This application provides a chat interface where users can interact with an AI assistant powered by Google's Generative AI.

Prerequisites

Before starting, ensure you have:

  • Node.js installed (version 16.x or higher)
  • A code editor (VS Code recommended)
  • A Google AI API key
  • Basic knowledge of React and JavaScript

Project Setup

  1. Open your terminal and navigate to where you want to create your project:
cd your/preferred/directory
Enter fullscreen mode Exit fullscreen mode
  1. Create a new Next.js project:
npx create-next-app@latest ai-support-assistant
Enter fullscreen mode Exit fullscreen mode

You should see something like this:
Create Next App

  1. When prompted, select the following options:
✔ Would you like to use TypeScript? → No
✔ Would you like to use ESLint? → Yes
✔ Would you like to use Tailwind CSS? → No
✔ Would you like to use `src/` directory? → No
✔ Would you like to use App Router? → Yes
✔ Would you like to customize the default import alias? → No
Enter fullscreen mode Exit fullscreen mode

Next.js Setup Options

  1. Navigate into your project directory:
cd ai-support-assistant
Enter fullscreen mode Exit fullscreen mode

Installing Dependencies

Install all required dependencies with the following commands:

# Install Material-UI and its dependencies
npm install @emotion/react@^11.13.0 @emotion/styled@^11.13.0 @mui/material@^5.16.6 @mui/icons-material@^5.16.7

# Install AI and utility packages
npm install @google/generative-ai@^0.16.0 react-markdown@^9.0.1

# Install development dependencies
npm install -D @babel/eslint-parser@^7.25.9 @types/json-schema@^7.0.15 eslint@^8 eslint-config-next@14.2.5
Enter fullscreen mode Exit fullscreen mode

Environment Configuration

  1. Create a new file called .env.local in your project root:
touch .env.local
Enter fullscreen mode Exit fullscreen mode
  1. Add the following environment variable to .env.local:
API_KEY=your_google_api_key_here
Enter fullscreen mode Exit fullscreen mode
  1. Add .env.local to your .gitignore file if it's not already there:
echo ".env.local" >> .gitignore
Enter fullscreen mode Exit fullscreen mode

Project Structure

Your initial project structure should look like this:

ai-support-assistant/
├── app/
│   ├── api/
│   │   └── chat/
│   │       └── route.js
│   ├── layout.js
│   ├── page.js
│   ├── globals.css
│   ├── page.module.css
│   └── favicon.ico
├── public/
├── .env.local
├── package.json
├── package-lock.json
├── next.config.mjs
├── jsconfig.json
└── .eslintrc.json
Enter fullscreen mode Exit fullscreen mode

Note: The next.config.mjs, jsconfig.json, and .eslintrc.json files are automatically created when you run npx create-next-app.

Application Code

Here's what we'll be building:

Preview

  1. Create app/layout.js:
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
  title: "Your title here",
  description: "Your description here",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. Create app/globals.css:
:root {
  --max-width: 1100px;
  --border-radius: 12px;
  --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
    "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
    "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
}
Enter fullscreen mode Exit fullscreen mode
  1. Create app/page.js:
"use client";

import {
  Box,
  CircularProgress,
  IconButton,
  Stack,
  TextField,
  Switch,
  FormControlLabel,
  Typography,
} from "@mui/material";
import { useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { createTheme, ThemeProvider } from "@mui/material/styles";
import SendIcon from "@mui/icons-material/Send";

export default function Home() {
  const [messages, setMessages] = useState([
    {
      role: "model",
      parts: [
        {
          text: "Hi, how can I be of assistance?",
        },
      ],
    },
  ]);
  const [message, setMessage] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [darkMode, setDarkMode] = useState(true);

  const sendMessage = async () => {
    if (!message.trim() || isLoading) return;
    setIsLoading(true);

    setMessage("");
    setMessages((messages) => [
      ...messages,
      { role: "user", parts: [{ text: message }] },
      { role: "model", parts: [{ text: "" }] },
    ]);

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

      if (!response.ok) {
        throw new Error("The network did not respond");
      }

      if (response.body) {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let fullResponse = "";

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          const text = decoder.decode(value || new Uint8Array(), {
            stream: true,
          });
          fullResponse += text;
          setMessages((messages) => {
            let lastMessage = messages[messages.length - 1];
            let otherMessages = messages.slice(0, messages.length - 1);
            return [
              ...otherMessages,
              {
                ...lastMessage,
                parts: [{ text: fullResponse }],
              },
            ];
          });
        }
      }
    } catch (error) {
      console.error("Error:", error);
      setMessages((messages) => [
        ...messages,
        {
          role: "model",
          parts: [
            {
              text: "An error occurred, please try again later",
            },
          ],
        },
      ]);
    }
    setIsLoading(false);
  };

  const handleKeyPress = (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };

  const messagesEndRef = useRef(null);
  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behaviour: "smooth" });
  };

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

  const lightTheme = createTheme({
    palette: {
      mode: "light",
      primary: {
        main: "#2563eb",
        dark: "#1d4ed8",
      },
      secondary: {
        main: "#059669",
      },
      background: {
        default: "#f8fafc",
        paper: "#ffffff",
      },
    },
    typography: {
      fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
    },
    components: {
      MuiTextField: {
        styleOverrides: {
          root: {
            "& .MuiOutlinedInput-root": {
              borderRadius: "12px",
            },
          },
        },
      },
    },
  });

  const darkTheme = createTheme({
    palette: {
      mode: "dark",
      primary: {
        main: "#3b82f6",
        dark: "#2563eb",
      },
      secondary: {
        main: "#10b981",
      },
      background: {
        default: "#111827",
        paper: "#1f2937",
      },
    },
    typography: {
      fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
    },
    components: {
      MuiTextField: {
        styleOverrides: {
          root: {
            "& .MuiOutlinedInput-root": {
              borderRadius: "12px",
            },
          },
        },
      },
    },
  });

  return (
    <ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
      <Box
        sx={{
          bgcolor: "background.default",
          minHeight: "100vh",
          color: "text.primary",
        }}
      >
        <Box
          sx={{
            maxWidth: "800px",
            margin: "0 auto",
            p: 2,
          }}
        >
          <Stack
            direction="row"
            spacing={2}
            alignItems="center"
            justifyContent="space-between"
            sx={{ mb: 2 }}
          >
            <Typography variant="h4" component="h1">
              Chatto AI
            </Typography>
            <FormControlLabel
              control={
                <Switch
                  checked={darkMode}
                  onChange={(e) => setDarkMode(e.target.checked)}
                />
              }
              label="Dark"
            />
          </Stack>

          <Box
            sx={{
              height: "calc(100vh - 200px)",
              bgcolor: "background.paper",
              borderRadius: 3,
              p: 2,
              mb: 2,
              overflowY: "auto",
            }}
          >
            {messages.map((message, index) => (
              <Box
                key={index}
                sx={{
                  mb: 2,
                  p: 2,
                  borderRadius: 2,
                  bgcolor:
                    message.role === "user"
                      ? "primary.dark"
                      : "background.default",
                  alignSelf:
                    message.role === "user" ? "flex-end" : "flex-start",
                  maxWidth: "80%",
                }}
              >
                <ReactMarkdown>{message.parts[0].text}</ReactMarkdown>
              </Box>
            ))}
            {isLoading && (
              <Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
                <CircularProgress size={24} />
              </Box>
            )}
            <div ref={messagesEndRef} />
          </Box>

          <Box
            component="form"
            sx={{
              display: "flex",
              gap: 1,
              bgcolor: "background.paper",
              p: 2,
              borderRadius: 3,
            }}
            onSubmit={(e) => {
              e.preventDefault();
              sendMessage();
            }}
          >
            <TextField
              fullWidth
              multiline
              maxRows={4}
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="Type your message..."
              variant="outlined"
              disabled={isLoading}
            />
            <IconButton
              color="primary"
              onClick={sendMessage}
              disabled={!message.trim() || isLoading}
              sx={{
                alignSelf: "flex-end",
                bgcolor: "primary.main",
                color: "white",
                "&:hover": {
                  bgcolor: "primary.dark",
                },
                width: 56,
                height: 56,
              }}
            >
              <SendIcon />
            </IconButton>
          </Box>
        </Box>
      </Box>
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. Create app/api/chat/route.js:
import { NextResponse } from "next/server";
const { GoogleGenerativeAI } = require("@google/generative-ai");

const systemPrompt =
  "Your system prompt here. E.g: You are a friendly and knowledgeable academic assistant. Your role is to help users with anything related to academics,";

export async function POST(req) {
  const genAI = new GoogleGenerativeAI(process.env.API_KEY);
  const model = genAI.getGenerativeModel({
    model: "gemini-1.5-flash",
    systemInstruction: systemPrompt,
  });

  const data = await req.json();

  const chat = model.startChat({
    history: data.messages,
  });

  const result = await chat.sendMessageStream(data.msg);

  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      try {
        for await (const chunk of result.stream) {
          const content = await chunk.text();
          if (content) {
            const text = encoder.encode(content);
            controller.enqueue(text);
          }
        }
      } catch (err) {
        controller.error(err);
      } finally {
        controller.close();
      }
    },
  });

  return new NextResponse(stream);
}
Enter fullscreen mode Exit fullscreen mode

Running and Testing

  1. Start the development server:
npm run dev
Enter fullscreen mode Exit fullscreen mode

You should see this in your terminal:
Dev Server

  1. Open your browser and navigate to:
http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

You should see the chat interface:
Initial App

  1. Try sending a message:
    Chat Example
    Chat Example

  2. To stop the development server:

    • Press `

Top comments (0)