Demo: Detector de Toxicidade com IA Local (Prompt API)
intermediárioAnalise comentários antes de enviar e receba sugestões de reescrita construtiva. Moderação on-device com Prompt API e JSON Schema via responseConstraint.
Visão Geral
Demo que intercepta o envio de um comentário/review e analisa se o conteúdo é tóxico antes de publicar. Se detectar toxicidade, bloqueia o envio, explica o motivo e sugere uma versão construtiva do texto. Tudo roda no navegador — sem servidor, sem latência, sem expor o conteúdo do usuário.
Pra quem: Desenvolvedores implementando moderação client-side como primeira camada de proteção.
Técnica principal: responseConstraint com JSON Schema retornando {toxic, reason, suggestion} — o modelo é forçado a entregar um objeto estruturado com a classificação, justificativa e alternativa construtiva.
Wireframe
┌─────────────────────────────────────────────────────┐
│ 🛡️ Detector de Toxicidade │
├─────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ textarea (placeholder: "Escreva seu │ │
│ │ comentário ou review...") │ │
│ │ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ [ 📤 Enviar Comentário ] 120 / 1000 │
│ │
│ ┌─── ALERTA (quando tóxico) ────────────────────┐ │
│ │ ⚠️ Conteúdo potencialmente ofensivo │ │
│ │ │ │
│ │ Motivo: Linguagem agressiva direcionada │ │
│ │ a indivíduos │ │
│ │ │ │
│ │ 💡 Sugestão de reescrita: │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ "Discordo da abordagem porque..." │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ │ │ │
│ │ [ ✏️ Usar Sugestão ] [ Enviar Mesmo Assim ] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─── SUCESSO (quando não tóxico) ───────────────┐ │
│ │ ✅ Comentário enviado com sucesso! │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ⚠️ Requer Chrome 148+ com Prompt API habilitada │
└─────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="detector-toxicidade">
<h2>🛡️ Detector de Toxicidade</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<textarea
id="input-comment"
placeholder="Escreva seu comentário ou review..."
rows="5"
maxlength="1000"
aria-label="Campo de comentário"
></textarea>
<div class="actions">
<button id="btn-submit" disabled>📤 Enviar Comentário</button>
<span id="char-count">0 / 1000</span>
</div>
<!-- Alerta de toxicidade -->
<div id="alert-toxic" class="alert alert-toxic" hidden>
<div class="alert-header">
<span class="alert-icon">⚠️</span>
<strong>Conteúdo potencialmente ofensivo</strong>
</div>
<p id="toxic-reason" class="alert-reason"></p>
<div class="suggestion-box">
<p class="suggestion-label">💡 Sugestão de reescrita:</p>
<blockquote id="toxic-suggestion"></blockquote>
</div>
<div class="alert-actions">
<button id="btn-use-suggestion" class="btn-secondary">✏️ Usar Sugestão</button>
<button id="btn-force-send" class="btn-ghost">Enviar Mesmo Assim</button>
</div>
</div>
<!-- Sucesso -->
<div id="alert-success" class="alert alert-success" hidden>
<span>✅</span> Comentário enviado com sucesso!
</div>
</section>
Código JavaScript
const TOXICITY_SCHEMA = {
type: "object",
properties: {
toxic: {
type: "boolean",
description: "true se o texto contém linguagem tóxica, ofensiva ou agressiva"
},
reason: {
type: "string",
description: "Explicação breve do motivo da classificação"
},
suggestion: {
type: "string",
description: "Versão reescrita do texto de forma construtiva e respeitosa"
}
},
required: ["toxic", "reason", "suggestion"],
additionalProperties: false
};
class ToxicityDetector {
constructor() {
this.session = null;
this.inputEl = document.getElementById("input-comment");
this.btnSubmit = document.getElementById("btn-submit");
this.alertToxic = document.getElementById("alert-toxic");
this.alertSuccess = document.getElementById("alert-success");
this.reasonEl = document.getElementById("toxic-reason");
this.suggestionEl = document.getElementById("toxic-suggestion");
this.btnUseSuggestion = document.getElementById("btn-use-suggestion");
this.btnForceSend = document.getElementById("btn-force-send");
this.statusBar = document.getElementById("status-bar");
this.statusMessage = document.getElementById("status-message");
this.charCount = document.getElementById("char-count");
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 moderador de conteúdo. Analise se o texto contém linguagem tóxica, ofensiva, agressiva ou desrespeitosa. Se for tóxico, explique brevemente o motivo e sugira uma reescrita construtiva que preserve a intenção original do autor. Se não for tóxico, retorne toxic: false com reason explicando que o texto é adequado.`
}],
monitor(m) {
m.addEventListener("downloadprogress", (e) => {
const pct = Math.round(e.loaded * 100);
document.getElementById("status-message").textContent =
`⏳ Baixando modelo... ${pct}%`;
});
}
});
this.btnSubmit.disabled = false;
this.hideStatus();
} catch (err) {
this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
return;
}
this.btnSubmit.addEventListener("click", () => this.handleSubmit());
this.btnUseSuggestion.addEventListener("click", () => this.applySuggestion());
this.btnForceSend.addEventListener("click", () => this.forceSend());
this.inputEl.addEventListener("input", () => this.updateCharCount());
this.inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) this.handleSubmit();
});
}
async handleSubmit() {
const text = this.inputEl.value.trim();
if (!text) return;
this.hideAlerts();
this.btnSubmit.disabled = true;
this.btnSubmit.textContent = "🔍 Analisando...";
try {
const raw = await this.session.prompt(
`Analise o seguinte comentário quanto à toxicidade:\n\n"${text}"`,
{ responseConstraint: TOXICITY_SCHEMA }
);
const result = JSON.parse(raw);
if (result.toxic) {
this.showToxicAlert(result);
} else {
this.publishComment();
}
} catch (err) {
if (err.name === "AbortError") return;
this.showStatus(`❌ Erro na análise: ${err.message}`, "error");
} finally {
this.btnSubmit.disabled = false;
this.btnSubmit.textContent = "📤 Enviar Comentário";
}
}
showToxicAlert({ reason, suggestion }) {
this.reasonEl.textContent = reason;
this.suggestionEl.textContent = suggestion;
this.alertToxic.hidden = false;
this.alertToxic.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
applySuggestion() {
this.inputEl.value = this.suggestionEl.textContent;
this.updateCharCount();
this.hideAlerts();
this.inputEl.focus();
}
forceSend() {
this.hideAlerts();
this.publishComment();
}
publishComment() {
this.alertSuccess.hidden = false;
this.inputEl.value = "";
this.updateCharCount();
setTimeout(() => { this.alertSuccess.hidden = true; }, 3000);
}
hideAlerts() {
this.alertToxic.hidden = true;
this.alertSuccess.hidden = true;
}
updateCharCount() {
const len = this.inputEl.value.length;
this.charCount.textContent = `${len} / 1000`;
}
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 ToxicityDetector());
Fluxo UX
- Página carrega → verifica suporte da Prompt API, cria sessão com system prompt de moderador
- Usuário digita comentário → contador de caracteres atualiza em tempo real
- Clica “Enviar” (ou Ctrl+Enter) → botão muda pra “Analisando…”, IA processa
- Texto limpo → comentário “publicado”, feedback verde de sucesso por 3s
- Texto tóxico → alerta vermelho aparece com motivo + sugestão de reescrita
- Usuário escolhe → “Usar Sugestão” substitui o texto no campo; “Enviar Mesmo Assim” publica direto
- Erro → mensagem no status bar sem travar a interface
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Prompt API não existe | Mensagem clara + requisitos mínimos |
| Modelo indisponível | Informa limitação de hardware |
| Texto vazio | Botão não executa (early return) |
| Texto ambíguo (sarcasmo, ironia) | Modelo tende a classificar como tóxico — reason explica |
| Falso positivo (texto legítimo marcado como tóxico) | “Enviar Mesmo Assim” permite override do usuário |
| JSON parse falha | Try/catch com mensagem genérica |
| Sessão abortada | Silencioso (AbortError ignorado) |
| Suggestion vazia ou incoerente | UI mostra mesmo assim — o usuário decide |
| Context overflow em textos longos | maxlength=“1000” previne no HTML |
| Modelo em download | Progress bar com percentual |
CSS Essencial
.alert {
padding: 1.25rem;
border-radius: 0.75rem;
margin-top: 1.5rem;
animation: slideIn 0.3s ease;
}
.alert-toxic {
background: #fef2f2;
border: 1px solid #fecaca;
}
.alert-success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
color: #166534;
}
.alert-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
color: #991b1b;
}
.alert-reason {
color: #7f1d1d;
margin-bottom: 1rem;
}
.suggestion-box {
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
}
.suggestion-label {
font-weight: 600;
margin-bottom: 0.5rem;
color: #166534;
}
.suggestion-box blockquote {
margin: 0;
font-style: italic;
color: #15803d;
}
.alert-actions {
display: flex;
gap: 0.75rem;
}
.btn-secondary {
background: #16a34a;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 600;
}
.btn-ghost {
background: transparent;
border: 1px solid #d1d5db;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
cursor: pointer;
color: #6b7280;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }