🤖 Gemini Chatbot 完整建置教學

從零開始建立一個完整的 AI 聊天機器人,使用 Next.js、TypeScript、Tailwind CSS 和 Google Gemini API

📑 目錄

1. 專案介紹

這個教學將帶你建立一個完整的 AI 聊天機器人,具備以下功能:

  • 現代化前端介面:使用 React 和 Tailwind CSS 建立美觀的聊天介面
  • 後端 API 整合:使用 Next.js API Routes 處理聊天請求
  • AI 對話功能:整合 Google Gemini API 實現智能對話
  • 對話紀錄儲存:自動儲存聊天紀錄到本地檔案
  • TypeScript 支援:完整的型別檢查,減少錯誤
💡 學習目標:完成這個專案後,你將學會如何建立全端 Web 應用程式、整合第三方 API、處理非同步請求,以及管理環境變數。

2. 前置需求

2.1 安裝必要軟體

在開始之前,請確保你的電腦已安裝以下軟體:

軟體 版本要求 下載連結
Node.js 18.0 或以上 https://nodejs.org/
npm 隨 Node.js 自動安裝 -
Git 最新版本 https://git-scm.com/
程式碼編輯器 - 建議使用 VS Code

2.2 驗證安裝

開啟終端機(Terminal 或 Command Prompt),執行以下指令確認安裝成功:

node --version
npm --version
git --version
✅ 預期結果:應該會顯示版本號碼,例如:
  • node: v20.x.x
  • npm: 10.x.x
  • git: 2.x.x

3. 環境設定

3.1 建立專案資料夾

選擇一個你喜歡的位置建立專案資料夾。例如在桌面或文件夾中:

# Windows (PowerShell)
mkdir gemini-chatbot
cd gemini-chatbot

# Mac/Linux
mkdir gemini-chatbot
cd gemini-chatbot
💡 提示:你可以將 gemini-chatbot 改成任何你喜歡的專案名稱。

4. 建立 Google AI API Key

4.1 前往 Google AI Studio

  1. 開啟瀏覽器,前往 Google AI Studio
  2. 使用你的 Google 帳號登入

4.2 建立 API Key

  1. 在頁面上點擊 "Create API Key""建立 API 金鑰" 按鈕
  2. 選擇你的 Google Cloud 專案(如果沒有,系統會自動建立一個)
  3. 點擊 "Create API Key in new project" 或選擇現有專案
  4. 等待幾秒鐘,系統會產生你的 API Key
⚠️ 重要:
  • API Key 只會顯示一次,請務必複製並妥善保存
  • 不要將 API Key 分享給他人或上傳到公開的程式碼庫
  • 如果遺失,可以重新建立新的 Key

4.3 複製 API Key

複製你的 API Key,格式類似:

AIzaSyDcthAMZTriGhOpjYt0zxmgpjGrEqlC2Zo

我們稍後會在專案中使用這個 Key。

5. 建立 Next.js 專案

5.1 使用 create-next-app 建立專案

在終端機中執行以下指令,建立一個新的 Next.js 專案:

npx create-next-app@latest gemini-chatbot --typescript --eslint --src-dir --app --tailwind --import-alias "@/*" --yes
💡 參數說明:
  • --typescript:啟用 TypeScript 支援
  • --eslint:啟用 ESLint 程式碼檢查
  • --src-dir:使用 src 資料夾結構
  • --app:使用 App Router(Next.js 13+ 的新路由系統)
  • --tailwind:啟用 Tailwind CSS
  • --import-alias "@/*":設定路徑別名
  • --yes:自動回答所有問題
✅ 預期結果:指令執行後會自動下載並安裝所有必要的套件,過程可能需要 1-2 分鐘。

5.2 進入專案資料夾

專案建立完成後,進入專案資料夾:

cd gemini-chatbot

5.3 檢查專案結構

你的專案資料夾應該包含以下結構:

gemini-chatbot/
├── src/
│   └── app/
│       ├── layout.tsx
│       ├── page.tsx
│       └── globals.css
├── public/
├── package.json
├── tsconfig.json
└── next.config.ts

6. 建立前端介面

6.1 建立聊天元件資料夾

src 資料夾中建立 components 資料夾:

# Windows
mkdir src\components

# Mac/Linux
mkdir -p src/components

6.2 建立 ChatWindow 元件

建立 src/components/ChatWindow.tsx 檔案,這是顯示聊天訊息的元件:

import React, { useEffect, useRef } from "react";

export type ChatMessage = {
  id: string;
  role: "user" | "assistant" | "system";
  content: string;
  timestamp: string;
};

type ChatWindowProps = {
  messages: ChatMessage[];
  isLoading: boolean;
};

export function ChatWindow({ messages, isLoading }: ChatWindowProps) {
  const containerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) return;
    containerRef.current.scrollTop = containerRef.current.scrollHeight;
  }, [messages, isLoading]);

  return (
    <div
      ref={containerRef}
      className="flex-1 overflow-y-auto rounded-2xl border border-zinc-200 bg-white/70 p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900/70"
    >
      {messages.length === 0 && !isLoading && (
        <div className="flex h-full items-center justify-center text-sm text-zinc-500 dark:text-zinc-400">
          開始輸入訊息,與你的 Gemini 助理聊天。
        </div>
      )}

      <div className="flex flex-col gap-3">
        {messages.map((msg) => (
          <div
            key={msg.id}
            className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
          >
            <div
              className={`max-w-[80%] whitespace-pre-wrap rounded-2xl px-4 py-2 text-sm leading-relaxed ${
                msg.role === "user"
                  ? "bg-blue-600 text-white"
                  : "bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50"
              }`}
            >
              {msg.content}
            </div>
          </div>
        ))}

        {isLoading && (
          <div className="flex justify-start">
            <div className="flex items-center gap-1 rounded-2xl bg-zinc-100 px-3 py-2 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
              <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-zinc-400 [animation-delay:-0.2s]" />
              <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-zinc-400 [animation-delay:-0.05s]" />
              <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-zinc-400" />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
💡 程式碼說明:
  • 這個元件負責顯示所有聊天訊息
  • 使用 useRefuseEffect 自動捲動到最新訊息
  • 使用者訊息顯示在右側(藍色),AI 訊息顯示在左側(灰色)
  • isLoading 為 true 時,顯示載入動畫

6.3 建立 ChatInput 元件

建立 src/components/ChatInput.tsx 檔案,這是輸入訊息的元件:

import React, { FormEvent, KeyboardEvent, useState } from "react";

type ChatInputProps = {
  onSend: (message: string) => void;
  disabled?: boolean;
};

export function ChatInput({ onSend, disabled }: ChatInputProps) {
  const [value, setValue] = useState("");

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    const trimmed = value.trim();
    if (!trimmed || disabled) return;
    onSend(trimmed);
    setValue("");
  };

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      const trimmed = value.trim();
      if (!trimmed || disabled) return;
      onSend(trimmed);
      setValue("");
    }
  };

  return (
    <form
      onSubmit={handleSubmit}
      className="flex items-end gap-3 rounded-2xl border border-zinc-200 bg-white/80 p-3 shadow-sm dark:border-zinc-800 dark:bg-zinc-900/80"
    >
      <textarea
        className="max-h-40 min-h-[3rem] flex-1 resize-none border-none bg-transparent text-sm text-zinc-900 outline-none placeholder:text-zinc-400 dark:text-zinc-50 dark:placeholder:text-zinc-500"
        placeholder="輸入訊息,按 Enter 送出(Shift+Enter 換行)"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onKeyDown={handleKeyDown}
        disabled={disabled}
      />
      <button
        type="submit"
        disabled={disabled || !value.trim()}
        className="inline-flex h-9 items-center rounded-full bg-blue-600 px-4 text-xs font-medium text-white shadow-sm transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-zinc-300 dark:disabled:bg-zinc-700"
      >
        發送
      </button>
    </form>
  );
}
💡 程式碼說明:
  • 使用 useState 管理輸入框的值
  • 按 Enter 鍵發送訊息,Shift+Enter 換行
  • disabled 為 true 時,無法發送訊息(防止重複請求)

6.4 更新主頁面

更新 src/app/page.tsx 檔案,整合聊天介面:

"use client";

import { useState } from "react";
import { ChatWindow, type ChatMessage } from "@/components/ChatWindow";
import { ChatInput } from "@/components/ChatInput";

export default function Home() {
  const [messages, setMessages] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleSend = async (text: string) => {
    const timestamp = new Date().toISOString();
    const userMessage: ChatMessage = {
      id: `${timestamp}-user`,
      role: "user",
      content: text,
      timestamp,
    };

    setMessages((prev) => [...prev, userMessage]);
    setIsLoading(true);
    setError(null);

    try {
      const res = await fetch("/api/chat", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          userMessage: text,
          messages,
        }),
      });

      if (!res.ok) {
        const data = await res.json().catch(() => null);
        const message =
          data?.error || data?.message || "伺服器發生未知錯誤,請稍後再試。";
        throw new Error(message);
      }

      const data: { reply: string } = await res.json();
      const aiTimestamp = new Date().toISOString();
      const aiMessage: ChatMessage = {
        id: `${aiTimestamp}-assistant`,
        role: "assistant",
        content: data.reply,
        timestamp: aiTimestamp,
      };
      setMessages((prev) => [...prev, aiMessage]);
    } catch (err) {
      console.error(err);
      const message =
        err instanceof Error ? err.message : "發送訊息失敗,請稍後再試。";
      setError(message);
    } finally {
      setIsLoading(false);
    }
  };

  const handleClear = () => {
    setMessages([]);
    setError(null);
  };

  return (
    <div className="flex min-h-screen items-center justify-center bg-gradient-to-b from-zinc-50 to-zinc-100 px-4 py-8 font-sans dark:from-zinc-900 dark:to-black">
      <main className="flex w-full max-w-4xl flex-1 flex-col gap-4 rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur dark:bg-zinc-950/80">
        <header className="flex flex-col gap-2 border-b border-zinc-200 pb-4 dark:border-zinc-800 sm:flex-row sm:items-center sm:justify-between">
          <div>
            <h1 className="text-xl font-semibold text-zinc-900 dark:text-zinc-50">
              Gemini Self Chatbot
            </h1>
            <p className="text-xs text-zinc-500 dark:text-zinc-400">
              基於 Google Gemini 的個人助理。你的訊息會在本服務端儲存以供日後查詢,請避免輸入敏感個資。
            </p>
          </div>
          <div className="flex items-center gap-2">
            <span className="rounded-full bg-emerald-100 px-3 py-1 text-xs font-medium text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200">
              部署準備完成 · GenAI deploy page
            </span>
            <button
              type="button"
              onClick={handleClear}
              className="rounded-full border border-zinc-200 px-3 py-1 text-xs text-zinc-500 transition hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
            >
              清除對話
            </button>
          </div>
        </header>

        {error && (
          <div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-200">
            {error}
          </div>
        )}

        <section className="flex min-h-0 flex-1 flex-col gap-3">
          <ChatWindow messages={messages} isLoading={isLoading} />
          <ChatInput onSend={handleSend} disabled={isLoading} />
        </section>

        <footer className="mt-1 flex flex-col gap-1 text-[10px] text-zinc-500 dark:text-zinc-500 sm:flex-row sm:items-center sm:justify-between">
          <span>使用 Google Gemini API · 僅於伺服器端存取 API Key。</span>
          <span>部署建議:搭配 Docker、GitHub Actions,自動化測試通過後再部署。</span>
        </footer>
      </main>
    </div>
  );
}
💡 程式碼說明:
  • "use client" 表示這是客戶端元件(可以使用 React Hooks)
  • 使用 useState 管理訊息列表、載入狀態和錯誤訊息
  • handleSend 函數負責發送訊息到後端 API
  • 使用 fetch API 與後端通訊

7. 建立後端 API

7.1 建立 API 資料夾結構

src/app 資料夾中建立 api/chat 資料夾:

# Windows
mkdir src\app\api\chat

# Mac/Linux
mkdir -p src/app/api/chat

7.2 建立 Gemini 客戶端

建立 src/lib 資料夾和 src/lib/geminiClient.ts 檔案:

# Windows
mkdir src\lib

# Mac/Linux
mkdir -p src/lib

然後建立 src/lib/geminiClient.ts

import "server-only";

type ChatRole = "user" | "assistant" | "system";

export type ChatMessageInput = {
  role: ChatRole;
  content: string;
};

export type GeminiReply = {
  replyText: string;
};

const DEFAULT_MODEL = process.env.GEMINI_MODEL_NAME || "gemini-pro";

export async function generateReplyFromGemini(
  messages: ChatMessageInput[],
): Promise {
  const apiKey = process.env.GOOGLE_GENAI_API_KEY;

  if (!apiKey) {
    throw new Error("伺服器尚未設定 GOOGLE_GENAI_API_KEY 環境變數。");
  }

  const userMessage = messages.findLast((m) => m.role === "user");

  if (!userMessage) {
    throw new Error("沒有可用的使用者訊息。");
  }

  const systemPrompt =
    "You are a helpful assistant for a personal Gemini-based chatbot. Respond in Traditional Chinese when possible.";

  const contents = [
    {
      role: "user",
      parts: [
        {
          text: `${systemPrompt}\n\nUser: ${userMessage.content}`,
        },
      ],
    },
  ];

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 30_000);

  try {
    const response = await fetch(
      `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(
        DEFAULT_MODEL,
      )}:generateContent`,
      {
        method: "POST",
        signal: controller.signal,
        headers: {
          "Content-Type": "application/json",
          "x-goog-api-key": apiKey,
        },
        body: JSON.stringify({
          contents,
        }),
      },
    );

    if (!response.ok) {
      const errorBody = await response.json().catch(() => null);
      const message =
        (errorBody as { error?: { message?: string } } | null)?.error
          ?.message ||
        `Gemini API 回應錯誤 (status ${response.status}).`;
      throw new Error(message);
    }

    const data = (await response.json()) as {
      candidates?: Array<{
        content?: {
          parts?: Array<{ text?: string }>;
        };
      }>;
    };

    const text =
      data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";

    if (!text) {
      throw new Error("Gemini 沒有回傳可用文字內容。");
    }

    return { replyText: text };
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      throw new Error("呼叫 Gemini API 逾時,請稍後再試。");
    }
    if (error instanceof Error) {
      throw error;
    }
    throw new Error("呼叫 Gemini API 時發生未知錯誤。");
  } finally {
    clearTimeout(timeoutId);
  }
}
💡 程式碼說明:
  • "server-only" 確保這個檔案只在伺服器端執行
  • 從環境變數讀取 API Key(不會暴露到前端)
  • 使用 AbortController 設定 30 秒超時
  • 完整的錯誤處理機制

7.3 建立聊天儲存功能

建立 src/lib/chatStore.ts 檔案,用於儲存聊天紀錄:

import "server-only";

import { promises as fs } from "fs";
import path from "path";

export type StoredMessage = {
  role: "user" | "assistant" | "system";
  content: string;
  timestamp: string;
};

export type ChatSession = {
  sessionId: string;
  createdAt: string;
  updatedAt: string;
  messages: StoredMessage[];
};

function getChatsDir() {
  return path.join(process.cwd(), "data", "chats");
}

export async function ensureChatsDirExists() {
  const dir = getChatsDir();
  try {
    await fs.mkdir(dir, { recursive: true });
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`無法建立聊天資料夾: ${error.message}`);
    }
    throw new Error("無法建立聊天資料夾(未知錯誤)。");
  }
}

export async function loadChat(sessionId: string): Promise {
  const filePath = path.join(getChatsDir(), `${sessionId}.json`);

  try {
    const content = await fs.readFile(filePath, "utf8");
    const data = JSON.parse(content) as ChatSession;
    return data;
  } catch (error) {
    if (
      error &&
      typeof error === "object" &&
      "code" in error &&
      (error as { code?: string }).code === "ENOENT"
    ) {
      return null;
    }
    if (error instanceof Error) {
      throw new Error(`讀取聊天紀錄失敗: ${error.message}`);
    }
    throw new Error("讀取聊天紀錄時發生未知錯誤。");
  }
}

export async function saveChat(session: ChatSession): Promise {
  await ensureChatsDirExists();
  const filePath = path.join(getChatsDir(), `${session.sessionId}.json`);

  const payload: ChatSession = {
    ...session,
    updatedAt: new Date().toISOString(),
  };

  try {
    await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`儲存聊天紀錄失敗: ${error.message}`);
    }
    throw new Error("儲存聊天紀錄時發生未知錯誤。");
  }
}

export function createEmptyChat(sessionId: string): ChatSession {
  const now = new Date().toISOString();
  return {
    sessionId,
    createdAt: now,
    updatedAt: now,
    messages: [],
  };
}

7.4 建立 API Route

建立 src/app/api/chat/route.ts 檔案,這是處理聊天請求的 API 端點:

import { NextRequest, NextResponse } from "next/server";
import { generateReplyFromGemini } from "@/lib/geminiClient";
import {
  ChatSession,
  StoredMessage,
  createEmptyChat,
  loadChat,
  saveChat,
} from "@/lib/chatStore";
import { randomUUID } from "crypto";

type RequestBody = {
  userMessage?: string;
  messages?: Array<{ role: "user" | "assistant" | "system"; content: string }>;
  sessionId?: string;
};

type ErrorResponse = {
  error: string;
};

export async function POST(req: NextRequest) {
  try {
    let body: RequestBody | null = null;

    try {
      body = (await req.json()) as RequestBody;
    } catch {
      const errorBody: ErrorResponse = { error: "無法解析請求內容。" };
      return NextResponse.json(errorBody, { status: 400 });
    }

    const userMessage = body?.userMessage?.trim();

    if (!userMessage) {
      const errorBody: ErrorResponse = { error: "userMessage 不可為空。" };
      return NextResponse.json(errorBody, { status: 400 });
    }

    const sessionId = body?.sessionId || randomUUID();

    let session: ChatSession | null;
    try {
      session = await loadChat(sessionId);
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "讀取聊天紀錄時發生錯誤。";
      const errorBody: ErrorResponse = { error: message };
      return NextResponse.json(errorBody, { status: 500 });
    }

    if (!session) {
      session = createEmptyChat(sessionId);
    }

    const now = new Date().toISOString();
    const newUserMessage: StoredMessage = {
      role: "user",
      content: userMessage,
      timestamp: now,
    };

    const historyMessages: StoredMessage[] = session.messages.concat(
      newUserMessage,
    );

    let replyText: string;
    try {
      const { replyText: text } = await generateReplyFromGemini(
        historyMessages.map((m) => ({
          role: m.role,
          content: m.content,
        })),
      );
      replyText = text;
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "呼叫 Gemini API 時發生錯誤。";
      const errorBody: ErrorResponse = { error: message };
      return NextResponse.json(errorBody, { status: 500 });
    }

    const assistantMessage: StoredMessage = {
      role: "assistant",
      content: replyText,
      timestamp: new Date().toISOString(),
    };

    const updatedSession: ChatSession = {
      ...session,
      messages: historyMessages.concat(assistantMessage),
    };

    try {
      await saveChat(updatedSession);
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "儲存聊天紀錄時發生錯誤。";
      const errorBody: ErrorResponse = { error: message };
      return NextResponse.json(errorBody, { status: 500 });
    }

    return NextResponse.json(
      {
        reply: replyText,
        sessionId,
      },
      { status: 200 },
    );
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "伺服器發生未知錯誤。";
    const errorBody: ErrorResponse = { error: message };
    return NextResponse.json(errorBody, { status: 500 });
  }
}
💡 程式碼說明:
  • 這是 Next.js 的 API Route Handler
  • 接收 POST 請求,處理聊天訊息
  • 自動管理 Session ID(如果沒有提供,會自動產生)
  • 完整的錯誤處理,回傳適當的 HTTP 狀態碼

7.5 更新 Layout 檔案

更新 src/app/layout.tsx 檔案,設定正確的標題和語言:

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Gemini Self Chatbot",
  description: "基於 Google Gemini 的個人助理 Chatbot",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="zh-TW">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}
💡 程式碼說明:
  • 更新了頁面標題和描述
  • 設定語言為繁體中文(zh-TW)
  • 這是 Next.js 的根 Layout,所有頁面都會使用這個設定

7.6 更新 .gitignore 檔案

更新 .gitignore 檔案,確保聊天紀錄不會被上傳到 Git:

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# chat data
/data/chats/*.json
💡 說明:
  • 最後兩行確保聊天紀錄不會被上傳到 Git
  • 環境變數檔案(.env*)也會被忽略,保護你的 API Key

8. 執行與測試

8.1 建立環境變數檔案

在專案根目錄建立 .env.local 檔案:

GOOGLE_GENAI_API_KEY=你的_API_Key_在這裡
GEMINI_MODEL_NAME=gemini-pro
⚠️ 重要:
  • 你的_API_Key_在這裡 替換成你在步驟 4 中取得的 API Key
  • .env.local 檔案已經在 .gitignore 中,不會被上傳到 Git
  • 不要將 API Key 分享給他人

8.2 啟動開發伺服器

在終端機中執行:

npm run dev
✅ 預期結果:你應該會看到類似以下的訊息:
▲ Next.js 16.1.3
- Local:        http://localhost:3000
- ready started server on 0.0.0.0:3000

8.3 開啟瀏覽器測試

  1. 開啟瀏覽器(Chrome、Firefox、Edge 等)
  2. 前往 http://localhost:3000
  3. 你應該會看到聊天介面
  4. 在輸入框中輸入訊息,例如:「你好,請介紹一下你自己」
  5. 按 Enter 或點擊「發送」按鈕
  6. 等待 AI 回應
✅ 如果一切正常:你應該會看到:
  • 你的訊息顯示在右側(藍色氣泡)
  • AI 的回應顯示在左側(灰色氣泡)
  • 對話紀錄會自動儲存在 data/chats/ 資料夾中

9. 常見問題排除

9.1 錯誤:伺服器尚未設定 GOOGLE_GENAI_API_KEY

原因:環境變數檔案沒有正確設定

解決方法:

  1. 確認 .env.local 檔案在專案根目錄
  2. 確認檔案內容格式正確(沒有多餘的空格)
  3. 重新啟動開發伺服器(按 Ctrl+C 停止,然後重新執行 npm run dev

9.2 錯誤:models/gemini-pro is not found

原因:模型名稱不正確或 API Key 沒有權限使用該模型

解決方法:

  1. 嘗試其他模型名稱,例如:
    • gemini-1.5-pro
    • gemini-1.5-flash
    • gemini-2.0-flash-exp
  2. .env.local 中更新 GEMINI_MODEL_NAME
  3. 重新啟動開發伺服器

9.3 錯誤:無法建立聊天資料夾

原因:檔案系統權限問題

解決方法:

  1. 確認專案資料夾有寫入權限
  2. 手動建立 data/chats 資料夾

9.4 頁面顯示空白或錯誤

原因:可能是程式碼錯誤或編譯問題

解決方法:

  1. 檢查終端機的錯誤訊息
  2. 確認所有檔案都已正確建立
  3. 確認檔案路徑和匯入路徑正確
  4. 嘗試清除快取:rm -rf .next(Mac/Linux)或 rmdir /s .next(Windows)

10. 部署到雲端

10.1 部署到 Vercel(推薦)

Vercel 是 Next.js 的官方部署平台,提供免費方案:

  1. 前往 https://vercel.com 並註冊帳號
  2. 點擊 "New Project"
  3. 連接你的 GitHub 帳號並選擇專案
  4. 在 "Environment Variables" 中新增:
    • GOOGLE_GENAI_API_KEY = 你的 API Key
    • GEMINI_MODEL_NAME = gemini-pro(可選)
  5. 點擊 "Deploy"
  6. 等待部署完成(約 2-3 分鐘)
✅ 完成後:你會獲得一個公開的網址,例如:https://your-project.vercel.app

10.2 使用 Docker 部署

如果你想要在自己的伺服器上部署,可以使用 Docker:

首先,建立 Dockerfile 檔案(已在專案中):

FROM node:20-alpine AS builder

WORKDIR /app

ENV NODE_ENV=production

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000

RUN addgroup -S nextjs && adduser -S nextjs -G nextjs

COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.* ./ || true
COPY --from=builder /app/tsconfig.json ./tsconfig.json

RUN mkdir -p /app/data/chats && chown -R nextjs:nextjs /app

USER nextjs

EXPOSE 3000

CMD ["npm", "start"]

建立 Docker 映像檔:

docker build -t gemini-chatbot .

執行 Docker 容器:

docker run -d -p 3000:3000 --env-file .env.local --name gemini-chatbot gemini-chatbot
⚠️ 注意:
  • 確保 .env.local 檔案存在且包含正確的環境變數
  • 如果使用雲端服務(如 AWS、GCP),記得設定安全群組允許 3000 埠

10.3 生產環境建置

在部署前,先測試生產環境建置:

npm run build

如果建置成功,可以啟動生產伺服器:

npm start
💡 說明:
  • npm run build 會建立最佳化的生產版本
  • npm start 會啟動生產伺服器(預設在 http://localhost:3000)
  • 生產版本效能更好,但開發時建議使用 npm run dev

🎉 恭喜!

你已經成功建立了一個完整的 AI 聊天機器人!

現在你可以:

教學文件建立日期:2026年1月

如有問題,請參考 Next.js 官方文件Google AI 文件