Building an AI Support Assistant: A Complete Step-by-Step Guide
Table of Contents
- Introduction
- Prerequisites
- Project Setup
- Installing Dependencies
- Environment Configuration
- Project Structure
- Application Code
- Running and Testing
- 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
- Open your terminal and navigate to where you want to create your project:
cd your/preferred/directory
- Create a new Next.js project:
npx create-next-app@latest ai-support-assistant
You should see something like this:
- 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
- Navigate into your project directory:
cd ai-support-assistant
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
Environment Configuration
- Create a new file called
.env.local
in your project root:
touch .env.local
- Add the following environment variable to
.env.local
:
API_KEY=your_google_api_key_here
- Add
.env.local
to your.gitignore
file if it's not already there:
echo ".env.local" >> .gitignore
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
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:
- 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>
);
}
- 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;
}
- 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>
);
}
- 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);
}
Running and Testing
- Start the development server:
npm run dev
You should see this in your terminal:
- Open your browser and navigate to:
http://localhost:3000
Top comments (0)