Implementing Frontend and Backend Service Separation with Nextjs and FastAPI

Building Efficient Web Interfaces

Posted by LuochuanAD on June 18, 2026 本文总阅读量

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>
  );
}