昨天完成「產生學習卡」表單後,App 終於可以知道使用者今天想學什麼。
測試時,我選擇:
7~8 歲兒童
浩瀚太空
3 張
React 也成功取得:
{
"ageGroup": "7~8 歲兒童",
"topic": "浩瀚太空",
"cardCount": 3
}
不過昨天按下「產生學習卡」後,這些資料最後只去了:
console.log(outputData);
如果要真的變成「AI 智慧學習卡」,總不能每次按下按鈕後,只在 Console 跟自己打招呼。
所以今天終於要跨出重要的一步:
把 React Form 收集到的資料送給 Gemini API,讓自己的 App 第一次真正取得 AI 產生的內容。
如果一路往後想,事情其實還很多。
Gemini 回來的資料要不要是 JSON?怎麼轉成 LearningCard[]?怎麼放回畫面?失敗怎麼處理?
但如果今天全部一起做,大概會直接把未來好幾天的進度一次做完。
所以今天先把範圍縮小成:
React Form
↓
Gemini API
↓
Gemini Response
先確認 App 能不能真的跟 Gemini 說上話。
Structured Output、JSON Schema,以及把 Response 接回 Learning Card UI,今天全部先不做。
我繼續在目前的 Google AI Studio 專案裡,請 Gemini 幫我完成 API 串接。
這次 Prompt 是:
目前 Learning Card App 已經有「產生學習卡」表單。
表單可以取得:
{
ageGroup,
topic,
cardCount
}
目前按下「產生學習卡」後,只會使用 console.log 輸出表單資料。
現在請幫我加入 Gemini API,讓按下「產生學習卡」後,可以把 ageGroup、topic、cardCount 組成簡單的 Prompt,送給 Gemini,並取得 Gemini 回傳內容。
例如:
ageGroup: "7~8 歲兒童"
topic: "浩瀚太空"
cardCount: 3
可以組成類似:
「請幫我產生適合 7~8 歲兒童的『浩瀚太空』學習卡,共 3 張。」
這一版的目標只需要確認 Gemini API 可以成功呼叫並取得 Response。
請:
- 使用 Google AI Studio 建議的 server-side Gemini API 整合方式
- API Key 必須保留在 server-side,不要寫入 React client code
- 保留目前 CardGeneratorForm
- 保留目前 oceanCards
- 不要修改 LearningCard 資料結構
- 不要修改其他既有學習卡資料
- 不要移除目前既有功能
這一版先不要:
- 使用 Structured Output
- 設定 JSON Schema
- 把 Response 轉成 LearningCard[]
- 用 Gemini Response 取代目前 oceanCards
- 修改目前 Learning Card UI
Gemini Response 可以先顯示在表單下方或輸出到 Console。
這次只完成:
Form Data
→ Gemini API
→ Gemini Response
經過前幾天的經驗,現在除了告訴 Gemini「要做什麼」,也開始習慣把「今天先不要做什麼」一起寫清楚。

這次使用 Gemini 3.8 Flash,大約執行了 125 秒。
它先檢查目前專案與 Gemini API 的使用方式,最後主要修改了:
server.ts
package.json
src/components/CardGeneratorForm.tsx
其中最大的改變,就是多了一層 Server。
原本 Day 17 的流程:
CardGeneratorForm
↓
console.log()
現在變成:
CardGeneratorForm
↓
POST /api/generate-cards
↓
server.ts
↓
Gemini API
↓
Gemini Response
這也讓我第一次比較具體地看到:
「在 React App 使用 Gemini」並不只是把 API Key 塞進前端然後 fetch()。

這次 Gemini 建立的 server.ts 會從 Server-side 取得:
process.env.GEMINI_API_KEY
而不是把 Gemini API Key 寫進 React Client。
概念上變成:
React Client
↓
/api/generate-cards
↓
Server
↓
GEMINI_API_KEY
↓
Gemini API
這一點很重要。
如果直接把 API Key 寫進前端程式,最後送到瀏覽器的 JavaScript 就可能讓使用者取得 Key。
因此需要保密的憑證留在 Server-side,由自己的 Server 代替前端呼叫 Gemini API。
前端只需要知道:
我要呼叫
/api/generate-cards。
至於 Gemini API Key,前端不需要知道。
Day 17 已經有:
ageGroup
topic
cardCount
今天則第一次把它們組成真正要送給 Gemini 的 Prompt。
目前版本非常簡單:
請幫我產生適合 {ageGroup} 的『{topic}』學習卡,共 {cardCount} 張。
例如:
{
"ageGroup": "7~8 歲兒童",
"topic": "浩瀚太空",
"cardCount": 3
}
就會變成:
請幫我產生適合 7~8 歲兒童 的『浩瀚太空』學習卡,共 3 張。
跟前面在 Google AI Studio 手動輸入 Prompt 最大的差別是:
這次 Prompt 的內容來自使用者操作 App 的結果。
API 串接完成後,我回到 Preview 實際測試。
第一組條件使用:
學習對象:7~8 歲兒童
學習主題:浩瀚太空
卡片數量:3 張
Client 先取得:
{
"ageGroup": "7~8 歲兒童",
"topic": "浩瀚太空",
"cardCount": 3
}
接著組成:
請幫我產生適合 7~8 歲兒童 的『浩瀚太空』學習卡,共 3 張。
Server 收到 Request 後,開始呼叫:
gemini-2.5-flash
最後 Console 終於出現:
[Client] 成功取得 Gemini Response
Server 也記錄:
[Server] 模型 gemini-2.5-flash 回應成功,字數: 1764
而且 Gemini 真的產生了三張與太空相關的內容,包括:
地球與月亮
太陽系
星星、銀河系與太空人
網站上也成功出現:
Gemini 回應成功!
到這裡,第一次完整的資料流程終於跑通了。
React Form
↓
Form Data
↓
Prompt
↓
Server
↓
Gemini API
↓
Gemini Response
↓
React
前面十幾天都是我自己到 Google AI Studio 按 Run。
今天第一次變成:
我的 App 幫我去問 Gemini。

不過只成功一次還不夠。
如果程式裡其實有什麼東西被寫死,我可能也不會發現。
所以第二次我把三個條件全部換掉:
學習對象:3~4 歲幼兒
學習主題:日常好習慣
卡片數量:7 張
Client 取得:
{
"ageGroup": "3~4 歲幼兒",
"topic": "日常好習慣",
"cardCount": 7
}
並組成新的 Prompt:
請幫我產生適合 3~4 歲幼兒 的『日常好習慣』學習卡,共 7 張。
這次 Gemini 也成功回傳內容。
Server Log 顯示:
[Server] 模型 gemini-2.5-flash 回應成功,字數: 1404
而且真的產生七張不同的日常習慣學習卡:
好好洗手手
刷刷牙齒亮晶晶
玩具回家囉
好好吃飯飯
我是小禮貌
準時睡覺覺
一起玩,真快樂
所以現在可以確認:
7~8 歲+浩瀚太空+3 張
↓
3 張太空內容
3~4 歲+日常好習慣+7 張
↓
7 張生活習慣內容
表單不只是裝飾。
使用者輸入的條件,真的已經開始影響 Gemini 產生的結果。


看到這裡,好像可以宣布:
Learning Card Generator 完成!
先等等。
把兩次 Gemini Response 放在一起看,就會發現一件很有趣的事情。
「浩瀚太空」那次,Gemini 自己設計的格式包含:
卡片主題
主要顏色
卡片插畫建議
學習內容
你知道嗎?
動動腦時間
但換成「日常好習慣」之後,又變成:
卡片名稱
說明文字
情境圖示建議
最後甚至自己多附上一段:
使用建議
Gemini 確實理解了:
給誰學
學什麼
要幾張
但是它不知道我的 React App 真正需要的是:
type LearningCard = {
id: string;
emoji: string;
title: string;
content: string;
question: string;
answer: string;
};
換句話說:
Gemini API 呼叫成功 ✓
主題有跟著表單改變 ✓
卡片數量有跟著表單改變 ✓
Response 可以直接變成
LearningCard[] ✗
這個問題其實有點熟悉。
前面 Day 09~Day 12 在 Google AI Studio 測試 Prompt 時,我就已經碰過類似問題:
人看得懂的 AI 回答,不代表程式好處理。
現在只是同一個問題,正式從 AI Studio 搬進自己的 App。
所以今天先不急著處理 Response 格式。
目前真正完成的是:
使用者
↓
Learning Card Form
↓
ageGroup
topic
cardCount
↓
Prompt
↓
POST /api/generate-cards
↓
Server
↓
Gemini API
↓
自然語言 Response
跟昨天相比,最大的差別只有最後幾步。
Day 17:
Form → console.log()
Day 18:
Form → Server → Gemini → Response
看起來只多了幾個箭頭,但這幾個箭頭終於讓目前的 Prototype 開始真的有 AI 能力。
這次畫面上其實同時出現兩個 Gemini Model 名稱。
協助我修改這個 App 的是:
Gemini 3.8 Flash
而 server.ts 實際呼叫、負責產生 Learning Card 內容的模型則是:
gemini-2.5-flash
兩者在這次實驗中扮演的角色不同。
可以把它理解成:
Gemini 3.8 Flash
→ 幫我開發 App
gemini-2.5-flash
→ 被我的 App 呼叫
→ 產生 Learning Card
這也是今天開始跟前面幾天不太一樣的地方。
Gemini 不再只是我開發時使用的工具,也開始成為 App 本身的一部分。
今天終於把昨天停在 Console 裡的:
{
"ageGroup": "3~4 歲幼兒",
"topic": "日常好習慣",
"cardCount": 7
}
真的送進 Gemini API。
而且經過兩組不同條件測試後,可以確認:
React Form
↓
動態學習條件
↓
Server-side API
↓
Gemini
↓
不同的學習內容
整條流程已經成功接通。
不過今天也同時發現兩個後面一定要處理的問題。
第一個是 API 不保證每次都成功,開發測試時就真的遇到了 503 UNAVAILABLE。
第二個則是更直接影響 Learning Card App:
Gemini 有回答,不代表 Gemini 回傳的資料可以直接給程式使用。
目前 Prompt 只有:
請幫我產生適合 {ageGroup} 的
『{topic}』學習卡,共 {cardCount} 張。
對 Gemini 來說,自由發揮的空間還非常大。
也因此兩次測試雖然都成功產生學習卡,回傳格式卻完全不一樣。
所以 API 接通之後,下一個問題已經很明顯了:
到底該怎麼把 Prompt 管理好?
今天只用一句:
請幫我產生適合 3~4 歲幼兒的
「日常好習慣」學習卡,共 7 張。
Gemini 就自己決定了卡片要有哪些欄位、要不要插畫建議,甚至連最後的使用建議都一起補上。
明天準備重新整理這段 Prompt。
把:
ageGroup
topic
cardCount
不只是塞進一句話,而是整理成比較完整、可維護的 Prompt Template。
看看當 Prompt 寫得更清楚之後,Gemini 的內容能不能更接近我們真正想要的 Learning Card。
至於「固定 JSON 格式」這件事——
前面學過的 Structured Output,也差不多該準備回來了。