昨天 Day 21,我終於把 Gemini Structured Output 真正接進 React。
原本停在 Console 裡的 JSON,現在已經可以:
Gemini Response
→ response.cards
→ LearningCard[]
→ React State
→ Learning Card UI
輸入不同的學習對象、主題與卡片數量後,Gemini 產生的內容就會直接變成畫面上的學習卡。
不過真的把 Gemini API 接進 App 後,還有另一個很實際的問題:
Gemini 不一定每次都會立刻成功。
如果 API 還在處理,使用者需要知道目前正在等待;如果 API 呼叫失敗,也不能讓整個 App 跟著壞掉。
所以今天先不增加新的 AI 功能,而是回頭檢查並補強 Loading 與 Error Handling。
一開始整理今天的程式時,我才發現 Day 21 其實已經有:
const [isLoading, setIsLoading] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
而且送出表單時也已經會:
setIsLoading(true);
setErrorMessage(null);
最後透過:
finally {
setIsLoading(false);
}
把 Loading 關掉。
表單上的年齡、主題、卡片數量與產生按鈕,也已經搭配:
disabled={isLoading}
所以 Day 22 並不是「從零加入 Loading」。
比較準確地說,今天要做的是:
把原本已經存在的 Loading / Error 流程補強,並且真的把 Success、Loading、Error 三種情況跑過一次。
今天主要確認三件事情:
正常產生
→ Loading → Success → 更新 Cards
產生途中
→ 鎖定生成表單 → 避免重複送出
產生失敗
→ Loading → Error → 保留原本 Cards
也就是開始處理 Gemini API 不只「成功」的情況。
這次仍然使用 Google AI Studio 的 Gemini 3.8 Flash。
我的重點是要求它保留 Day 21 已經完成的資料流程,不要重新修改 Structured Output、Prompt Template 或 LearningCard[]。
主要希望補強:
Loading 狀態
Error 狀態
防止重複 Submit
錯誤訊息處理
API 失敗時保留既有 Cards

Gemini 執行結果:
Gemini 3.8 Flash
Ran for 91s
Edited 1 file
src/components/CardGeneratorForm.tsx
Built
這次只修改一個 Component。

比對 Day 21 與 Day 22 後,isLoading 與 errorMessage 本來就已經存在。
這次比較明顯的改動,是增加流程 Log:
console.log('Submit');
console.log('Loading');
console.log('Success');
console.log('Error');
讓我可以直接從 Console 觀察:
Submit
→ Loading
→ Success
或:
Submit
→ Loading
→ Error
另外也補上 Response 解析失敗的處理:
let data: any;
try {
data = await res.json();
} catch {
throw new Error(`伺服器回應格式異常 (${res.status})`);
}
Error Message 也增加了網路錯誤、503、API Key、必要參數等不同情況的處理。
Loading 時按鈕則會顯示:
AI 正在產生學習卡...
接著就實際測看看。

第一組測試:
學習對象:5~6 歲幼兒
學習主題:交通工具
卡片數量:3

送出後,畫面先進入:
AI 正在產生學習卡...

Console 則依序出現:
Submit
Loading
Success
接著 Day 21 的資料流程也正常執行:
Gemini Structured Response
→ response.cards
→ LearningCard[]
→ React State
最後成功產生:
🚗 小汽車
🚌 公車
🚂 火車
畫面更新成:
✨ 交通工具 1 / 3
代表補強 Loading / Error Handling 後,原本 Gemini → React 的流程仍然正常。

接著我在 Gemini 還沒回應時,故意嘗試操作表單。
結果 Loading 期間:
再次產生學習卡 ❌
切換學習對象 ❌
修改 Topic ❌
快速選擇主題 ❌
切換 3 / 5 / 7 張 ❌
這部分其實 Day 21 就已經透過 disabled={isLoading} 實作,今天則是真的把它測出來。
不過最上方的「探索卡」、「大挑戰」仍然可以使用。
這讓我發現:
Loading 不代表整個 App 都必須鎖住,只需要限制會影響這次 Gemini Request 的操作。

正常流程確認完後,我想知道 Error Handling 到底是不是真的有用。
所以我暫時把 Server 使用的模型改成一個不存在的名稱:
const candidateModels = ['gemini-day22-error-test'];

接著送出:
學習對象:7~8 歲兒童
學習主題:人體器官
卡片數量:5

這次流程變成:
Submit
→ Loading
→ Gemini API
→ 404 NOT_FOUND
→ Error
Console 收到:
models/gemini-day22-error-test is not found
status: NOT_FOUND
但 App 沒有 Crash,Loading 也正常結束,畫面成功進入 Error State:
學習卡產生失敗
代表至少:
Gemini API 失敗時,前端確實有把錯誤接住。

這次我也特別回到 Learning Card 畫面確認。
剛才 Test A 成功產生的:
交通工具 3 / 3
仍然存在,原本的「飛機」卡片也可以正常顯示。

所以目前的資料更新方式可以理解成:
Gemini Success
→ 更新 LearningCard[]
→ 更新 React State
Gemini Error
→ 顯示 Error
→ 保留原本 React State
也就是說,一次 API Failure 不會把上一組成功產生的教材一起清空。
對實際使用來說,這個行為合理很多。
Gemini 3.8 Flash 原本表示已經將錯誤轉成比較友善的訊息。
但 Test C 實際遇到 404 NOT_FOUND 時,畫面仍然出現了一大段 Gemini API 原始訊息:
models/gemini-day22-error-test is not found...
status: NOT_FOUND
回頭看程式才發現,目前有針對網路問題、503、API Key 等情況處理,但沒有特別處理這次的 404。
所以目前比較準確的狀態是:
Error Handling 有了,但 Error UX 還有改善空間。
這也再次證明,AI 告訴我「功能完成」,跟實際把 Error Case 跑過一次,還是兩件不同的事情。
Day 22 沒有增加新的 AI 功能,而是讓原本的 Gemini API 流程更完整。
目前整條流程已經變成:
Form Data
→ Prompt Template
→ Gemini API
│
├─ Success
│ ↓
│ Structured Output
│ ↓
│ LearningCard[]
│ ↓
│ React State
│ ↓
│ Learning Card UI
│
└─ Error
↓
Error State
↓
保留原本 Cards
從 Day 18 第一次把 Gemini API 接進 App,到現在已經不只是:
API 有回應就算成功。
而是開始處理真正使用 AI App 時一定會遇到的:
等待、重複操作、API Failure,以及既有資料保留。
今天最有感的其實不是新增多少程式碼,而是:
不要只測 AI 成功的時候。
如果今天只跑 Test A,我可能會覺得 Loading / Error Handling 已經沒有問題。
但真的故意讓 Gemini API 失敗一次後,才發現:
Error State ✅
App 沒有 Crash ✅
原本 Cards 保留 ✅
Error Message ⚠️
這種問題只看 AI 產生的程式碼,其實不一定看得出來。
還是得真的跑一次。
現在 Learning Card 已經可以:
輸入條件
→ Gemini 產生內容
→ Structured Output
→ LearningCard[]
→ 顯示在 UI
Loading 與 Error 也開始有基本的處理。
但還有一個很常見的情況:
Gemini 成功產生了,但這組內容我不喜歡怎麼辦?
目前只能重新回到表單再產生一次。
所以明天要來處理「重新產生」這件事,讓使用者不需要重新設定所有條件,就能請 Gemini 再生成另一組內容。