Demo: Quiz Gerador com IA Local (Prompt API)

intermediário

Cole 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.

Verificando...

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

  1. Página carrega → verifica Prompt API e disponibilidade do modelo
  2. Modelo pronto → botão “Gerar Quiz” habilitado
  3. Usuário cola texto → contador de caracteres atualiza em tempo real
  4. Clica “Gerar Quiz” (ou Ctrl+Enter) → botão muda pra “Gerando…”, status loading
  5. Quiz gerado → transição pra Fase 2. Primeira pergunta renderiza com 4 opções
  6. Seleciona opção → destaque visual, botão “Próxima” habilita
  7. Avança pelas 5 perguntas → dots de progresso atualizam
  8. Última pergunta respondida → transição pra Fase 3 com score, barra animada e detalhes
  9. “Novo Quiz” → reset total, volta pra Fase 1

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API indisponívelMensagem + link para requisitos
Modelo indisponívelInforma limitação de hardware
Texto com menos de 50 caracteresAviso pedindo mais conteúdo
Texto muito longo (>5000 chars)maxlength no textarea impede
JSON Schema violado pelo modeloTry/catch com retry automático (1x)
Modelo retorna menos de 5 questõesSchema minItems: 5 impede isso
correctIndex fora do range 0-3Schema maximum: 3 valida
Usuário tenta avançar sem selecionarBotão “Próxima” fica disabled
Context overflow em textos densosTrunca silenciosamente nos últimos 4000 chars
Download do modeloProgress 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; }