Demo: Corretor Gramatical de Português com IA On-Device

intermediário

Corrija erros gramaticais e ortográficos com IA local no navegador. Visualize um diff detalhado das correções com explicações — tudo via Prompt API sem nuvem.

Verificando...

Visão Geral

Demo que corrige erros gramaticais e ortográficos em textos em português, mostrando tanto o texto corrigido quanto a lista de cada correção com motivo. Confesso que esse é um dos meus demos favoritos — a UI de diff fica linda e o structured output garante que tudo vem mastigadinho pra renderizar.

Pra quem: Desenvolvedores que querem structured output complexo e UI de diff.

Técnica principal: responseConstraint com schema contendo texto corrigido + array de correções com original, corrigido e motivo.


Wireframe

┌─────────────────────────────────────────────────────────┐
│  ✍️ Corretor de Português                               │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │ textarea                                        │    │
│  │ "O menino foi na loja e comprou bastante        │    │
│  │  coisa. Ele tava muito contente com as coisas   │    │
│  │  que ele comprou pra mãe dele."                 │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  [ ✍️ Corrigir Texto ]                                  │
│                                                         │
│  ┌─────────────────────────────────────────────────┐    │
│  │ ✅ Texto Corrigido:                             │    │
│  │ "O menino foi à loja e comprou muitas           │    │
│  │  coisas. Ele estava muito contente com o        │    │
│  │  que comprou para a mãe."                       │    │
│  └─────────────────────────────────────────────────┘    │
│                                                         │
│  📋 Correções (4):                                      │
│  ┌─────────────────────────────────────────────────┐    │
│  │ ❌ "foi na" → ✅ "foi à"                        │    │
│  │    Motivo: Regência do verbo "ir" com crase     │    │
│  │                                                 │    │
│  │ ❌ "tava" → ✅ "estava"                          │    │
│  │    Motivo: Linguagem informal → formal          │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="corretor-portugues">
  <h2>✍️ Corretor de Português</h2>

  <div id="status-bar" class="status" hidden>
    <span id="status-message"></span>
  </div>

  <textarea
    id="input-text"
    placeholder="Digite ou cole um texto em português para corrigir..."
    rows="6"
    maxlength="3000"
  ></textarea>

  <div class="actions">
    <button id="btn-correct" disabled>✍️ Corrigir Texto</button>
  </div>

  <div id="result" hidden>
    <div class="corrected-section">
      <h3>✅ Texto Corrigido:</h3>
      <p id="corrected-output" class="corrected-text"></p>
      <button id="btn-copy">📋 Copiar Texto Corrigido</button>
    </div>

    <div class="corrections-section">
      <h3>📋 Correções (<span id="corrections-count">0</span>):</h3>
      <ul id="corrections-list"></ul>
    </div>
  </div>
</section>

Código JavaScript

const CORRECTION_SCHEMA = {
  type: "object",
  properties: {
    corrected: {
      type: "string",
      description: "O texto completo corrigido"
    },
    corrections: {
      type: "array",
      items: {
        type: "object",
        properties: {
          original: { type: "string", description: "Trecho original com erro" },
          fixed:    { type: "string", description: "Trecho corrigido" },
          reason:   { type: "string", description: "Motivo da correção" }
        },
        required: ["original", "fixed", "reason"]
      }
    }
  },
  required: ["corrected", "corrections"],
  additionalProperties: false
};

class PortugueseCorrector {
  constructor() {
    this.session = null;

    this.inputEl = document.getElementById("input-text");
    this.btnEl = document.getElementById("btn-correct");
    this.btnCopy = document.getElementById("btn-copy");
    this.resultEl = document.getElementById("result");
    this.correctedEl = document.getElementById("corrected-output");
    this.listEl = document.getElementById("corrections-list");
    this.countEl = document.getElementById("corrections-count");
    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;
    }

    try {
      this.session = await LanguageModel.create({
        expectedInputs: [{ type: "text", languages: ["pt"] }],
        expectedOutputs: [{ type: "text", languages: ["pt"] }],
        initialPrompts: [{
          role: "system",
          content: `Você é um revisor de português brasileiro. Corrija erros de gramática, ortografia, concordância, regência e pontuação. Mantenha o estilo e tom do autor. Liste cada correção feita com o trecho original, a versão corrigida e o motivo. Se não houver erros, retorne o texto original e array vazio de correções.`
        }]
      });

      this.btnEl.disabled = false;
      this.hideStatus();
    } catch (err) {
      this.showStatus(`❌ Erro: ${err.message}`, "error");
      return;
    }

    this.btnEl.addEventListener("click", () => this.correct());
    this.btnCopy.addEventListener("click", () => this.copy());
    this.inputEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && e.ctrlKey) this.correct();
    });
  }

  async correct() {
    const text = this.inputEl.value.trim();
    if (!text) return;

    this.btnEl.disabled = true;
    this.btnEl.textContent = "⏳ Corrigindo...";
    this.resultEl.hidden = true;

    try {
      const raw = await this.session.prompt(
        `Corrija o seguinte texto em português:\n\n"${text}"`,
        { responseConstraint: CORRECTION_SCHEMA }
      );

      const result = JSON.parse(raw);
      this.renderResult(result);
    } catch (err) {
      this.showStatus(`❌ Erro: ${err.message}`, "error");
    } finally {
      this.btnEl.disabled = false;
      this.btnEl.textContent = "✍️ Corrigir Texto";
    }
  }

  renderResult({ corrected, corrections }) {
    // Texto corrigido
    this.correctedEl.textContent = corrected;

    // Lista de correções
    this.listEl.innerHTML = "";
    this.countEl.textContent = corrections.length;

    if (corrections.length === 0) {
      const li = document.createElement("li");
      li.className = "no-corrections";
      li.textContent = "✅ Nenhum erro encontrado!";
      this.listEl.appendChild(li);
    } else {
      for (const c of corrections) {
        const li = document.createElement("li");
        li.className = "correction-item";
        li.innerHTML = `
          <div class="correction-diff">
            <span class="diff-original">❌ "${this.esc(c.original)}"</span>
            <span class="diff-arrow">→</span>
            <span class="diff-fixed">✅ "${this.esc(c.fixed)}"</span>
          </div>
          <div class="correction-reason">💡 ${this.esc(c.reason)}</div>
        `;
        this.listEl.appendChild(li);
      }
    }

    this.resultEl.hidden = false;
  }

  copy() {
    navigator.clipboard.writeText(this.correctedEl.textContent).then(() => {
      this.btnCopy.textContent = "✅ Copiado!";
      setTimeout(() => { this.btnCopy.textContent = "📋 Copiar Texto Corrigido"; }, 2000);
    });
  }

  esc(str) {
    const el = document.createElement("span");
    el.textContent = str;
    return el.innerHTML;
  }

  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 PortugueseCorrector());

Fluxo UX

  1. Página carrega → cria sessão com prompt de revisor
  2. Usuário digita ou cola texto com erros em português
  3. Clica “Corrigir” (ou Ctrl+Enter) → botão mostra “Corrigindo…”
  4. Resultado estruturado → texto corrigido aparece no topo
  5. Lista de correções → cada item mostra original → corrigido + motivo
  6. Zero erros → mensagem positiva “Nenhum erro encontrado!”
  7. Copiar → apenas o texto corrigido vai para clipboard

Edge Cases e Tratamento de Erros

CenárioTratamento
Texto sem errosSchema permite corrections: []; UI mostra “Nenhum erro”
Texto em outro idiomaModel pode corrigir ou retornar como está
Correção que muda significadoSystem prompt instrui “manter estilo e tom”
JSON parse falhaTry/catch com mensagem genérica
Texto muito longo (>3000 chars)maxlength no textarea
XSS no texto de entradaEscape HTML na renderização
Correção falsa positivaExibe motivo para o usuário avaliar

CSS Essencial

.corrected-text {
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  border-radius: 0.5rem;
  padding: 1rem;
  line-height: 1.6;
}

.correction-item {
  padding: 0.75rem;
  border-bottom: 1px solid #f3f4f6;
}

.correction-diff { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.diff-original { color: #dc2626; text-decoration: line-through; }
.diff-fixed { color: #16a34a; font-weight: 600; }
.diff-arrow { color: #6b7280; }
.correction-reason { color: #6b7280; font-size: 0.875rem; margin-top: 0.25rem; }

.no-corrections { color: #16a34a; font-weight: 600; }

JSON Schema — Detalhes

O schema garante:

  • corrected (string obrigatória): texto completo já corrigido
  • corrections (array obrigatória): pode ser vazia se não há erros
  • Cada correção tem original, fixed e reason — todos obrigatórios
  • additionalProperties: false impede o modelo de inventar campos extras

Isso elimina a necessidade de regex para parsear a resposta.