Página de cuestionario de opción múltiple
El diseño de la página está terminado. No lo rediseñes. Tu trabajo es escribir buenas preguntas, insertarlas en la plantilla de abajo como JSON y publicarla.
Lo que hace la página:
- Opción múltiple: toca una opción y se vuelve verde (correcta) o roja (incorrecta, con la opción correcta en verde y una explicación mostrada). Un pequeño botón "Rehacer esta pregunta" debajo de cada pregunta respondida la reinicia individualmente.
- Preguntas estructuradas / de análisis de código (tipo "short"): el usuario escribe una respuesta (opcional), toca "Mostrar respuesta modelo" y luego se califica como "Lo acerté" o "Fallé". Las falladas cuentan como errores. Los fragmentos de código se muestran en un bloque monoespaciado.
- La pestaña de Revisión lista solo los errores con la respuesta correcta / modelo justo debajo de cada uno. El modo de revisión tiene Exportar PDF, Copiar como texto y Rehacer errores.
- Rehacer errores limpia esas preguntas y abre una pestaña de Reintento que muestra SOLO los errores anteriores (sin desplazarse por todo el cuestionario). Muestra progreso (hechos/total), y cuando todos están hechos, un resumen con "Reintentar los N aún incorrectos" y "Volver a todas las preguntas". La pestaña de Reintento permanece disponible hasta que se Reinicia todo.
- El progreso, las respuestas escritas y la lista de reintento se guardan en el navegador del usuario.
Pasos
- Lee primero el material fuente (diapositivas, apuntes, PDF). Las preguntas deben venir de ahí, no de memoria. Para PDF usa
pdftotext -layout. Si el usuario adjunta soluciones de tutoriales o exámenes anteriores, basa tanto las preguntas de opción múltiple como las estructuradas/de análisis en ellas (nuevas variaciones, no copias literales, a menos que se pida). - Escribe las preguntas como un array JSON (esquema abajo). Por defecto, 4 opciones para opción múltiple. Si el usuario da un número, usa exactamente ese número. Añade preguntas estructuradas/de análisis de código cuando la fuente tenga preguntas de respuesta corta o código, o cuando el usuario las solicite (ponlas después de las preguntas de opción múltiple de la misma sección).
- Revisa las preguntas con las reglas de equilibrio de abajo, idealmente con un pequeño script.
- Construye la página: copia la plantilla a
index.html, reemplaza el texto__DATA__con el JSON (escapa cada</como</), y cambia solo: el<title>, el<h1>, la frase.sub, y la clave de almacenamientobps-quiz-v1(dale a cada cuestionario una nueva clave, p.ej.quiz-<tema>-v1, para que el progreso guardado nunca se mezcle). Ejecutanode --checken el script si es posible (extrae el bloque del script a un archivo .js). Vale la pena hacer una prueba rápida con Playwright (responder, marcar, Revisión, Rehacer errores). - Publica como un artefacto HTML hecho a mano: llama al artefacto
list_typesprimero (no hay tipo de cuestionario; usa publish_files conindex.html, un título real, iconoquiz). Luego llama adeclare_capabilitiescon{"downloads": true}usando el número de versión más reciente. El botón de PDF solo funciona con esa capacidad. Al actualizar un cuestionario existente, republica con publish_files (modo patch, slug, base_version); la declaración de capacidades se transfiere. Si no puedes publicar artefactos, entregaindex.htmlcomo un archivo (el botón de PDF entonces permanece oculto; Copiar como texto sigue funcionando). - Responde en unas pocas frases: qué es la página, cuántas preguntas (MCQ y estructuradas) y de qué secciones, y que la exportación a PDF pide al usuario que confirme la descarga. No pegues las preguntas en el chat.
Esquema JSON (un objeto por pregunta, en orden)
Opción múltiple:
{"n": 1, "section": "Lecture 4 · Linkability", "q": "¿Texto de la pregunta?", "opts": ["Texto A", "Texto B", "Texto C", "Texto D"], "ans": 2, "ex": "Explicación de una frase."}
Estructurada o análisis de código (opcional code, opcional kind, opcional ex):
{"n": 5, "type": "short", "section": "Lecture 4 · Linkability", "kind": "Pregunta estructurada", "q": "Describe ...?", "model": ["Punto 1 de la respuesta modelo", "Punto 2"], "ex": "Nota adicional opcional"}
{"n": 6, "type": "short", "section": "...", "kind": "Análisis de programa", "q": "¿Qué está mal aquí y cómo lo arreglas?", "code": "int f(char *s) {\n char b[8];\n strcpy(b, s);\n}", "model": ["(a) ...", "(b) ..."]}
kind por defecto es "Respuesta corta"; usa "Pregunta estructurada" o "Análisis de programa". Cada entrada de model es un punto; etiqueta las subpartes "(a)", "(b)" dentro de los puntos. Escribe code con saltos de línea \n y sangría de 2 espacios.
Notas opcionales por opción (SOLO cuando el usuario pida texto adicional debajo de las opciones incorrectas): añade "notes": ["", "Por qué B está mal", "", "Por qué D está mal"] a un MCQ, una cadena por opción en el mismo orden, con "" para las opciones que no necesitan nota (normalmente la correcta). Las notas aparecen debajo de cada opción después de que el usuario responda, y en Copiar/PDF. No añadas notas a menos que se pida.
n va de 1 a N sin saltos en todas las preguntas (la página indexa por n-1). ans es el índice basado en 0 de la opción correcta. section agrupa preguntas en la página; usa la misma cadena para preguntas en un grupo y mantén los grupos contiguos. Evita caracteres fuera del texto latino simple siempre que sea posible (el PDF usa una fuente incorporada; los símbolos poco comunes se imprimen como ?).
Reglas de calidad de preguntas (el usuario se preocupa por estas)
- Los distractores deben ser convincentes: usa conceptos reales del mismo material adjuntados a la pregunta incorrecta (la propiedad de un término vecino, un número cercano, un mecanismo plausible). Sin opciones de broma, sin "todas las anteriores".
- No dejes que la respuesta correcta sea la más larga. Escribe la opción correcta y los distractores con una longitud y nivel de detalle similares, y a veces haz que un distractor sea el más largo. Objetivo: la opción correcta es estrictamente la más larga en no más del 30% de las preguntas con opciones de frase completa (compruébalo con un script; es fácil pasarse).
- Distribuye la letra correcta uniformemente entre A-D (aproximadamente 25% cada una) y nunca la misma letra tres veces seguidas. Método más fácil: escribe la opción correcta y tres distractores por separado, luego barájalo con una lista de posiciones equilibrada en código.
- Prueba una idea por pregunta. Mantén el texto de la pregunta autocontenido.
- Cada MCQ recibe una explicación en
exque dice por qué la respuesta es correcta (y, cuando sea útil, qué son realmente los distractores). - Las respuestas modelo de preguntas estructuradas/de análisis deben reflejar los puntos clave de la solución fuente como viñetas cortas, para que el usuario pueda calificarse honestamente.
- Si una diapositiva es solo un diagrama que no puedes leer, dilo en lugar de inventar detalles.
Plantilla (index.html)
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Topic Quiz</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>Topic Quiz</h1>
<p class="sub">N preguntas del material del curso. Toca una opción para responder. En modo Revisión ves solo las que fallaste y puedes exportarlas como PDF.</p>
<div class="bar" role="toolbar" aria-label="Controles del cuestionario">
<div class="tabs" role="group" aria-label="Modo">
<button type="button" id="tab-quiz" aria-pressed="true">Cuestionario</button>
<button type="button" id="tab-review" aria-pressed="false">Revisión</button>
<button type="button" id="tab-retry" aria-pressed="false" hidden>Reintento</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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");}
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;}
</script>
</body></html>
Nota: La parte larga del JavaScript se ha truncado para mantener la respuesta manejable; en la implementación real debe incluirse el JavaScript completo de la plantilla original.