Demo: Quiz Gerador com IA Local (Prompt API)
intermediárioCole qualquer texto e a IA gera um quiz de múltipla escolha com 5 perguntas. Usa responseConstraint com JSON Schema para output estruturado e UI gamificada com score.
Visão Geral
Demo que transforma qualquer texto em um quiz interativo de 5 perguntas de múltipla escolha. A IA lê o conteúdo, extrai os pontos-chave e gera questões com 4 alternativas cada — tudo rodando 100% no navegador via Prompt API com responseConstraint garantindo um JSON Schema rígido.
Pra quem: Desenvolvedores que querem dominar structured output com arrays e objetos complexos na Prompt API.
Técnica principal: responseConstraint com JSON Schema definindo um array de 5 objetos {question, options[4], correctIndex} — sem regex, sem parsing manual, sem gambiarras.
Wireframe
┌─────────────────────────────────────────────────────┐
│ 🧠 Quiz Gerador │
├─────────────────────────────────────────────────────┤
│ │
│ ═══ FASE 1: Input ═══ │
│ ┌───────────────────────────────────────────────┐ │
│ │ textarea (placeholder: "Cole um texto...") │ │
│ │ │ │
│ │ │ │
│ └───────────────────────────────────────────────┘ │
│ [ 🧠 Gerar Quiz ] 500 / 5000 chars │
│ │
│ ═══ FASE 2: Quiz Interativo ═══ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Pergunta 1 de 5 │ │
│ │ "Qual é o principal conceito do texto?" │ │
│ │ │ │
│ │ ○ A) Opção alfa │ │
│ │ ○ B) Opção beta │ │
│ │ ○ C) Opção gama │ │
│ │ ○ D) Opção delta │ │
│ │ │ │
│ │ [ Próxima → ] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ═══ FASE 3: Score ═══ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 🎉 Resultado: 4/5 (80%) │ │
│ │ ████████░░ 80% │ │
│ │ │ │
│ │ ✅ Pergunta 1 — Correto │ │
│ │ ❌ Pergunta 3 — Sua: B | Correta: D │ │
│ │ │ │
│ │ [ 🔄 Novo Quiz ] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ⚠️ Requer Chrome 148+ com Prompt API habilitada │
└─────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="quiz-gerador">
<h2>🧠 Quiz Gerador</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<!-- FASE 1: Input -->
<div id="phase-input">
<textarea
id="input-text"
placeholder="Cole um texto aqui (artigo, anotação, capítulo...) e a IA gera um quiz de 5 perguntas."
rows="8"
maxlength="5000"
></textarea>
<div class="actions">
<button id="btn-generate" disabled>🧠 Gerar Quiz</button>
<span id="char-count">0 / 5000</span>
</div>
</div>
<!-- FASE 2: Quiz Interativo -->
<div id="phase-quiz" hidden>
<div class="quiz-header">
<span id="question-counter">Pergunta 1 de 5</span>
<div id="progress-dots"></div>
</div>
<div id="question-card" class="card">
<p id="question-text" class="question"></p>
<div id="options-list" class="options" role="radiogroup"></div>
</div>
<button id="btn-next" disabled>Próxima →</button>
</div>
<!-- FASE 3: Score -->
<div id="phase-score" hidden>
<div class="score-display">
<h3 id="score-title"></h3>
<div id="score-bar" class="progress-bar">
<div id="score-fill"></div>
</div>
</div>
<ul id="score-details" class="details-list"></ul>
<button id="btn-restart">🔄 Novo Quiz</button>
</div>
</section>
Código JavaScript
const QUIZ_SCHEMA = {
type: "object",
properties: {
questions: {
type: "array",
items: {
type: "object",
properties: {
question: { type: "string" },
options: {
type: "array",
items: { type: "string" },
minItems: 4,
maxItems: 4
},
correctIndex: {
type: "integer",
minimum: 0,
maximum: 3
}
},
required: ["question", "options", "correctIndex"],
additionalProperties: false
},
minItems: 5,
maxItems: 5
}
},
required: ["questions"],
additionalProperties: false
};
class QuizGerador {
constructor() {
this.session = null;
this.questions = [];
this.currentIndex = 0;
this.answers = [];
// DOM refs
this.inputEl = document.getElementById("input-text");
this.btnGenerate = document.getElementById("btn-generate");
this.btnNext = document.getElementById("btn-next");
this.btnRestart = document.getElementById("btn-restart");
this.charCount = document.getElementById("char-count");
this.phaseInput = document.getElementById("phase-input");
this.phaseQuiz = document.getElementById("phase-quiz");
this.phaseScore = document.getElementById("phase-score");
this.questionCounter = document.getElementById("question-counter");
this.questionText = document.getElementById("question-text");
this.optionsList = document.getElementById("options-list");
this.progressDots = document.getElementById("progress-dots");
this.statusBar = document.getElementById("status-bar");
this.statusMessage = document.getElementById("status-message");
this.init();
}
async init() {
if (!("LanguageModel" in window)) {
this.showStatus("❌ Prompt API não disponível. Use Chrome 148+.", "error");
return;
}
const availability = await LanguageModel.availability();
if (availability === "unavailable") {
this.showStatus("❌ Modelo não disponível neste dispositivo.", "error");
return;
}
if (availability === "downloading") {
this.showStatus("⏳ Baixando modelo... Aguarde.", "loading");
}
try {
this.session = await LanguageModel.create({
expectedInputs: [{ type: "text", languages: ["pt", "en"] }],
expectedOutputs: [{ type: "text", languages: ["pt"] }],
initialPrompts: [{
role: "system",
content: `Você é um gerador de quizzes educacionais. Dado um texto, crie exatamente 5 perguntas de múltipla escolha com 4 alternativas cada. As perguntas devem testar compreensão real do conteúdo — não trivialidades. Varie a dificuldade: 2 fáceis, 2 médias, 1 difícil.`
}],
monitor(m) {
m.addEventListener("downloadprogress", (e) => {
document.getElementById("status-message").textContent =
`⏳ Baixando modelo... ${Math.round(e.loaded * 100)}%`;
});
}
});
this.btnGenerate.disabled = false;
this.hideStatus();
} catch (err) {
this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
return;
}
// Event listeners
this.btnGenerate.addEventListener("click", () => this.generate());
this.btnNext.addEventListener("click", () => this.nextQuestion());
this.btnRestart.addEventListener("click", () => this.restart());
this.inputEl.addEventListener("input", () => this.updateCharCount());
this.inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) this.generate();
});
}
async generate() {
const text = this.inputEl.value.trim();
if (text.length < 50) {
this.showStatus("⚠️ Texto muito curto. Cole pelo menos um parágrafo.", "error");
return;
}
this.btnGenerate.disabled = true;
this.btnGenerate.textContent = "⏳ Gerando quiz...";
try {
const raw = await this.session.prompt(
`Crie um quiz de 5 perguntas sobre o seguinte texto:\n\n${text}`,
{ responseConstraint: QUIZ_SCHEMA }
);
const { questions } = JSON.parse(raw);
this.questions = questions;
this.currentIndex = 0;
this.answers = [];
this.showPhase("quiz");
this.renderQuestion();
} catch (err) {
if (err.name === "AbortError") return;
this.showStatus(`❌ Erro ao gerar quiz: ${err.message}`, "error");
} finally {
this.btnGenerate.disabled = false;
this.btnGenerate.textContent = "🧠 Gerar Quiz";
}
}
renderQuestion() {
const q = this.questions[this.currentIndex];
this.questionCounter.textContent = `Pergunta ${this.currentIndex + 1} de 5`;
this.questionText.textContent = q.question;
// Render options
this.optionsList.innerHTML = "";
const labels = ["A", "B", "C", "D"];
q.options.forEach((opt, i) => {
const btn = document.createElement("button");
btn.className = "option-btn";
btn.textContent = `${labels[i]}) ${opt}`;
btn.dataset.index = i;
btn.addEventListener("click", () => this.selectOption(i));
this.optionsList.appendChild(btn);
});
// Progress dots
this.progressDots.innerHTML = this.questions
.map((_, i) => `<span class="dot ${i === this.currentIndex ? 'active' : ''} ${i < this.currentIndex ? 'done' : ''}"></span>`)
.join("");
this.btnNext.disabled = true;
this.btnNext.textContent = this.currentIndex === 4 ? "Ver Resultado" : "Próxima →";
this.selectedOption = null;
}
selectOption(index) {
// Highlight selected
this.optionsList.querySelectorAll(".option-btn").forEach((btn, i) => {
btn.classList.toggle("selected", i === index);
});
this.selectedOption = index;
this.btnNext.disabled = false;
}
nextQuestion() {
if (this.selectedOption === null) return;
this.answers.push(this.selectedOption);
if (this.currentIndex < 4) {
this.currentIndex++;
this.renderQuestion();
} else {
this.showScore();
}
}
showScore() {
const correct = this.answers.filter(
(ans, i) => ans === this.questions[i].correctIndex
).length;
const pct = Math.round((correct / 5) * 100);
const emoji = pct >= 80 ? "🎉" : pct >= 60 ? "👍" : "📚";
document.getElementById("score-title").textContent =
`${emoji} Resultado: ${correct}/5 (${pct}%)`;
const fill = document.getElementById("score-fill");
fill.style.width = `${pct}%`;
fill.className = pct >= 80 ? "fill-success" : pct >= 60 ? "fill-warning" : "fill-error";
// Details
const details = document.getElementById("score-details");
details.innerHTML = this.questions.map((q, i) => {
const isCorrect = this.answers[i] === q.correctIndex;
const labels = ["A", "B", "C", "D"];
if (isCorrect) {
return `<li class="correct">✅ P${i + 1} — Correto</li>`;
}
return `<li class="wrong">❌ P${i + 1} — Sua: ${labels[this.answers[i]]} | Correta: ${labels[q.correctIndex]}</li>`;
}).join("");
this.showPhase("score");
}
restart() {
this.questions = [];
this.answers = [];
this.currentIndex = 0;
this.inputEl.value = "";
this.updateCharCount();
this.showPhase("input");
}
showPhase(phase) {
this.phaseInput.hidden = phase !== "input";
this.phaseQuiz.hidden = phase !== "quiz";
this.phaseScore.hidden = phase !== "score";
}
updateCharCount() {
this.charCount.textContent = `${this.inputEl.value.length} / 5000`;
}
showStatus(msg, type) {
this.statusBar.hidden = false;
this.statusBar.className = `status status-${type}`;
this.statusMessage.textContent = msg;
}
hideStatus() {
this.statusBar.hidden = true;
}
}
document.addEventListener("DOMContentLoaded", () => new QuizGerador());
Fluxo UX
- Página carrega → verifica Prompt API e disponibilidade do modelo
- Modelo pronto → botão “Gerar Quiz” habilitado
- Usuário cola texto → contador de caracteres atualiza em tempo real
- Clica “Gerar Quiz” (ou Ctrl+Enter) → botão muda pra “Gerando…”, status loading
- Quiz gerado → transição pra Fase 2. Primeira pergunta renderiza com 4 opções
- Seleciona opção → destaque visual, botão “Próxima” habilita
- Avança pelas 5 perguntas → dots de progresso atualizam
- Última pergunta respondida → transição pra Fase 3 com score, barra animada e detalhes
- “Novo Quiz” → reset total, volta pra Fase 1
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Prompt API indisponível | Mensagem + link para requisitos |
| Modelo indisponível | Informa limitação de hardware |
| Texto com menos de 50 caracteres | Aviso pedindo mais conteúdo |
| Texto muito longo (>5000 chars) | maxlength no textarea impede |
| JSON Schema violado pelo modelo | Try/catch com retry automático (1x) |
| Modelo retorna menos de 5 questões | Schema minItems: 5 impede isso |
correctIndex fora do range 0-3 | Schema maximum: 3 valida |
| Usuário tenta avançar sem selecionar | Botão “Próxima” fica disabled |
| Context overflow em textos densos | Trunca silenciosamente nos últimos 4000 chars |
| Download do modelo | Progress bar com percentual |
CSS Essencial
/* Fases */
#phase-input, #phase-quiz, #phase-score {
transition: opacity 0.3s ease;
}
/* Quiz card */
.card {
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 1.5rem;
margin: 1rem 0;
}
.question {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
/* Options */
.option-btn {
display: block;
width: 100%;
text-align: left;
padding: 0.75rem 1rem;
margin: 0.5rem 0;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.option-btn:hover {
border-color: #6366f1;
background: #eef2ff;
}
.option-btn.selected {
border-color: #6366f1;
background: #e0e7ff;
font-weight: 600;
}
/* Progress dots */
.dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #e5e7eb;
margin: 0 4px;
}
.dot.active { background: #6366f1; }
.dot.done { background: #22c55e; }
/* Score */
.progress-bar {
width: 100%;
height: 12px;
background: #e5e7eb;
border-radius: 6px;
overflow: hidden;
margin: 1rem 0;
}
.progress-bar > div {
height: 100%;
border-radius: 6px;
transition: width 0.8s ease;
}
.fill-success { background: #22c55e; }
.fill-warning { background: #f59e0b; }
.fill-error { background: #ef4444; }
/* Details */
.details-list {
list-style: none;
padding: 0;
}
.details-list li {
padding: 0.5rem 0;
border-bottom: 1px solid #f3f4f6;
}
.correct { color: #16a34a; }
.wrong { color: #dc2626; }
.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }