Communityコーディング&開発github.com

KyeYongChoo/mcq-quiz-page

ユーザーがスライドやノートから多肢選択クイズや練習問題を要求した場合に、クリック可能なページとして、緑/赤の回答表示、復習モード、誤答の再挑戦、PDFエクスポート機能を提供します。構造問題やプログラム解析問題も含められます。

mcq-quiz-page とは?

このスキルは、ユーザーが提供する講義スライド、ノート、PDFなどのソース資料に基づいて、多肢選択式(MCQ)および構造問題/プログラム解析問題からなるインタラクティブなクイズページを生成します。主にClaude Code(Artifacts機能を使用)での利用を想定しており、HTML+CSS+JavaScriptで構築された完全なWebページとして出力されます。ユーザーは各問題に回答後、即座に正誤判定(緑/赤の色分け)と解説を確認でき、間違えた問題のみを復習タブで一覧表示し、さらに再挑戦タブで誤答のみを再度解くことが可能です。進行状況、入力済みの回答、再挑戦リストはブラウザのローカルストレージに保持されるため、ページを閉じても状態が維持されます。また、PDFエクスポート機能により、復習内容をダウンロードすることもできます(`downloads`機能の宣言が必要)。このスキルは、ユーザーがクイズの作成を依頼した際に、与えられたソース資料から正確に問題を作成し、指定されたJSONスキーマに従ってテンプレートに埋め込み、最終的にArtifactsとして公開するまでの一連のワークフローを定義しています。

対応✓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に質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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-<トピック>-v1 を設定し、保存された進行状況が混ざらないようにします)。可能であれば、スクリプトブロックを.jsファイルに抽出して node --check を実行します。簡単な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. 数文で返信します。ページの説明、問題数(MCQと構造問題)、およびどのセクションからのものか、PDFエクスポートではダウンロードの確認を求めること。問題をチャットに貼り付けないでください。

JSONスキーマ(質問ごとに1つのオブジェクト、順序どおり)

多肢選択:

{"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" です。「構造問題」または「プログラム解析」を使用してください。各 model エントリは1つの箇条書きです。サブパートには箇条書きの中で "(a)"、"(b)" のラベルを付けてください。code は \n 改行と2スペースのインデントで記述してください。

選択肢ごとのオプションノート(ユーザーが誤った選択肢の下に追加テキストを要求した場合のみ): MCQに "notes": ["", "Bが間違いである理由", "", "Dが間違いである理由"] を追加します。各選択肢に対して順序通りに1つの文字列を指定し、不要な選択肢(通常は正解)には "" を使用します。ノートは、ユーザーが回答した後に各選択肢の下に表示され、コピー/PDFにも含まれます。要求されない限り、ノートを追加しないでください。

n はすべての問題で1からNまでギャップなく連続して実行されます(ページは n-1 でインデックス付けされます)。ans は正しい選択肢の0から始まるインデックスです。section はページ上で問題をグループ化します。1つのグループ内の質問には同じ文字列を使用し、グループを連続して配置してください。可能な限りプレーンなラテン文字以外の文字は避けてください(PDFは組み込みフォントを使用し、特殊な記号は ? と表示されます)。

問題品質ルール(ユーザーはこれらを重視しています)

  • ディストラクター(誤答選択肢)は説得力がある必要があります。同じ資料から、間違った質問に関連付けられた実際の概念(隣接する用語のプロパティ、近くの数字、もっともらしいメカニズム)を使用してください。ふざけた選択肢や「すべて上記」は避けてください。
  • 正解が最も長くならないようにしてください。正解とディストラクターは同程度の長さと詳細レベルで記述し、時にはディストラクターを最も長くしてください。目標: 完全な文の選択肢がある質問のうち、正解が厳密に最も長いものは約30%以下にしてください(スクリプトで確認してください。簡単に超えてしまいます)。
  • 正解の文字をA-Dに均等に分散させ(各約25%)、同じ文字が3回連続しないようにしてください。最も簡単な方法: 正解と3つのディストラクターを別々に作成し、コード内でバランスの取れた位置リストでシャッフルします。
  • 1つの問題につき1つのアイデアをテストしてください。問題文は自己完結型にしてください。
  • すべての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="クイズコントロール">
  <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

…

[翻訳のため省略]

(注: スクリプトの残りの部分は元のテンプレートと同一です。ここでは完全なコピーではなく、主要な構造と翻訳箇所を示しています。実際の実装では、テンプレート全体をそのまま使用し、必要な文字列のみを翻訳してください。)

関連スキル