Background
Previously, the Web pages for the AIAgent were built using Html+Css+Js. Besides looking unattractive, the session management and multi-user state handling in the Agent were overly complex and inefficient.
Choice
Use Next.js to build an efficient UI, with Next.js and FastAPI enabling front-end and back-end service separation for the Agent.
Below is the Agent UI page built with Next.js:

Next.js Code Example (Since it’s simple and classic, I won’t elaborate)
app/page.tsx
"use client";
import { useState, useRef, useEffect } from "react";
type Msg = {
role: "user" | "assistant";
content: string;
image?: string;
};
export default function ChatPage() {
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [messages, setMessages] = useState<Msg[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const bottomRef = useRef<HTMLDivElement | null>(null);
// Auto-scroll to the bottom
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = reject;
});
}
function startVoiceInput() {
const SpeechRecognition =
(window as any).SpeechRecognition ||
(window as any).webkitSpeechRecognition;
if (!SpeechRecognition) {
alert("Current browser does not support speech recognition");
return;
}
const recognition = new SpeechRecognition();
recognition.lang = "ja-JP";
recognition.interimResults = false;
recognition.onresult = (event: any) => {
const transcript = event.results[0][0].transcript;
setInput((prev) =>
prev ? prev + " " + transcript : transcript
);
};
recognition.start();
}
async function send() {
if (!input.trim() && !selectedImage) return;
const userMsg = input;
let imageBase64 = "";
if (selectedImage) {
imageBase64 = await fileToBase64(selectedImage);
}
setInput("");
setMessages((prev) => [
...prev,
{
role: "user",
content: userMsg,
image: imageBase64,
},
{
role: "assistant",
content: "",
},
]);
setSelectedImage(null);
setLoading(true);
const res = await fetch(
"/api/chat",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: userMsg,
image: imageBase64,
}),
}
);
const reader = res.body?.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { value, done } = await reader!.read();
if (done) break;
buffer += decoder.decode(value, {
stream: true,
});
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
if (!part.startsWith("data: ")) continue;
const text = part.replace("data: ", "");
if (text === "[DONE]") {
setLoading(false);
return;
}
setMessages((prev) => {
const copy = [...prev];
copy[copy.length - 1].content += text;
return copy;
});
}
}
setLoading(false);
}
return (
<div className="h-screen bg-zinc-950 text-white flex flex-col">
{/* Header */}
<div className="h-14 flex items-center justify-center border-b border-zinc-800">
<div className="text-sm text-zinc-300">AI Agent Chat</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-6">
<div className="max-w-3xl mx-auto space-y-6">
{messages.length === 0 && (
<div className="text-center text-zinc-500 mt-20">
Start a conversation...
</div>
)}
{messages.map((msg, i) => (
<div
key={i}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`px-4 py-3 rounded-2xl text-sm leading-relaxed whitespace-pre-wrap max-w-[80%] ${msg.role === "user"
? "bg-blue-600 text-white"
: "bg-zinc-800 text-zinc-100"
}`}
>
<>
{msg.image && (
<img
src={msg.image}
alt="upload"
className="rounded-xl mb-2 max-w-xs"
/>
)}
{msg.content}
</>
</div>
</div>
))}
<div ref={bottomRef} />
</div>
</div>
{/* Input */}
<div className="border-t border-zinc-800 p-4">
<div className="max-w-3xl mx-auto flex gap-2 items-center">
<button
onClick={() => fileInputRef.current?.click()}
className="px-3 py-3 rounded-xl bg-zinc-800 hover:bg-zinc-700"
>
📷
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
hidden
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setSelectedImage(file);
}
}}
/>
<button
onClick={startVoiceInput}
className="px-3 py-3 rounded-xl bg-zinc-800 hover:bg-zinc-700"
>
🎤
</button>
{selectedImage && (
<div className="max-w-3xl mx-auto mb-2">
<img
src={URL.createObjectURL(selectedImage)}
alt="preview"
className="h-24 rounded-lg"
/>
</div>
)}
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
}}
placeholder="Message..."
className="flex-1 bg-zinc-900 text-white px-4 py-3 rounded-xl outline-none border border-zinc-700 focus:border-zinc-500"
/>
<button
onClick={send}
disabled={loading}
className="px-4 py-3 rounded-xl bg-white text-black font-medium hover:bg-zinc-200 disabled:opacity-50"
>
{loading ? "..." : "Send"}
</button>
</div>
</div>
</div>
);
}