Community程式設計與開發github.com

KyeYongChoo/mcq-quiz-page

當使用者要求從簡報或筆記產生選擇題測驗或練習題時使用,提供可點選的頁面,包含綠色/紅色答案、複習模式、錯誤題重做及PDF匯出;可包含結構化與程式分析題型。

mcq-quiz-page 是什麼?

這是一個 AI Agent 技能,用於從使用者的課程教材(如簡報、筆記、PDF)中自動生成互動式選擇題測驗頁面。它能根據教材內容撰寫高品質的選擇題(MCQ)與結構化/程式分析題(short type),並生成一個可獨立運作的 HTML 頁面,其中包含作答、即時回饋(正確/錯誤標示與解釋)、錯誤題複習、重做錯誤題目、以及匯出為 PDF 或純文字等功能。此技能支援 Claude Code、Cursor、Codex 等具備發布檔案與宣告能力的人工智慧代理平台。工作流程包括:讀取原始教材、依據 JSON 綱要撰寫題目、將題目嵌入預設的 HTML 模板中、並透過發布 artifact 或檔案的方式交付給使用者。它特別強調題目品質,要求干擾選項必須有說服力、正確答案不能總是過長、且選項分佈需平均,以確保測驗的有效性。

相容平台✓Claude Code~Codex CLI✓Cursor
Part ofMobile
npx skills add https://github.com/KyeYongChoo/mcq-quiz-page-skill/tree/HEAD/mcq-quiz-page

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

MCQ 測驗頁面

頁面設計已完成。請勿重新設計。您的工作是撰寫良好的題目,將它們以下方模板的 JSON 格式填入,並發布。

頁面功能:

  • 選擇題:點選選項後,正確答案會變綠色,錯誤答案會變紅色(同時顯示正確答案與解釋)。每個已回答的題目下方會出現一個小的「重做此題」按鈕,可重設該題。
  • 結構化 / 程式分析題(類型 "short"):使用者可選擇輸入答案,然後點選「顯示範例答案」,再自行標記「我答對了」或「我答錯了」。答錯的題目會計入錯誤。程式碼片段會以等寬字型區塊顯示。
  • 複習標籤頁僅列出答錯的題目,並在每題下方直接顯示正確答案/範例答案。複習模式具有「匯出 PDF」、「複製為文字」及「重做錯誤題」按鈕。
  • 「重做錯誤題」會清除這些題目的記錄,並開啟一個「重試」標籤頁,該頁面只顯示先前的錯誤題目(無需滾動整個測驗)。它會顯示進度(已完成/總數),當全部完成時,會顯示「重試仍答錯的 N 題」與「返回所有題目」的摘要。「重試」標籤頁會持續存在,直到按下「全部重設」。
  • 進度、輸入的答案及重試清單會儲存在瀏覽器中。

步驟

  1. 首先閱讀原始教材(簡報、筆記、PDF)。題目必須來自教材,而非憑記憶。對於 PDF 請使用 pdftotext -layout。如果使用者附加了習題或考古題解答,則應根據它們來出題(選擇題與結構化/程式分析題皆可,應為新的變化題,而非逐字複製,除非使用者要求)。
  2. 將題目撰寫為 JSON 陣列(架構如下)。選擇題預設為 4 個選項。如果使用者指定了數量,則使用該確切數量。當教材中有簡答或程式題,或使用者要求時,則加入結構化/程式分析題(將它們放在同一小節的選擇題之後)。
  3. 使用下方平衡規則檢查題目,最好透過一個小腳本進行。
  4. 建構頁面:將模板複製到 index.html,將文字 __DATA__ 替換為 JSON(將每個 </ 跳脫為 </),並僅更改:<title>、<h1>、.sub 句子,以及儲存金鑰 bps-quiz-v1(為每個測驗提供一個新的金鑰,例如 quiz-<topic>-v1,這樣儲存的進度就不會混合)。如果可能,請對腳本執行 node --check(將腳本區塊提取到一個 .js 檔案中)。建議進行快速的 Playwright 冒煙測試(回答、標記、複習、重做錯誤題)。
  5. 發布為手動建構的 HTML 成品:首先將成品命名為 list_types(沒有測驗類型;使用 publish_files 搭配 index.html、真實標題、圖示 quiz)。然後使用最新的版本號碼呼叫 declare_capabilities,參數為 {"downloads": true}。PDF 按鈕僅在具備該功能時才起作用。更新現有測驗時,請使用 publish_files(模式 patch、slug、base_version)重新發布;功能宣告會繼承。如果無法發布成品,請改為以檔案形式交付 index.html(屆時 PDF 按鈕會保持隱藏;「複製為文字」仍可運作)。
  6. 以幾句話回覆:說明頁面是什麼、有多少題目(選擇題與結構化題)、來自哪些章節,以及 PDF 匯出會要求檢視者確認下載。請勿將題目貼到對話中。

JSON 架構(每個問題一個物件,按順序排列)

選擇題:

{"n": 1, "section": "Lecture 4 · Linkability", "q": "問題文字?", "opts": ["A 文字", "B 文字", "C 文字", "D 文字"], "ans": 2, "ex": "一句話解釋。"}

結構化或程式分析(可選的 code、可選的 kind、可選的 ex):

{"n": 5, "type": "short", "section": "Lecture 4 · Linkability", "kind": "結構化問題", "q": "描述...?", "model": ["範例答案的要點 1", "要點 2"], "ex": "可選的額外備註"}
{"n": 6, "type": "short", "section": "...", "kind": "程式分析", "q": "這裡有什麼問題,以及如何修正?", "code": "int f(char *s) {\n  char b[8];\n  strcpy(b, s);\n}", "model": ["(a) ...", "(b) ..."]}

kind 預設為「簡答」;請使用「結構化問題」或「程式分析」。每個 model 條目是一個要點;在要點內將子部分標記為 "(a)"、"(b)"。code 應使用 \n 換行和 2 個空格的縮排。

可選的每個選項備註(僅在使用者要求在錯誤選項下方顯示額外文字時):在 MCQ 中加入 "notes": ["", "為什麼 B 是錯的", "", "為什麼 D 是錯的"],每個選項對應一個字串,順序相同,不需要備註的選項使用 ""(通常是正確選項)。備註會在使用者回答後顯示在每個選項下方,並包含在複製/PDF 中。除非使用者要求,否則不要加入備註。

n 從 1 到 N 連續編號,所有題目之間無間隔(頁面使用 n-1 作為索引)。ans 是正確選項的 0 為基礎索引。section 用於在頁面上對題目進行分組;同一個組內的題目使用相同的字串,並保持組別連續。盡可能避免使用純拉丁文字以外的字元(PDF 使用內建字型;不常見的符號會列印為 ?)。

題目品質規則(使用者很重視這些)

  • 干擾選項必須有說服力:使用來自相同教材、但與錯誤問題相關的真實概念(相鄰術語的屬性、附近的數字、合理的機制)。不要有玩笑選項,也不要使用「以上皆是」。
  • 不要讓正確答案成為最長的選項。應將正確選項與干擾選項的長度和詳細程度寫得相似,有時甚至讓干擾選項最長。目標:在具有完整句子選項的題目中,正確選項嚴格為最長的比例不超過約 30%(可用腳本檢查;很容易超標)。
  • 將正確字母均勻分佈在 A-D 之間(各約 25%),且永遠不要連續三次出現同一個字母。最簡單的方法:先分別寫出正確選項和三個干擾選項,然後在程式碼中使用平衡的位置列表進行洗牌。
  • 每題只測試一個概念。保持問題文字自包含。
  • 每個選擇題都必須在 ex 中提供解釋,說明為什麼答案是正確的(並且,在有用的情況下,說明干擾選項實際上是什麼)。
  • 結構化/程式分析題的範例答案應以簡短的要點形式反映來源解答的關鍵點,以便使用者能夠誠實地自我評分。
  • 如果一張投影片只是您無法閱讀的圖表,請說明這一點,而不是憑空編造細節。

模板 (index.html)

<!DOCTYPE html>
<html lang="zh-Hant"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>主題測驗</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@500&display=swap">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
:root{--bg:#f5f6f8;--card:#ffffff;--ink:#16202e;--mute:#5b6678;--line:#d5dae3;--accent:#1d4ed8;--accent-soft:#e6ecff;
--ok:#0f7a4d;--ok-soft:#dcf3e6;--ok-line:#52b887;--bad:#b42318;--bad-soft:#fde4e1;--bad-line:#e57a70;
--body:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",sans-serif;--mono:"IBM Plex Mono",ui-monospace,Menlo,monospace}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#10151d;--card:#171e29;--ink:#e6eaf1;--mute:#9aa6b8;--line:#2e3849;--accent:#8aa9ff;--accent-soft:#1f2a45;--ok:#6fe0aa;--ok-soft:#12332a;--ok-line:#2f8a63;--bad:#ff9a8f;--bad-soft:#3b1b1a;--bad-line:#a94a42;color-scheme:dark}}
:root[data-theme="dark"]{--bg:#10151d;--card:#171e29;--ink:#e6eaf1;--mute:#9aa6b8;--line:#2e3849;--accent:#8aa9ff;--accent-soft:#1f2a45;--ok:#6fe0aa;--ok-soft:#12332a;--ok-line:#2f8a63;--bad:#ff9a8f;--bad-soft:#3b1b1a;--bad-line:#a94a42;color-scheme:dark}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:400 16px/1.55 var(--body)}
main{max-width:760px;margin:0 auto;padding:24px 16px 64px}
h1{font-size:1.7rem;line-height:1.2;margin:0 0 6px;text-wrap:balance}
.sub{color:var(--mute);margin:0 0 14px}
.bar{position:sticky;top:0;z-index:3;background:var(--bg);border-bottom:1px solid var(--line);padding:10px 0;display:flex;flex-wrap:wrap;gap:8px 10px;align-items:center}
.tabs{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;background:var(--card)}
.tabs button{border:0;background:transparent;color:var(--ink);font:500 .88rem var(--body);padding:8px 14px;cursor:pointer}
.tabs button[aria-pressed="true"]{background:var(--accent);color:var(--bg)}
.stats{font-size:.88rem;color:var(--mute);font-variant-numeric:tabular-nums;display:flex;gap:12px;flex-wrap:wrap}
.stats b{color:var(--ink)}
.acts{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
.btn{font:500 .85rem var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 12px;cursor:pointer;text-decoration:none;display:inline-flex;gap:8px;align-items:center}
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--bg)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn:not(:disabled):hover{filter:brightness(.95)}
:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
.jump{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0 0}
.jump a{font:500 .85rem var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 11px;text-decoration:none}
.jump a span{color:var(--mute);margin-left:8px;font-variant-numeric:tabular-nums}
h2{font-size:1.08rem;margin:30px 0 14px;padding-bottom:8px;border-bottom:2px solid var(--accent);text-wrap:balance;scroll-margin-top:70px}
.banner{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin:18px 0 6px}
.banner p{margin:0}.banner p+p{margin-top:8px;color:var(--mute);font-size:.92rem}
.q{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 16px 14px;margin-bottom:14px;scroll-margin-top:70px}
h3{font-size:1rem;font-weight:600;margin:0 0 12px;display:flex;gap:10px;line-height:1.45}
.num{font:500 .8rem var(--mono);color:var(--accent);background:var(--accent-soft);border-radius:6px;padding:2px 7px;height:fit-content;font-variant-numeric:tabular-nums}
.sec{display:block;font-weight:400;font-size:.78rem;color:var(--mute);margin-bottom:2px}
.opts{display:grid;gap:8px}
.opt{display:flex;gap:10px;align-items:flex-start;text-align:left;width:100%;font:inherit;color:inherit;background:transparent;border:1px solid var(--line);border-radius:8px;padding:10px 12px;cursor:pointer}
.opt b{font:500 .85rem var(--mono);color:var(--mute);min-width:1.2em;padding-top:1px}
.opt:not(:disabled):hover{border-color:var(--accent);background:var(--accent-soft)}
.opt:disabled{cursor:default}
.opt.ok{background:var(--ok-soft);border-color:var(--ok-line)}
.opt.ok b,.opt.ok .tag{color:var(--ok)}
.opt.bad{background:var(--bad-soft);border-color:var(--bad-line)}
.opt.bad b,.opt.bad .tag{color:var(--bad)}
.opt .txt{flex:1}
.tag{font:500 .75rem var(--mono);white-space:nowrap;padding-top:2px}
.fb{margin-top:10px;border-radius:8px;padding:10px 12px;background:var(--accent-soft)}
.fb p{margin:0}.fb .head{font-weight:600}.fb.good .head{color:var(--ok)}.fb.miss .head{color:var(--bad)}
.fb .why{color:var(--mute);font-size:.92rem;margin-top:4px}
.empty{text-align:center;color:var(--mute);padding:36px 12px}
.code{margin:0 0 12px;padding:10px 12px;background:var(--bg);border:1px solid var(--line);border-radius:8px;font:500 .82rem/1.5 var(--mono);overflow-x:auto;white-space:pre;tab-size:2}
textarea.ans{width:100%;font:inherit;color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 12px;resize:vertical;min-height:84px}
textarea.ans[readonly]{background:var(--bg)}
.row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:10px}
.ask{color:var(--mute);font-size:.9rem}
.model{margin:6px 0 0;padding-left:20px}
.model li{margin:3px 0}
.mark{margin:10px 0 0;font-weight:600;font-size:.92rem}
.mark.good{color:var(--ok)}.mark.miss{color:var(--bad)}
.onote{display:block;margin-top:4px;font-size:.86rem;font-weight:400;color:var(--mute)}
.btn.small{padding:5px 10px;font-size:.8rem;color:var(--mute)}
</style></head><body><main>
<h1>主題測驗</h1>
<p class="sub">來自課程教材的 N 道題目。點選選項以作答。在複習模式中,您只會看到答錯的題目,並可將它們匯出為 PDF。</p>
<div class="bar" role="toolbar" aria-label="測驗控制項">
  <div class="tabs" role="group" aria-label="模式">
    <button type="button" id="tab-quiz" aria-pressed="true">測驗</button>
    <button type="button" id="tab-review" aria-pressed="false">複習</button>
    <button type="button" id="tab-retry" aria-pressed="false" hidden>重試</button>
  </div>
  <div class="stats" id="stats" aria-live="polite"></div>
  <div class="acts" id="acts"></div>
</div>
<div id="view"></div>
</main>
<script>
var DATA=__DATA__;
var L="ABCD", KEY="bps-quiz-v1", DKEY=KEY+"-d", RKEY=KEY+"-r";
var answers={}, drafts={}, retry=[], mode="quiz", dl=null;
try{var s=localStorage.getItem(KEY); if(s) answers=JSON.parse(s)||{};}catch(e){}
try{var s2=localStorage.getItem(DKEY); if(s2) drafts=JSON.parse(s2)||{};}catch(e){}
try{var s3=localStorage.getItem(RKEY); if(s3) retry=JSON.parse(s3)||[];}catch(e){}
function save(){try{localStorage.setItem(KEY,JSON.stringify(answers));}catch(e){}}
function saveD(){try{localStorage.setItem(DKEY,JSON.stringify(drafts));}catch(e){}}
function saveR(){try{localStorage.setItem(RKEY,JSON.stringify(retry));}catch(e){}}
function esc(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
function byN(n){return DATA[n-1];}
function isDone(q){var a=answers[q.n]; return q.type==="short" ? (a==="ok"||a==="miss") : a!==undefined;}
function wrongList(){return DATA.filter(function(q){return q.type==="short" ? answers[q.n]==="miss" : (answers[q.n]!==undefined&&answers[q.n]!==q.ans);});}
function doneCount(){return DATA.filter(isDone).length;}

function shortHTML(q,ctx){
  var review=(ctx!=="quiz"), a=answers[q.n], marked=(a==="ok"||a==="miss"), shown=marked||a==="rev";
  var lab=(review?esc(q.section)+" - ":"")+es

…

[為翻譯而截斷]

相關技能