前幾天我們都在 Google AI Studio 裡測試《喵語日誌》的功能:先設定貓咪 Persona,再加入多模態分析,最後使用 Response Schema 讓 Gemini 穩定輸出 JSON。
不過,AI Studio 裡測試成功,只代表概念驗證完成。真正要做成一個可以操作的應用程式,還是要把這些設定搬進前端專案裡。
所以今天要正式進入實作階段,完成這條完整流程:
使用者輸入近況 → 前端呼叫 Gemini API → Gemini 回傳結構化 JSON → React 即時渲染貓咪日誌卡片
接下來就從建立 Vite + React 專案開始吧!
首先使用 Vite 建立一個 React + TypeScript 專案:
npm create vite@latest cat-diary -- --template react-ts
cd cat-diary
npm install
npm install @google/genai
npm run dev
啟動完成後,瀏覽器打開終端機顯示的網址,就可以看到 Vite 的初始畫面。
接著,我在 VS code 中把 src 資料夾整理成以下結構:
src/
├── components/
│ └── DiaryCard.tsx
├── services/
│ └── geminiService.ts
├── types/
│ └── diary.ts
├── App.tsx
└── main.tsx
這樣分工之後,每個檔案都有比較清楚的責任:
components:負責畫面元件services:負責 API 呼叫types:負責 TypeScript 型別定義App.tsx:負責頁面狀態與互動流程為了避免直接把 API Key 寫在程式碼中,我們先使用 Vite 的環境變數機制管理本機設定。
在專案根目錄建立 .env.local 檔案:
VITE_GEMINI_API_KEY=your_gemini_api_key_here
接著確認 .gitignore 中包含:
.env
.env.*
!.env.example
這樣可以避免不小心把本機環境設定檔推送到 Git 儲存庫。Vite 也建議將本機使用的 .env.local 類型檔案加入 Git 忽略清單。
在前端程式中,可以透過以下方式讀取:
const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
這次先使用環境變數完成本機練習,避免將金鑰直接寫死在程式碼裡。
不過要特別注意,因為 VITE_ 開頭的變數會被打包到瀏覽器端,所以這種方式適合學習和 Demo;正式上線時,仍應將 API 呼叫移到後端。
Day 05 的 JSON Schema 有三個欄位,因此我們可以在 src/types/diary.ts 中建立對應的型別:
// src/types/diary.ts
export interface CatDiaryResponse {
mood_score: number;
lifestyle_label: string;
cat_response: string;
}
這樣做的好處是,當 API 回傳資料時,TypeScript 可以幫我們檢查欄位名稱和資料型別,減少前端寫錯的機會。
接著建立 src/components/DiaryCard.tsx:
import type { CatDiaryResponse } from "../types/diary";
interface DiaryCardProps {
data: CatDiaryResponse;
}
export function DiaryCard({ data }: DiaryCardProps) {
return (
<div className="diary-card">
<div className="card-header">
{/* 顯示生活場景標籤 */}
<span>🏷️ {data.lifestyle_label}</span>
{/* 顯示情緒指數 */}
<span>情緒指數:{data.mood_score} / 10</span>
</div>
{/* 顯示貓咪陪伴文字 */}
<div className="card-content">
🐾 {data.cat_response}
</div>
</div>
);
}
這個元件會接收一個 CatDiaryResponse 物件,然後把三個欄位分別顯示成:
在 src/services/geminiService.ts 中封裝 Gemini API:
import { GoogleGenAI, Type } from "@google/genai";
import type { CatDiaryResponse } from "../types/diary";
const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
const ai = new GoogleGenAI({ apiKey });
export async function analyzeDiary(userText: string): Promise<CatDiaryResponse> {
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: userText,
config: {
systemInstruction: "你是一隻名為「喵喵」的貼心寵物貓...",
responseMimeType: "application/json",
// 強制要求 Gemini 輸出符合結構的 JSON
responseSchema: {
type: Type.OBJECT,
properties: {
mood_score: { type: Type.INTEGER, description: "1 到 10 的情緒指數" },
lifestyle_label: { type: Type.STRING, description: "生活場景標籤" },
cat_response: { type: Type.STRING, description: "貓咪陪伴回應" },
},
required: ["mood_score", "lifestyle_label", "cat_response"],
},
},
});
const jsonText = response.text;
if (!jsonText) throw new Error("未取得 Gemini 回應");
return JSON.parse(jsonText) as CatDiaryResponse;
}
這裡最重要的設定是:
responseMimeType: "application/json"
以及:
responseSchema: {
...
}
前者要求模型使用 JSON 格式回應,後者則定義 JSON 必須包含哪些欄位和型別。Gemini 官方文件也示範了使用 responseSchema 搭配 Type.OBJECT、Type.STRING 和 Type.INTEGER 來限制輸出結構。
這次範例使用的模型 ID 是:
"gemini-3-flash-preview"
Gemini 模型名稱會隨版本和平台更新,實作時最好以 Google AI Studio 或官方模型清單中顯示的正式 ID 為準,不要自行猜測模型名稱。gemini-3-flash-preview 是官方列出的模型代碼。
接著修改 src/App.tsx:
import { useState } from "react";
import { DiaryCard } from "./components/DiaryCard";
import { analyzeDiary } from "./services/geminiService";
import type { CatDiaryResponse } from "./types/diary";
function App() {
const [inputText, setInputText] = useState("");
const [loading, setLoading] = useState(false);
const [diaryData, setDiaryData] = useState<CatDiaryResponse | null>(null);
const [errorMessage, setErrorMessage] = useState("");
// 表單送出處理
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!inputText.trim()) return;
setLoading(true);
setErrorMessage("");
try {
const result = await analyzeDiary(inputText);
setDiaryData(result); // 儲存 API 回傳的結構化資料
} catch (error) {
setErrorMessage("喵喵暫時沒聽懂,請稍後再試!");
} finally {
setLoading(false);
}
}
return (
<main>
<h1>🐾 喵語日誌</h1>
<form onSubmit={handleSubmit}>
<textarea value={inputText} onChange={(e) => setInputText(e.target.value)} />
<button type="submit" disabled={loading}>
{loading ? "喵喵正在思考中..." : "送出給喵喵"}
</button>
</form>
{/* API 分析成功後渲染卡片 */}
{diaryData && <DiaryCard data={diaryData} />}
</main>
);
}
export default App;
這裡使用三個 useState:
inputText:保存文字輸入loading:控制送出後的載入狀態diaryData:保存 Gemini 回傳的 JSON 資料errorMessage:顯示 API 失敗時的提示整個互動流程就完成了:
輸入文字
↓
點擊送出
↓
loading = true
↓
呼叫 analyzeDiary()
↓
取得 JSON
↓
更新 diaryData
↓
DiaryCard 重新渲染
這次串接過程中,我遇到兩個問題。
從純 .ts 檔案匯入 Interface 時,建議使用:
import type { CatDiaryResponse } from "../types/diary";
因為 Interface 只存在於編譯階段,並不是執行時真正需要載入的 JavaScript 物件。加上 type 後,TypeScript 對這類匯入的判斷會更清楚。
如果 VS Code 還是顯示奇怪的紅字,也可以執行:
TypeScript: Restart TS Server
有時候只是編輯器的型別快取還沒有更新。
如果 API 回傳 404 Not Found,很可能是模型名稱不存在、拼錯,或目前的 API 版本不支援。
例如:
model: "gemini-2.5-flash"
不一定在所有時期或環境都能直接使用。實作時要以 AI Studio 或官方模型清單中的模型 ID 為準。
這次輸入的內容是:
今天沒發生什麼特別的事,但是完成前端專案骨架建置,眼睛有點酸,只想躺在床上發呆。
點擊送出後,Gemini 成功回傳結構化資料,前端也順利渲染出日誌卡片:
從前幾天的 AI Studio 測試,到今天正式讓會說貓語的 AI 躍上網頁,我們順利完成了專案骨架、型別對接、結構化輸出與 API 串接!看著文字輸入後即時變成貓咪卡片,真的非常有成就感。
💡 提醒:本篇的前端直連方式適合學習與 Demo,若要正式上線,建議將 API 呼叫移至後端或 Cloud Functions,避免金鑰暴露於瀏覽器中。
明天將為第一週進行總整理!我們會彙整這幾天從 Prompt 到 API 串接的成果,並用 AI 工具產出《喵語日誌》三大核心畫面(對話、月曆、成就卡)的 Wireframe 原型,準備迎接下一階段的 Vibe Coding 實戰!喵~