昨天終於把 Day 12 透過 Structured Output 產生的五張海洋動物學習卡,成功放進 React UI。
現在資料是真的了,畫面也可以正常顯示。
不過馬上又遇到下一個問題。
目前想換學習內容,還是得自己去修改程式。
假設今天不想學海洋動物,而是想改成:
7~8 歲兒童
浩瀚太空
3 張學習卡
難道每次都要重新修改 learningCards.ts 嗎?
這樣好像還不能算是一個真正的「智慧學習卡產生器」。
所以今天先不急著串 Gemini API,而是先解決前端的第一個問題:
讓使用者自己告訴 App:「今天想學什麼?」
回頭看前面幾天使用的 Prompt,其實一直都有幾個重要條件:
學習對象:3~4 歲幼兒
學習主題:海洋動物
卡片數量:5 張
以前這些條件都是直接寫在 Prompt 裡。
既然現在要慢慢把它做成 Web App,那第一步就是把這些條件搬到畫面上。
這次我先留下三個最基本的設定:
學習對象
學習主題
卡片數量
預期最後可以得到類似:
{
"ageGroup": "3~4 歲幼兒",
"topic": "海洋動物",
"cardCount": 5
}
不過今天先做到這裡就好。
先不串 Gemini API。
按下「產生學習卡」後,只要能把這三個值正確取得並輸出到 Console,就算成功。
經過 Day 15 和 Day 16,我已經稍微認識 Gemini 的熱心程度了(笑)。
Day 15 原本只是希望它建立 Learning Card UI,結果 Make this an app 不只建立了完整 Prototype,還自己加入:
語音朗讀
星星獎勵
挑戰模式
多種學習主題
parentTip
soundName
pinyin
甚至連原本的 LearningCard Data Contract 都一起擴充了。
所以這次除了描述功能,我也特別限制修改範圍。
我給 Gemini 的 Prompt 是:
目前 Learning Card App 已經可以顯示學習卡。
其中海洋動物的 oceanCards 已經替換成前面透過 Structured Output 產生的 5 張 Learning Card,請保留目前既有的資料與功能,不要修改 oceanCards。
現在我希望新增一個「產生學習卡」表單,讓使用者可以設定學習條件。
表單需要包含:
1. 學習對象
- 3~4 歲幼兒
- 5~6 歲幼兒
- 7~8 歲兒童
2. 學習主題
- 使用文字輸入
- 預設值為「海洋動物」
3. 卡片數量
- 可以選擇 3、5、7 張
- 預設為 5 張
4. 一個「產生學習卡」按鈕
這一版先不要串接 Gemini API。
按下「產生學習卡」後,只需要使用 console.log 輸出:
{
ageGroup,
topic,
cardCount
}
請保留目前 Learning Card App 原本的功能。
不要修改:
- oceanCards
- LearningCard 資料結構
- 其他既有學習卡資料
- 現有學習卡功能
只新增這次需要的表單功能。
跟之前相比,這次 Prompt 不只是 Requirement,也開始加入 Scope。
簡單來說就是:
告訴 AI 要做什麼,也告訴它這次先不要做什麼。
這次使用 Gemini 3.8 Flash 執行,大約花了 80 秒。
Action history 顯示只修改了兩個檔案:
src/components/CardGeneratorForm.tsx
src/LearningCardApp.tsx
其中新增的:
CardGeneratorForm.tsx
就是今天主要的表單 Component。
比較重要的是,這次 Gemini 有遵守前面設定的範圍:
oceanCards 沒有修改
LearningCard Type 沒有修改
其他學習卡資料 沒有修改
Gemini API 沒有串接
既有 Learning Card 功能 保留
至少這次沒有又突然多出一個新的 Data Contract。

雖然 Gemini 這次有守住功能範圍,但在 UI 上還是忍不住多做了一點。
原本我只要求「學習對象」可以選擇三種年齡,它最後做成了三個選項:
🐣 3~4 歲幼兒
🐥 5~6 歲幼兒
🦅 7~8 歲兒童
「學習主題」除了文字輸入框之外,還自己增加了快速選擇:
海洋動物
森林恐龍
交通工具
美味水果
浩瀚太空
日常好習慣
卡片數量則做成:
3 張 5 張 7 張
輕巧探索 標準推薦 深度體驗
也就是說,Gemini 有遵守「不要串 API、不要改資料」這些功能邊界,但 UI 要怎麼呈現,它還是做了自己的設計決策。
這跟 Day 15 的結果有點像,只是這次 Scope 已經收斂很多。

回到程式本身,今天真正需要的事情其實不複雜。
表單需要記住三個值:
ageGroup
topic
cardCount
也就是:
使用者選了誰要學?
使用者今天想學什麼?
使用者想產生幾張?
當使用者修改表單時,React State 會跟著更新。
最後按下「產生學習卡」,再把目前的資料整理成:
const outputData = {
ageGroup,
topic: topic.trim() || '海洋動物',
cardCount,
};
目前還沒有 API,所以接下來只做:
console.log(outputData);
整個流程其實就是:
Form
↓
React State
↓
handleSubmit()
↓
outputData
↓
console.log()
今天的目標不是產生卡片,而是先確認 App 能不能正確知道使用者的需求。
只測預設值還不太夠。
如果畫面預設就是:
3~4 歲幼兒
海洋動物
5 張
最後 Console 也印出相同內容,還不能完全確認表單互動是不是正常。
所以這次我刻意把三個條件都換掉。
改成:
7~8 歲兒童
改成:
浩瀚太空
改成:
3 張
最後按下:
✨ 產生學習卡
Console 成功得到:
{
"ageGroup": "7~8 歲兒童",
"topic": "浩瀚太空",
"cardCount": 3
}
三個值都和畫面上的選擇一致。

也就是說,目前的資料流程已經可以做到:
使用者操作 Form
↓
React State 更新
↓
按下「產生學習卡」
↓
取得目前設定
↓
{
ageGroup: "7~8 歲兒童",
topic: "浩瀚太空",
cardCount: 3
}
到這裡,Day 17 的目標就完成了。
這裡要特別說明一下。
雖然按鈕上已經很有氣勢地寫著:
✨ 產生學習卡
但它現在其實還不會產生任何新的 Learning Card。
因為今天刻意沒有串 Gemini API。
目前按下按鈕後,資料只走到:
console.log(outputData);
所以即使我選:
7~8 歲兒童
浩瀚太空
3 張
App 也不會突然生成三張太空學習卡。
這一版真正完成的是:
把使用者的學習需求,從 UI 轉換成程式可以使用的資料。
而這三個值,正好就是接下來要交給 Gemini 的原料。
今天除了完成 Form,我覺得另一個比較有意思的地方,是跟 Gemini 協作方式的改變。
Day 15 比較像:
我要 Learning Card UI
↓
Gemini 自由發揮
↓
得到一個比預期大很多的 Prototype
Day 17 則變成:
我要新增 Form
要做:
ageGroup
topic
cardCount
console.log
不要做:
Gemini API
修改 oceanCards
修改 LearningCard
修改其他既有功能
最後 Gemini 雖然還是在 UI 上加入快速選項、成功提示等自己的設計,但至少沒有再次修改核心資料結構,也沒有提前把下一天的 Gemini API 一起做掉。
對我來說,這也是使用 AI 寫程式時開始慢慢學到的一件事:
Prompt 不只是描述想要什麼,也可以拿來界定這一次工作的邊界。
尤其當專案開始有既有程式碼之後,「哪些東西不要動」有時候跟「要新增什麼」一樣重要。
昨天我們把真正的 Learning Card Data 接進 React。
今天則往前走了一步,把原本寫死在 Prompt 裡的:
學習對象
學習主題
卡片數量
搬到 UI 上。
現在使用者已經可以自己決定:
「今天想學什麼?」
而 React 也可以取得:
{
"ageGroup": "7~8 歲兒童",
"topic": "浩瀚太空",
"cardCount": 3
}
如果把目前進度接起來,大概變成:
使用者
↓
Learning Card Form
↓
ageGroup
topic
cardCount
↓
console.log()
不過問題也很明顯。
資料都準備好了,結果最後只是印在 Console。
那接下來呢?
當然就是把它真的送出去。
明天準備把今天取得的:
ageGroup
topic
cardCount
真正交給 Gemini。
讓目前的:
React Form
↓
console.log()
開始變成:
React Form
↓
使用者學習條件
↓
Gemini API
↓
Learning Card Response
前面十幾天一直都在 Google AI Studio 裡手動按 Run。
接下來,終於要讓自己的 React App 開始跟 Gemini 說話了。