Demo: Corretor Gramatical de Português com IA On-Device
intermediárioCorrija 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.
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
- Página carrega → cria sessão com prompt de revisor
- Usuário digita ou cola texto com erros em português
- Clica “Corrigir” (ou Ctrl+Enter) → botão mostra “Corrigindo…”
- Resultado estruturado → texto corrigido aparece no topo
- Lista de correções → cada item mostra original → corrigido + motivo
- Zero erros → mensagem positiva “Nenhum erro encontrado!”
- Copiar → apenas o texto corrigido vai para clipboard
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Texto sem erros | Schema permite corrections: []; UI mostra “Nenhum erro” |
| Texto em outro idioma | Model pode corrigir ou retornar como está |
| Correção que muda significado | System prompt instrui “manter estilo e tom” |
| JSON parse falha | Try/catch com mensagem genérica |
| Texto muito longo (>3000 chars) | maxlength no textarea |
| XSS no texto de entrada | Escape HTML na renderização |
| Correção falsa positiva | Exibe 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á corrigidocorrections(array obrigatória): pode ser vazia se não há erros- Cada correção tem
original,fixedereason— todos obrigatórios additionalProperties: falseimpede o modelo de inventar campos extras
Isso elimina a necessidade de regex para parsear a resposta.