Community编程与开发github.com

KyeYongChoo/mcq-quiz-page

在用户要求基于幻灯片或笔记生成选择题测验或练习题时使用,提供可点击的页面,支持绿色/红色答案、复习模式、重做错题和 PDF 导出;可包含结构化和程序分析题。

mcq-quiz-page 是什么?

该技能可根据用户提供的幻灯片、笔记或 PDF 等材料,自动生成包含选择题和结构化/程序分析题的交互式测验页面。它提供了一个功能完善的模板,支持点击选择答案(正确绿色/错误红色)、显示解释、逐题重做、错题复习、PDF 导出、复制为文本以及专门的「重做」标签页,该标签页仅显示之前的错题并进行重试,所有进度和输入均保存在浏览器本地存储中。该技能强调题目质量,要求干扰项具有迷惑性、答案分布均匀(A-D 约 25%)、并避免正确答案总是最长。它会构建一个独立的 HTML artifact 并发布,支持通过 `publish_files` 和 `declare_capabilities` 发布到支持 artifact 的平台(如 Claude Code),或在无法发布时以文件形式提供。该技能适合开发者和教育工作者快速为技术或非技术内容创建自定进度的测验。

兼容平台✓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。如果用户附加了练习或往年试卷解答,则基于它们编写 MCQ 和结构化/程序分析题(新的变体,除非特别要求,否则不要逐字复制)。
  2. 将问题编写为一个 JSON 数组(模式如下)。MCQ 默认为 4 个选项。如果用户给出了数量,则使用确切的数量。当源材料有简答题或代码题,或者用户要求时,添加结构化/程序分析题(将它们放在同一部分的 MCQ 之后)。
  3. 使用下面的平衡规则检查问题,最好使用一个小脚本。
  4. 构建页面:将模板复制到 index.html,将文本 __DATA__ 替换为 JSON(将每个 </ 转义为 </),并仅更改:<title>、<h1>、.sub 句子以及存储键 bps-quiz-v1(为每个测验提供一个新键,例如 quiz-<topic>-v1,这样保存的进度就不会混淆)。如果可能,对脚本运行 node --check(将脚本块提取到 .js 文件中)。值得做一个快速的 Playwright 冒烟测试(答题、标记、复习、重做错题)。
  5. 作为手动构建的 HTML artifact 发布:首先调用 list_types 类型的 artifact(没有测验类型;使用 publish_files 和 index.html,真实标题,图标 quiz)。然后使用最新的版本号调用 declare_capabilities,参数为 {"downloads": true}。PDF 按钮仅在该功能可用时有效。更新现有测验时,使用 publish_files(模式为 patch,slug,base_version)重新发布;功能声明会延续。如果无法发布 artifact,则将 index.html 作为文件交付(此时 PDF 按钮保持隐藏;复制为文本仍有效)。
  6. 用几句话回复:说明页面是什么、有多少个问题(MCQ 和结构化题)以及来自哪些部分,并说明 PDF 导出会要求查看者确认下载。不要将问题粘贴到聊天中。

JSON 模式(每个问题一个对象,按顺序)

选择题:

{"n": 1, "section": "讲座 4 · 可链接性", "q": "问题文本?", "opts": ["A 文本", "B 文本", "C 文本", "D 文本"], "ans": 2, "ex": "一句话解释。"}

结构化或程序分析题(可选的 code,可选的 kind,可选的 ex):

{"n": 5, "type": "short", "section": "讲座 4 · 可链接性", "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 默认为 "Short answer";使用 "Structured question" 或 "Program analysis"。每个 model 条目是一个要点;在要点内标记子部分 "(a)"、"(b)"。使用 \n 换行和 2 空格缩进编写 code。

可选的每选项注释(仅当用户要求在错误选项下方添加额外文本时):在 MCQ 中添加 "notes": ["", "为什么 B 错误", "", "为什么 D 错误"],每个选项按照相同顺序对应一个字符串,不需要注释的选项使用 ""(通常是正确的那个)。注释在用户回答后显示在每个选项下方,并包含在复制/PDF 中。除非被要求,否则不要添加注释。

n 在所有问题中从 1 到 N 连续运行(页面通过 n-1 索引)。ans 是正确选项的基于 0 的索引。section 在页面上对问题进行分组;对同一组的问题使用相同的字符串,并保持组连续。尽可能避免使用普通拉丁文本之外的字符(PDF 使用内置字体;不常见的符号会打印为 ?)。

问题质量规则(用户很在意这些)

  • 干扰项必须具有说服力:使用来自同一材料的真实概念,但将其附加到错误的问题上(相邻术语的属性、附近的数字、合理的机制)。不要使用玩笑选项,不要使用 "以上都是"。
  • 不要让正确答案最长。以相似的篇幅和详细程度编写正确选项和干扰项,有时让干扰项最长。目标:正确选项严格来说,在包含完整句子选项的问题中,最长的不超过大约 30%(用脚本检查;很容易超标)。
  • 将正确字母均匀分布在 A-D 之间(各约 25%),并且永远不要连续三次出现同一个字母。最简单的方法:分别编写正确选项和三个干扰项,然后在代码中使用平衡的位置列表进行随机排列。
  • 每道题测试一个概念。使问题文本自包含。
  • 每个 MCQ 都在 ex 中给出解释,说明为什么答案是正确的(并且,在有用的时候,说明干扰项实际上是什么)。
  • 结构化/程序分析题的参考答案应以简短的要点的形式反映源解决方案的关键点,以便用户可以诚实地标记自己。
  • 如果一张幻灯片只是一个你无法阅读的图表,请说明情况,而不是凭空捏造细节。

模板(index.html)

<!DOCTYPE html>
<html lang="en"><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="Quiz controls">
  <div class="tabs" role="group" aria-label="Mode">
    <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

…

[为翻译截断]

相关技能