Demo: Detector de Toxicidade com IA Local (Prompt API)

intermediário

Analise comentários antes de enviar e receba sugestões de reescrita construtiva. Moderação on-device com Prompt API e JSON Schema via responseConstraint.

Verificando...

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

  1. Página carrega → verifica suporte da Prompt API, cria sessão com system prompt de moderador
  2. Usuário digita comentário → contador de caracteres atualiza em tempo real
  3. Clica “Enviar” (ou Ctrl+Enter) → botão muda pra “Analisando…”, IA processa
  4. Texto limpo → comentário “publicado”, feedback verde de sucesso por 3s
  5. Texto tóxico → alerta vermelho aparece com motivo + sugestão de reescrita
  6. Usuário escolhe → “Usar Sugestão” substitui o texto no campo; “Enviar Mesmo Assim” publica direto
  7. Erro → mensagem no status bar sem travar a interface

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API não existeMensagem clara + requisitos mínimos
Modelo indisponívelInforma limitação de hardware
Texto vazioBotã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 falhaTry/catch com mensagem genérica
Sessão abortadaSilencioso (AbortError ignorado)
Suggestion vazia ou incoerenteUI mostra mesmo assim — o usuário decide
Context overflow em textos longosmaxlength=“1000” previne no HTML
Modelo em downloadProgress 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; }