來到第 30 天,我終於將幼兒互動認知學習卡正式發布。
正式網站:
幼兒互動認知學習卡(Toddler Learning Cards)
不過,最後一天的工作不只是按下 Publish。
在發布以前,我重新檢查了輸入資料、重新產生功能、瀏覽器 Console、API Response 與後端日誌,確認開發階段使用的除錯資料不會不必要地出現在正式環境。
今天完成的工作包括:
原本產生學習卡表單已經提供固定的年齡與卡片數量選項,但如果有人跳過畫面、直接呼叫 API,還是可能送出錯誤或過大的資料。
因此,我先建立前後端一致的輸入規則。
| 欄位 | 限制 |
|---|---|
ageGroup |
只能是 3~4 歲、5~6 歲或 7~8 歲 |
topic |
去除前後空白後不可為空,最多 10 個字元,不可包含換行或控制字元 |
cardCount |
只能是整數 3、5 或 7 |
| Request Body | 最大 2 KB |
主題輸入框加入 10 個字元的輸入上限,畫面上也會顯示目前字數:
{Array.from(topic.trim()).length} / 10
送出表單以前,前端會再次檢查:
驗證失敗時,畫面會顯示繁體中文錯誤訊息,而且不會呼叫後端 API。
圖 1:Gemini 修改產生學習卡的前後端輸入限制。
前端限制可以改善操作體驗,但仍可能被繞過,所以 /api/generate-cards 必須重新驗證一次。
例如主題驗證:
const trimmedTopic = topic.trim();
if (!trimmedTopic || Array.from(trimmedTopic).length > 10) {
return res.status(400).json({
error: '輸入資料不符合規定。',
});
}
卡片數量驗證:
const VALID_CARD_COUNTS = [3, 5, 7];
if (
typeof cardCount !== 'number' ||
!Number.isInteger(cardCount) ||
!VALID_CARD_COUNTS.includes(cardCount)
) {
return res.status(400).json({
error: '輸入資料不符合規定。',
});
}
後端同時將 JSON Request Body 限制為 2 KB。
超過限制時,伺服器會停止處理並回傳 HTTP 413:
{
"error": "請求內容過大。"
}
只有通過後端驗證的資料,才會進入 Prompt Builder 並呼叫 Gemini API。
接著,我發現一個容易讓使用者困惑的 UI 問題。
操作流程如下:
問題原因是按鈕文字與實際執行邏輯讀取了不同資料:
lastGenerateParams。currentTheme。圖 2:目前選擇太陽系,重新產生按鈕卻仍顯示旅遊英文。
修改後,先取得目前主題:
const activeTopic =
currentTheme.generationParams?.topic || currentTheme.name;
按鈕文字改為:
<span>
重新產生學習卡
{activeTopic ? `(${activeTopic})` : ''}
</span>
handleRegenerate 也使用相同的 activeTopic:
const targetParams = {
ageGroup:
currentTheme.generationParams?.ageGroup ||
lastGenerateParams?.ageGroup ||
'3~4 歲幼兒',
topic: activeTopic.trim(),
cardCount: validCount,
};
現在切換到哪個主題,按鈕顯示與實際產生內容就會指向同一個主題。
如果目前選擇的是 Preset Theme,重新產生後會建立對應的 Generated Theme,不會覆蓋原本的內建卡片。
圖 3:修正後,重新產生按鈕與目前主題一致。
除了在對話區輸入程式修改 Prompt,Google AI Studio 也提供 Edit Tool。
我可以先在預覽畫面選取 ThemeSelector,再輸入:
一行只顯示三個主題按鈕,其餘部分不要修改。
Gemini 便會針對選取的元件調整版面,讓主題按鈕以較整齊的兩排顯示。
這種修改方式對不熟悉元件位置與 CSS 的使用者很方便,但完成後仍然要實際檢查:
圖 4:Google AI Studio 的 Edit Tool。
圖 5:選取 ThemeSelector 後,以自然語言調整主題按鈕排列。
在開發階段,為了追蹤資料流程,我曾經在前端 Console 顯示:
LearningCard[]。這些資料對開發很有幫助,但正式發布後,訪客只要開啟瀏覽器開發工具,就能查看前端 Console。
因此,我先移除不必要的前端 Debug Log,只保留不含 Prompt 與卡片內容的必要錯誤訊息。
不過,進一步檢查後,我發現只整理 Console 還不夠。
修改以前,後端會將下列資料一起送回瀏覽器:
return res.json({
success: true,
prompt,
data: structuredData,
response: responseText,
});
即使前端刪除了所有 console.log,訪客仍可從 Network 面板看到:
修改後,API 只回傳畫面真正需要的內容:
return res.json({
success: true,
data: structuredData,
});
Prompt Builder 也從前端移除,只保留在後端。
現在前端只送出:
{
"ageGroup": "5~6 歲幼兒",
"topic": "浩瀚太空",
"cardCount": 5
}
伺服器驗證資料、建立 Prompt、呼叫 Gemini,再將結構化卡片送回前端。
後端 Console 不會直接出現在訪客的瀏覽器,但它可能保存在 Cloud Run 或其他雲端平台的歷史日誌中。
原本後端會輸出:
修改後不再記錄完整內容,只保留操作指標,例如:
學習卡產生成功
使用模型:gemini-2.5-flash
處理時間:xxxx ms
卡片數量:5
這樣仍能確認服務是否正常,卻不會把使用者輸入與完整卡片內容留在雲端日誌。
完成修改後,我使用 Chrome DevTools 的 Network 面板重新產生學習卡。
實際 Response 為:
{
"success": true,
"data": {
"cards": [
{
"emoji": "☀️",
"title": "太陽亮晶晶",
"content": "太陽是我們地球的大燈泡……",
"question": "太陽會發光又發熱嗎?",
"answer": "是的,太陽會發光又發熱。"
}
]
}
}
檢查結果:
200 OK。success 與 data。responseText。cardCount 相符。Network 中仍然可以看到 Generated Cards,這是正常的,因為瀏覽器必須取得資料才能顯示卡片。
資料最小化不是讓 Network 完全沒有資料,而是只傳送功能真正需要的內容。
圖 6:在 Network 面板確認 API Response 不再包含 Prompt。
圖 7:Console 中已沒有 App 的 Prompt、Structured Response 與 React State。
Console 畫面仍可能顯示 React DevTools 或 Chrome 本身的提示,這些不是 Learning Card App 主動輸出的資料。
完成輸入限制、資料回傳整理與實際測試後,我在 Google AI Studio 開啟 Publish 面板,開始發布 App。
發布過程會依序進行:
圖 8:Google AI Studio 正在發布 App。
發布完成後,狀態顯示為:
Ready
Google AI Studio 同時提供正式網址:
https://toddler-learning-cards.ai.studio
圖 9:App 發布成功,狀態顯示 Ready。
這次發布也讓我第一次完整走過:
開發
↓
人工測試
↓
輸入限制
↓
資料暴露檢查
↓
Build
↓
Publish
↓
正式網址驗證
發布成功不代表後續不需要維護,但它代表這 30 天累積的功能,已經從 AI Studio 預覽畫面,變成可以透過獨立網址開啟的實際 App。
| 編號 | 測試項目 | 結果 |
|---|---|---|
| D30-01 | 空白主題不可送出 | ✅ |
| D30-02 | 10 個字元主題可以送出 | ✅ |
| D30-03 | 11 個字元主題被阻擋 | ✅ |
| D30-04 | 含換行或控制字元的主題被阻擋 | ✅ |
| D30-05 | 非法 ageGroup 回傳 HTTP 400 |
✅ |
| D30-06 | 非法 cardCount 回傳 HTTP 400 |
✅ |
| D30-07 | 超過 2 KB 的 Request Body 回傳 HTTP 413 | ✅ |
| D30-08 | 合法條件可正常產生卡片 | ✅ |
| D30-09 | Network Response 不含 Prompt 與原始 Response | ✅ |
| D30-10 | Console 不顯示 App 的 Prompt 與卡片 State | ✅ |
| D30-11 | 重新產生按鈕與目前主題一致 | ✅ |
| D30-12 | 專案 Build 成功 | ✅ |
| D30-13 | Google AI Studio Publish 狀態為 Ready | ✅ |
| D30-14 | 正式網址可以開啟 | ✅ |
themeId + cardId 鎖定編輯目標。使用者選擇年齡、主題、卡片數量
↓
前端表單驗證
↓
POST /api/generate-cards
↓
後端再次驗證輸入資料
↓
Server-side Prompt Builder
↓
Gemini Structured Output
↓
後端只回傳 data.cards
↓
轉換為 LearningCard[]
↓
加入或更新 AppTheme[]
↓
ThemeSelector 顯示 Generated Theme
↓
自動儲存至 LocalStorage
↓
探索卡/大挑戰/Human Edit/列印 PDF
這條流程也說明了,AI App 不能只有一段 Prompt。
真正可以持續使用的產品,還需要資料驗證、狀態管理、錯誤處理、儲存機制、人工檢查與清楚的使用者介面。
換裝置、清除瀏覽器資料或改用其他瀏覽器後,Generated Themes 不會自動同步。
開發網址與正式發布網址屬於不同來源時,兩邊的 LocalStorage 也不會共用。
Structured Output 可以固定資料格式,但不能保證所有知識內容永遠正確。家長與老師使用前仍應檢查內容。
現在已限制單次輸入資料,但仍無法阻止他人重複送出合法請求。
如果正式網址開始對外大量分享,還需要:
目前星星會儲存在 toddler_stars,但畫面沒有清楚說明取得規則,也還需要避免重複領取。
目前使用其他卡片的答案作為干擾選項。雖然已修正 Question 與 Answer 對應問題,未來仍可讓 Gemini 產生更合理的錯誤選項。
目前主要依靠 Build 與人工操作測試。未來可以加入單元測試、API 測試與端對端測試。
如果繼續開發,我會優先處理:
這 30 天最大的收穫,不只是完成一個能產生學習卡的 App。
我逐漸學會將模糊需求拆成可以驗證的小問題:
AI 可以協助產生程式、分析問題與提出修改方案,但最終仍需要人決定:
從第一張固定內容的學習卡,到現在可以自訂主題、呼叫 Gemini、人工編輯、保存資料、進行挑戰、輸出 PDF,最後發布成可以實際開啟的網站,這個 App 並不是單次 Prompt 的結果,而是 30 天持續觀察、測試與修正的累積。
30 天專題在今天告一段落,但這個 App 的下一個階段,才正要開始。