從問題集資料夾產生自主測驗網頁實作教學

Module 2 教學帶你把考古題 PDF 變成「一題一張乾淨圖片 + 標準答案 JSON」的問題集資料夾。這份教學接著往下一步走:從零開始,跟 Codex 一起設計並打造一個小工具,把這種問題集資料夾轉成一個不用架站、雙擊就能打開的自主測驗學習包。

這份教學從只有「問題集資料夾」這個起點出發,一步一步請 Codex 幫你分析需求、設計資料結構、寫出網頁樣板跟打包程式。重點不是背指令,而是學會從零跟 AI Agent 討論、設計並打造可重複使用的學習工具。

本教學對應 module3/ 資料夾,裡面目前只有兩組問題集資料夾(圖片 + 答案 JSON;本範例檔名為 115keys.json),沒有任何預先做好的 app/ 引擎或成品。這是刻意維持的乾淨起點:這份教學的重點是你(跟 Codex)從這兩組資料夾出發,自己設計、生出等效的做法,沒有現成答案可以抄,過程比結果更重要。如果你的專案裡還沒有這兩組資料夾,可以下載 module3-repo.tar,解壓後把 repo/ 資料夾放進 module3/。

範例測驗問題集資料夾(輸入,目前唯一存在的東西)examLabel(建議命名)雙擊即用成品(輸出目標,預期大小)
115 學年度
電機與電子群資電類
專業科目(一)(二),共 100 題
module3/repo/115/Formative_Assessment_115_Professionalapp/dist/Formative_Assessment_115_Professional_quiz.html
(預估約 15.5 MB)
115 學年度
數學(A)
共同科目,共 25 題
module3/repo/math-115a/Formative_Assessment_115_MathAapp/dist/Formative_Assessment_115_MathA_quiz.html
(預估約 2.6 MB)

這兩份問題集資料夾,其實就是 Module 2 教學做出來的東西 只是換了一組資料,重跑一次「下載/切題/清理」流程而已。module3/ 底下沒有留原始 PDF、也沒有第三組待處理的資料 「動手做」單元會告訴你怎麼自己準備一組全新資料,驗證你打造的工具是不是真的可以重複使用,不用每換一組資料就重新設計一次。

學習目標

做完這份教學,你應該能夠:

先備條件

整體設計總覽

先講結論,用一句話說清楚我們要跟 Codex 提出什麼要求:「我手上有一份題目跟正確答案,麻煩幫我做成一個學生可以自己打開作答、立刻看到回饋的網頁。」不用先弄懂任何技術名詞,就跟你平常請人幫忙做事一樣 把「我想要什麼」講清楚,剩下技術上怎麼做,交給 Codex。

自主測驗學習包從整理題目資料、檢查題目與答案、安排練習內容、設計學生作答體驗、製作學習包到實際確認的流程圖
從題目資料到學生自主作答與即時複習的整體流程

接下來會照下面這個順序,一步一步跟 Codex 討論、確認:

  1. 先講好東西要放在哪裡:之後做出來的這些檔案,放在跟你題目資料夾另外分開的一個資料夾,不要混在一起,以後比較不會亂。
  2. 請 Codex 先看一下你的題目資料夾:確認題目跟答案對得起來、總共有幾題、圖片有沒有缺,心裡有個底再往下做。
  3. 講清楚「一份測驗」要記住什麼:像是這份測驗叫什麼名字、題目跟答案放在哪 這樣以後想再加一份新的練習,才知道要告訴 Codex 什麼資訊。
  4. 做出學生實際看到的作答畫面:輸入暱稱、選一次要寫幾題、作答、按送出看分數與逐題回饋。
  5. 把題目、圖片跟畫面「包」成一個檔案:做出來的是一個單獨的網頁檔案,雙擊就能打開,不用架網站、不用連網路。
  6. 自己先打開來試用一次:不是「Codex 說做完了」就代表真的沒問題,一定要自己實際打開、作答一次看看,確認逐題回饋清楚。

這整套東西只要跟 Codex 一起做過一次,以後每多一份新的題目,大部分只需要重複第 2 到第 5 步,不用從頭再來一次;而且同一個網頁工具之後可以陸續裝進好幾份不同的測驗,不用每份測驗都各自做一個新網頁。

步驟一:決定 app/ 要放在哪

設計決策:輸出位置

先想清楚一個問題:工具產生出來的東西(設定檔、樣板、打包程式、最終成品)該放在哪裡,才不會跟問題集資料夾攪在一起、之後也方便換資料?合理的做法是固定放在問題集資料夾的同層(sibling),不是塞進問題集資料夾裡面。以 module3/ 為例,問題集資料夾是 module3/repo/<name>/,工具的輸出就放在 module3/app/,跟 module3/repo/ 平級 這樣以後問題集資料夾即使被整理、封存或換掉,app/ 這個「引擎」也不會受影響。

如果 app/ 已經存在:代表你之前已經對同一層底下的另一組問題集資料夾設計過這套工具(例如做完這份教學後,回頭對第二組資料重跑一次)。這是正常情況,不是衝突 之後每加一份新測驗,做法是在既有 config.json 的 assessments 陣列裡再加一筆,而不是整套工具重新設計一遍。quiz.html 與打包程式是共用引擎,改進的時候可以直接覆蓋,不會影響已經疊進去的其他測驗設定。

先跟 Codex 討論學習包的安排

我有一份已整理好的練習題資料,想做成學生可直接雙擊開啟的自主測驗學習包。
先查看目前的題目資料,提出一個容易維護的安排:保留原始題目、保留日後可更新的內容,並另外準備可直接發給學生的學習包。
先用簡單的方式說明你的安排與理由,等我確認後再開始製作。

把這一步當成跟 Codex 一起做架構設計,而不是丟一句指令就期待它自動生出整套東西 討論清楚位置規則之後,才進入下一步的分析。

步驟二:分析問題集資料夾的形狀

交付物:一支你自己的分析腳本

在動手設計 config.json、寫網頁樣板之前,得先知道這個問題集資料夾實際長什麼樣子:答案 JSON 在哪、有幾題、圖片放在哪個子資料夾、檔名有沒有規律。這一步的交付物是一支你(跟 Codex)自己寫出來的分析腳本,只讀取、不寫入任何檔案。

請 Codex 檢查題目資料

檢查這份題目資料是否適合做成自主測驗學習包。
先不要改動任何內容。
告訴我共有幾題、每題是否都有題目圖片與正確答案、是否有缺漏,以及哪些地方需要先補齊。
用容易閱讀的摘要呈現結果,讓我能決定是否可以開始製作學習包。

對 module3/repo/115 跑起來,實際的分析報告如下(field 名稱可以跟 Codex 討論調整,重點是資訊要齊全):

{
 "folder": "module3/repo/115",
 "keysFile": "115keys.json",
 "questionCount": 100,
 "answerLetters": ["A", "B", "C", "D"],
 "multiAnswerCount": 1,
 "multiAnswerSample": ["115專業2Q8.png"],
 "repoDir": ".",
 "missingImages": [],
 "missingImageCount": 0,
 "extraImageFiles": ["115keys.json"],
 "extraImageFileCount": 1,
 "filenamePattern": "^115專業(?<group>[12])Q(?<number>\\d+)\\.png$",
 "filenamePatternNote": "group token candidates: ['1', '2']"
}

每個欄位在講什麼、為什麼要設計成這樣

欄位怎麼解讀
keysFile / questionCount找到的答案 JSON 檔名、裡面有幾題。跟你預期的題數(例如專業科目 50 題 × 2 科 = 100)對一下,不一致代表分析腳本可能找錯檔案。
repoDir圖片放在哪個子資料夾。如果是 ".",代表圖片跟答案 JSON 直接放在同一層,沒有再包一層子資料夾;module3/repo/115、module3/repo/math-115a 都是這種情況。這個欄位之所以要獨立設計出來,是因為下一步 config.json 需要明確記下這個值,不能靠猜。
missingImages / missingImageCount答案 JSON 裡有寫、但找不到對應圖片檔的題目。這裡是 0,代表 Module 2 的清理成果沒有缺檔;如果不是 0,之後打包時這些題目該怎麼處理(跳過?報錯?),是你設計打包程式時要先決定的規則。
extraImageFiles / extraImageFileCount資料夾裡有、但答案 JSON 沒提到的檔案。這裡列出 115keys.json 自己;這是正常現象:因為 repoDir 是 ".",分析腳本把整個資料夾掃過一輪,keys 檔本身當然不會出現在 keys 的內容裡,可以放心忽略,跟圖片缺漏無關。
multiAnswerCount / multiAnswerSample答案含一個以上字母的複選題數量。115 有 1 題,範例是 115專業2Q8.png;計分邏輯要以答案集合完全相同才算對,不能只檢查其中一個字母。
filenamePattern / filenamePatternNote從檔名推論出來的規則,建議用具名捕獲群組的正規表達式表示。115 這組因為檔名裡有「專業1」跟「專業2」兩種,可以抓出一個 group(分科目)加一個 number(題號);math-115a 檔名只有題號沒有科目差異,所以只有 number、沒有 group(見下方)。如果抓不出規律,不是分析腳本的失敗;代表這組檔名沒有清楚規律,這個欄位直接留空,後面設計網頁樣板時要有「退回逐題顯示原始檔名」的備案,一樣能正常運作,只是不能按科目平均分配抽題。

module3/repo/math-115a 跑出來的報告比較單純,可以對照著看兩者的差別:

{
 "folder": "module3/repo/math-115a",
 "keysFile": "115keys.json",
 "questionCount": 25,
 "answerLetters": ["A", "B", "C", "D"],
 "repoDir": ".",
 "missingImageCount": 0,
 "extraImageFiles": ["115keys.json"],
 "filenamePattern": "^115數學AQ(?<number>\\d+)\\.png$",
 "filenamePatternNote": "single template — number-only pattern"
}

驗收重點

步驟三:設計 config.json 的資料結構

交付物:一份你自己設計的設定檔格式

這一步要決定:怎麼用一份設定檔描述「一份測驗」,還要讓同一套工具之後能疊加好幾份測驗,不用每加一份就重新設計格式。合理的做法是設計一個 assessments 陣列,每一筆對應一份測驗。跟 Codex 討論之後,把 module3/repo/115、module3/repo/math-115a 兩組分析結果,各自對應成一筆設定:

根據剛才的檢查結果,整理兩份題目資料,讓它們可以使用同一套自主測驗學習包。
每份練習都要有清楚名稱、題目、答案、預設練習題數與作答選項。
以後加入新題目時,要能沿用同一套安排,也不能影響已完成的學習包。
先把你的整理方式與兩份練習的用途說明給我確認。

合理設計出來的結果會類似:

{
 "assessments": [
 {
 "examLabel": "Formative_Assessment_115_MathA",
 "assetsDir": "../repo/math-115a",
 "keysFile": "115keys.json",
 "repoDir": ".",
 "questionCount": 20,
 "choiceLetters": "ABCD",
 "filenamePattern": "^115數學AQ(?<number>\\d+)\\.png$"
 },
 {
 "examLabel": "Formative_Assessment_115_Professional",
 "assetsDir": "../repo/115",
 "keysFile": "115keys.json",
 "repoDir": ".",
 "questionCount": 20,
 "choiceLetters": "ABCD",
 "filenamePattern": "^115專業(?<group>[12])Q(?<number>\\d+)\\.png$"
 }
 ]
}

每個欄位怎麼設計、怎麼填

欄位設計考量/怎麼填
examLabel這份測驗的名字,會變成輸出檔名的一部分。設計時務必確保陣列裡每一筆都不一樣,因為它同時決定輸出檔名,撞名會互相覆蓋 加新的一筆之前,先看一眼既有陣列裡已經用過哪些名字。
assetsDir從 app/ 這個資料夾出發,走到問題集資料夾的相對路徑。這是唯一一個分析報告不會直接告訴你的欄位,要自己算;例如問題集資料夾是 module3/repo/115、app/ 是 module3/app/,相對路徑就是 ../repo/115。
keysFile / repoDir / questionCount(原始題數)/ filenamePattern直接照抄步驟二分析報告的對應欄位。filenamePattern 抓不出來時,設計上要允許整個欄位不寫。
questionCount(config.json 裡實際寫的值)不是原始題數,是「每次作答預設抽幾題」,可以設計成 min(20, 分析報告的 questionCount) 這類上限公式,避免一次抽太多題拖慢作答。115 有 100 題、math-115a 有 25 題,兩筆都寫 20,就是因為兩者都超過或等於 20。學生開始作答時仍然可以自己把題數改成 1~全部題數之間的任何數字,這裡只是預設值;這個「使用者仍可覆蓋預設值」的彈性,要在設計網頁樣板(步驟四)時一併考慮進去。
choiceLetters沒特殊情況固定寫 "ABCD";只有分析報告的 answerLetters 超出 A~D 才需要換成對應字母組合。

如果日後對已經有內容的 app/config.json 疊加新測驗,記得先讀出既有內容,用陣列 push 的方式加一筆,不要整個檔案覆蓋掉,不然會把之前疊上去的測驗設定弄丟。這也是「設計成陣列」而不是「單一物件」的原因。

步驟四:打造測驗網頁樣板(quiz.html)

交付物:學生作答用的網頁樣板

這一步要設計、寫出學生實際會打開的那個頁面。核心設計限制是:最終交付的檔案必須能直接雙擊開啟,不能依賴任何伺服器;瀏覽器基於安全性,會擋掉 file:// 路徑底下用 fetch() 讀外部檔案的行為,所以最終版本不能用 fetch() 讀取 config.json 或圖片,資料必須直接內嵌在 HTML 裡。

學生從選擇練習題數、開始作答、立即看到結果、對照正確答案到回看錯題再練習的自主練習旅程
學生使用學習包時,從作答到複習的完整旅程

請 Codex 設計學生作答畫面

設計一個學生可直接使用的自主測驗學習包。
學生可輸入暱稱、選擇練習題數、隨機作答,完成後立刻看到總分、每題對錯與正確答案。
結果頁要有簡短的自我複習提示,協助學生回看錯題。
全程只在學生自己的裝置上使用,不蒐集、不下載、不上傳任何作答資料。
完成後學生必須能直接雙擊開啟,不需要登入、安裝或連網。

把這一步的產物想成兩種版本並存的設計:一份是可編輯原始碼版本(quiz.html 本身,可能還是用 fetch() 讀 config.json 方便你邊改邊測,本機測試時需要另外開伺服器);另一份是下一步打包出來的雙擊即用成品(dist/*.html,資料已經內嵌,不需要伺服器)。分清楚這兩種版本的差別,是這一步最重要的設計決定。

驗收重點

步驟五:寫打包程式,產出雙擊即用檔案

交付物:一支你自己的打包腳本

這一步要把 config.json 裡每一筆設定指到的圖片,逐一轉成 base64 字串,塞進步驟四的樣板裡,輸出成一個完全自包含、不需要 fetch()、不需要網路的 HTML 檔。

請 Codex 建立可發送的學習包

把每份已整理好的題目、答案與學生作答畫面,製作成各自獨立的自主測驗學習包。
每個學習包都要能直接雙擊開啟,題目和圖片要完整包含在裡面,不需要連網或另外安裝內容。
完成後列出每個學習包的名稱、題數,並說明是否已可直接發給學生。
如果有題目圖片或答案缺漏,要清楚指出需要補齊的練習;其他完整的學習包仍可繼續完成。

建立全部學習包

依照目前已確認的安排,建立全部自主測驗學習包。
完成後逐一確認:能否直接開啟、題目與圖片是否完整、作答後是否能顯示正確答案與複習提示。
告訴我哪些學習包已完成,以及是否有需要補齊的題目資料。

只想重建剛剛新加的那一筆時(例如另外兩份都已經是最新的,重新打包全部只是浪費時間、每份都可能是好幾十 MB):

只更新剛新增的這一份自主測驗學習包,不要改動其他已完成的學習包。
完成後實際檢查這一份能否直接開啟、作答並看到複習回饋,再告訴我是否可以直接發給學生使用。

產出:每一筆測驗對應 module3/app/dist/<examLabel>_quiz.html 一個檔案。終端機應該會印出類似:

Wrote module3/app/dist/Formative_Assessment_115_MathA_quiz.html (25 questions, 2.6 MB)
Wrote module3/app/dist/Formative_Assessment_115_Professional_quiz.html (100 questions, 約 15.5 MB)
Built 2/2 assessments

驗收重點

步驟六:驗收成果

不要只信任「腳本沒報錯」

發給學生前,確認學習包能直接開啟、題目內容完整,並能在作答後提供複習回饋的三項檢查圖
發給學生前,三項都要實際確認

動手做:套用在一組全新資料上

module3/ 目前只有 repo/115、repo/math-115a 這兩組資料,沒有預先準備第三組。這個空白正好是驗證「自己造的工具是否真的可以重複使用」最直接的方式:找一組全新的問題集資料,看看步驟二到步驟五做出來的分析腳本、config.json 設計、打包程式,能不能不修改、原封不動地套用上去。如果可以,代表這套工具的設計是對的;如果每換一組資料就要改工具本身的程式碼,代表某個環節設計得不夠通用,值得回頭檢討。

第一步:準備一組新的問題集資料夾

資料來源不重要,挑一種你方便的方式就好:拿你自己手邊已經有的任何一份考古題 PDF;或回到 Module 2,換一個你感興趣的學年度或科目,照「下載 → 切題 → 清理」完整流程重新走一次。做完之後,你會得到一組新的「圖片 + 答案 JSON」資料夾,存到 module3/repo/<新資料夾名稱>/(跟現有的 115、math-115a 平級)。如果換到的是共同科目,記得比照 Module 2 處理數學科的方式,注意題數跟檔名裡的科目代號、以及切題完檔名裡殘留的「專業」字樣要改名。

第二步:檢查新題目資料

檢查我剛新增的題目資料是否完整,並用和前面相同的方式整理重點:題數、圖片與答案是否一一對應,以及需要先修正的地方。

如果這支腳本是步驟二設計得夠通用,不需要為了這組新資料改任何程式碼,就應該能印出一份格式一致的分析報告(questionCount、repoDir、filenamePattern 等欄位都齊全)。

第三步:疊加進 config.json、重新打包

可以直接拿 config.json 裡既有的任何一筆設定當模板改。把新的一筆加進 module3/app/config.json 的陣列(記得 examLabel 要跟既有的不一樣),然後只重建這一筆:

把這組新題目加入現有的自主測驗學習包,並只建立這一份新的學習包。
不要改動其他已完成的學習包。
完成後實際確認新學習包能直接開啟、練習幾題並看到複習回饋,再告訴我檢查結果。

最後照步驟六驗收:檔案存在、大小合理、雙擊打開能正常作答送出。完成後 module3/app/config.json 應該多一筆 assessments,module3/app/dist/ 也應該多一個檔案,另外已經做好的測驗完全不受影響 這正好證明「疊加新測驗不需要重新設計工具」這個目標達成了。

常見狀況快速對照表

狀況代表什麼該怎麼做
分析腳本找不到 keys.json,或找到不只一個 .json 檔資料夾裡的 JSON 檔不只一個、或完全沒有,腳本猜不出哪個是答案 key回頭跟 Codex 討論,讓分析腳本支援明確指定檔名的參數,再重跑
repoDir 分析不出來,或找不到圖片資料夾腳本的圖片位置偵測邏輯不夠周全確認圖片實際路徑後,讓分析腳本支援明確指定資料夾的參數,再重跑
repoDir 是 ".",但 config.json 裡漏寫了這個欄位如果設計上把 repoDir 省略時的預設值定成 "repo",跟 "." 不一樣,打包時會去找不存在的 repo/ 子資料夾"repoDir": "." 一定要明確寫進 config.json,不能省略;或索性把設計改成沒有預設值、缺欄位就報錯,逼自己每次都寫清楚
filenamePattern 找出來了,但 group 候選字元看起來是雜訊檔名剛好符合腳本的推論規則,但那個「差異」其實不代表任何有意義的分類不要硬寫進 config.json,直接省略這個欄位,確認網頁樣板有「退回逐題顯示原始檔名」的備案
打包程式印出「Built N/M assessments」,N < M至少一筆 assessment 打包失敗往上找對應的警告訊息,通常是路徑欄位寫錯
新加的 examLabel 跟既有某一筆撞名examLabel 同時決定輸出檔名,撞名會互相覆蓋加新的一筆之前,先看一眼 config.json 既有陣列用過哪些名字
直接雙擊 app/quiz.html(不是 dist/ 裡的檔案),畫面空白或作答按鈕沒反應quiz.html 是「可編輯原始碼」版本,如果設計上還是用 fetch() 讀取設定跟圖片,瀏覽器基於安全性會擋掉 file:// 底下的 fetch()要測原始碼版本,在 app/ 資料夾下開一個本機伺服器再開瀏覽器:python3 -m http.server;真正要交給學生的是 dist/*.html,不需要伺服器
改了 config.json 之後,dist/ 裡的檔案內容沒變dist/*.html 是打包當下的快照,不會隨 config.json 即時更新改完 config.json 一定要重跑一次打包程式(可以只加 --only 那一筆)

下一步

疊加更多測驗,只要重複「Module 2 切題清理 → 套用自己的分析腳本 → 疊加設定 → 重新打包」這個循環,工具本身不需要重新設計。

把這份教學做出來的腳本留著。之後每增加一組題目,只要更新設定並重新打包,就能產生新的離線自主測驗學習包;這正是可重複使用工具最重要的價值。

交給學生前,記得分清楚兩種產出:dist/*.html 是可直接發給學生的學習包,雙擊即用、不需要伺服器、不需要網路;quiz.html/config.json/打包程式是留著未來修改題目與重建學習包的可編輯原始碼,測試這一份才需要本機伺服器。

如果問題集內容是有版權疑慮的考古題(例如技專校院入學測驗中心的試題),交付雙擊即用的 HTML 之前,記得再次確認自己有沒有合法散布這些內容的權利。