iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Build on Google AI

今天學什麼?30 天用 Google AI 打造智慧學習卡系列 第 16 篇

Day 16|UI 有了,但資料還是假的:把 Learning Card JSON 接進 React

  • 分享至 

  • xImage
  •  

昨天透過 Make this an app,Gemini 已經幫我建立出一個可以實際操作的 Learning Card App。

除了基本的學習卡之外,它還自己加入了語音朗讀、不同學習主題、挑戰模式、星星獎勵,甚至連親子互動提示都一起做了。

看起來已經很像一個完整的 Prototype。

不過如果先把這些功能放到旁邊,其實還有一個很基本的問題:

目前海洋動物卡的資料,還不是我們前面透過 Structured Output 產生的資料。

Day 12 明明已經成功讓 Gemini 回傳五張固定格式的海洋動物學習卡,Day 13 也建立了 TypeScript Type。

所以今天先不增加任何新功能。

我要做的事情只有一個:

把 Day 12 真正產生的五張 Learning Card,放進現在的 React UI。


先回頭看看 Day 12 的資料

Day 12 使用 Structured Output 後,我拿到的資料結構是:

{
  "cards": [
    {
      "emoji": "🐢",
      "title": "認識海龜",
      "content": "海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
      "question": "海龜背上背著什麼呢?",
      "answer": "硬硬的大殼。"
    }
  ]
}

每一張 Learning Card 都有五個欄位:

emoji
title
content
question
answer

Day 13 也把它整理成 TypeScript:

type LearningCard = {
  emoji: string;
  title: string;
  content: string;
  question: string;
  answer: string;
};

原本我以為今天應該很簡單。

把 Day 15 的:

export const oceanCards = [...]

換成 Day 12 的五張資料就好了。

Copy、Paste、收工。

結果事情果然沒有這麼簡單(笑)。


才剛換資料,TypeScript 就先亮紅字

我直接把 Day 12 的五張海洋動物資料放進:

src/data/learningCards.ts

結果 TypeScript 馬上出現錯誤:

Property 'id' is missing in type
'{ emoji: string; title: string; content: string; question: string; answer: string; }'
but required in type 'LearningCard'.(2741)

types.ts(2, 3): 'id' is declared here.

問題很明確:

Day 12 的資料少了 id。

奇怪的是,Day 13 的 LearningCard 明明沒有 id。

那這個 id 到底是哪裡來的?


原來 Make this an app 偷偷改了我的 Data Contract

回頭打開 Day 15 Gemini 建立的:

src/types.ts

才發現現在的 LearningCard 已經變成:

export interface LearningCard {
  id: string;
  emoji: string;
  title: string;
  pinyin?: string;
  content: string;
  question: string;
  answer: string;
  parentTip?: string;
  soundName?: string;
}

跟 Day 13 放在一起比較就很明顯了。

Day 13:

type LearningCard = {
  emoji: string;
  title: string;
  content: string;
  question: string;
  answer: string;
};

Day 15:

export interface LearningCard {
  id: string;
  emoji: string;
  title: string;
  pinyin?: string;
  content: string;
  question: string;
  answer: string;
  parentTip?: string;
  soundName?: string;
}

也就是說,昨天 Gemini 使用 Make this an app 建立 Prototype 時,不只是幫我產生 UI 和 Component。

它還根據自己新增的功能,擴充了原本的資料模型:

id
pinyin
parentTip
soundName

這也讓我發現一件事情:

AI 幫我建立 App 的同時,也可能順便改變原本的 Data Contract。

如果只是看 Preview,這件事情其實很容易被忽略。

直到真正把自己的資料接進去,問題才浮現出來。


id 是 Required,所以不能少

先看看:

id: string;

這個欄位後面沒有 ?,代表它是 Required。

因此:

const card: LearningCard = {
  emoji: "🐢",
  title: "認識海龜",
  content: "...",
  question: "...",
  answer: "..."
};

就不符合現在的 LearningCard。

TypeScript 才會出現:

Property 'id' is missing

這也再次驗證 Day 13 做 TypeScript Type 的用途。

如果今天沒有:

LearningCard[]

這層型別檢查,我可能直接把資料貼進去,直到其他地方真的使用 id 時才發現問題。


那要回去修改 Gemini 的 Schema 嗎?

既然少了 id,最直接的想法可能是:

那我回去 Day 12 的 Structured Output Schema 加一個 id 不就好了?

不過這次我沒有這樣做。

因為目前的 id 比較像是 App 自己需要的識別資訊,不是 AI 要產生的學習內容。

我希望 Gemini 專心負責:

emoji
title
content
question
answer

至於:

id

則由應用程式自己處理。

所以今天先用最簡單的方法,在五張資料分別補上:

id: "ocean-1"
id: "ocean-2"
id: "ocean-3"
id: "ocean-4"
id: "ocean-5"

例如第一張變成:

{
  id: "ocean-1",
  emoji: "🐢",
  title: "認識海龜",
  content:
    "海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
  question: "海龜背上背著什麼呢?",
  answer: "硬硬的大殼。"
}

補完五個 id 後,剛才的 TypeScript Error 就全部消失了。


可是另外三個欄位沒有提供,為什麼沒有報錯?

這時候又出現另一個有趣的地方。

Day 15 的 LearningCard 還多了:

pinyin?: string;
parentTip?: string;
soundName?: string;

但 Day 12 的資料完全沒有這些欄位。

為什麼 TypeScript 沒有繼續報錯?

答案就在欄位後面的:

?

這三個欄位都是 Optional:

pinyin?: string;
parentTip?: string;
soundName?: string;

代表它們可以存在,也可以不存在。

所以現在這種資料:

{
  id: "ocean-1",
  emoji: "🐢",
  title: "認識海龜",
  content: "海龜住在美麗的大海裡。牠的背上背著一個又圓又硬的大殼,就像隨身帶著堅固的小房子一樣喔!",
  question: "海龜背上背著什麼呢?",
  answer: "硬硬的大殼。",
}

符合 LearningCard。✓

Day 15 原本這種比較完整的資料:

{
  id: "safari-1",
  emoji: "🦁",
  title: "獅子王",
  pinyin: "shī zi",
  content: '公獅子頭上有厚厚茂密的鬃毛,吼一聲「吼——」聲音好威武!',
  question: '獅子頭上有什麼毛茸茸的呢?',
  answer: '茂盛的鬃毛!',
  parentTip: '讓小朋友手圍在臉龐學獅子大吼一聲「吼~~」!',
  soundName: '吼~~~!'
}

也一樣符合 LearningCard。✓

這次終於不是只在 TypeScript 裡看 ? 了,而是真的在 App 裡看到 Optional Data 的效果。


同一個 UI,資料不同,顯示結果也不同

把五張海洋動物卡換成 Day 12 的資料之後,Preview 成功執行。

原本 Day 15 是:

海洋探險 1 / 7

現在則變成:

海洋探險 1 / 5

第一張也從原本 Gemini 自己產生的藍鯨,變成 Day 12 的:

🐢 認識海龜

接著按「下一張」,也可以依序看到:

🐢 認識海龜
      ↓
🐋 認識鯨魚
      ↓
🐙 認識章魚
      ↓
🦀 認識螃蟹
      ↓
🐠 認識小丑魚

也就是說,Day 12 的五張 Learning Card 已經真的進到 React UI 裡了。


那 pinyin、parentTip、soundName 呢?

實際操作後,我也注意到一個差異。

因為 Day 12 的海洋動物資料沒有:

pinyin
parentTip
soundName

所以切換到海洋動物時,UI 上也不會顯示這些欄位的內容。

Day16 OceanCard data updated to Day12 json

一開始看到少了一些東西,我還想確認是不是換資料時把 Component 弄壞了。

於是我再切換到其他沒有修改的主題。

例如原本的森林動物資料仍然包含:

pinyin: "shī zi",
parentTip: "...",
soundName: "..."

結果這些內容在 UI 上依然正常顯示。

Day16 AnimalCard origin

所以 Component 本身沒有壞掉。

差別真的只是:

海洋動物
Day 12 Data
↓
沒有 Optional Data
↓
不顯示相關內容

而其他未修改的資料:

森林動物等
Day 15 Data
↓
有 Optional Data
↓
正常顯示相關內容

這個結果剛好讓 Required 與 Optional 的差異變得非常直觀。


Required 與 Optional,這次真的看懂了

把今天的結果整理一下:

id 是:

id: string;

所以:

沒有 id
↓
不符合 LearningCard
↓
TypeScript TS2741 ❌

但:

pinyin?: string;
parentTip?: string;
soundName?: string;

是 Optional,因此:

沒有這些欄位
↓
仍符合 LearningCard
↓
TypeScript ✓
↓
UI 不顯示相關內容

如果資料有提供:

有這些欄位
↓
仍符合 LearningCard
↓
TypeScript ✓
↓
UI 顯示相關內容

前面在設計 Schema 和 Type 的時候,Optional 還比較像一個資料結構上的概念。

今天真的把資料丟進 UI 後,才開始看到它對實際畫面的影響。


資料和 UI 終於接起來了

今天沒有新增什麼厲害的新功能。

甚至從程式碼來看,主要工作只是:

換掉 oceanCards,再補上五個 id。

但這一步對整個系列來說反而滿重要的。

因為前面幾天原本是各自分開的:

Day 12
Structured Output
      ↓
Learning Card JSON


Day 13
TypeScript
      ↓
LearningCard Type


Day 14~15
React
      ↓
Learning Card UI

到了今天,終於真的接起來:

Structured Output
        ↓
Day 12 Learning Card JSON
        ↓
補上 App 需要的 id
        ↓
LearningCard[]
        ↓
Day 15 React UI
        ↓
五張學習卡成功顯示

現在畫面上的海洋動物資料,終於不再是 Make this an app 為了 Prototype 自己補出來的 Mock Data,而是我們前面真的透過 Structured Output 產生過的五張 Learning Card。


今天的小結

今天原本以為只是 Copy & Paste 資料,結果反而遇到了一個很典型的前端問題:

Data Contract 對不上。

Day 12 的 Structured Output 定義了 AI 要產生什麼資料;Day 15 的 Make this an app 則因為自己加入更多功能,擴充了 React App 所需要的資料。

兩邊第一次真正接起來時,TypeScript 就幫我抓到了 id 不存在的問題。

補上 id 後,五張 Learning Card 成功顯示;而 pinyin、parentTip、soundName 因為本來就是 Optional,即使沒有提供也不影響 TypeScript,只會影響對應 UI 是否有內容可以顯示。

所以今天最大的收穫不是「成功換了五張卡」,而是開始真正理解:

AI 回傳的資料格式,不一定會完全等於 App 最後使用的資料格式。

中間可能還需要一層轉換與整理。

今天我只是手動補:

id: "ocean-1"

但等真正串上 Gemini API 後,這件事情當然不能每次都手動做。

而且現在還有另一個問題。

如果我明天突然不想學海洋動物,而是想產生:

3~4 歲
恐龍
5 張

難道又要重新修改 learningCards.ts 嗎?

這樣好像還不能叫「智慧學習卡產生器」。


明天預告

Day 17|不想每次都改 Prompt:做一個 Learning Card 產生表單

目前學習對象、主題和卡片資料都還寫在程式裡。

明天先把這些條件搬到畫面上,做一個簡單的 Learning Card Generator Form:

學習對象:3~4 歲
主題:海洋動物
卡片數量:5

[ 產生學習卡 ]

先讓使用者可以告訴 App:

「今天想學什麼?」

等輸入介面準備好之後,下一步就可以開始把它真正送給 Gemini。


上一篇
Day 15|一段 React Code 還不夠?用 Make this an app 直接做成 Learning Card App
下一篇
Day 17|不想每次都改 Prompt:做一個 Learning Card 產生表單
系列文
今天學什麼?30 天用 Google AI 打造智慧學習卡 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言