Demo: Classificador de Sentimento com IA Local (Prompt API)

iniciante

Classifique textos como positivo, negativo ou neutro usando IA on-device no navegador. Structured output com JSON Schema via Prompt API garante consistência.

Verificando...

Visão Geral

Demo que classifica o sentimento de um texto em Positivo, Negativo ou Neutro, devolvendo também um score de confiança. Roda 100% no navegador usando a Prompt API com responseConstraint pra garantir output estruturado — sem gambiarras de parsing.

Pra quem: Desenvolvedores aprendendo structured output com a Prompt API.

Técnica principal: responseConstraint com JSON Schema forçando o modelo a retornar um objeto com sentiment (enum) e confidence (number).


Wireframe

┌─────────────────────────────────────────────────────┐
│  🎭 Classificador de Sentimento                     │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │ textarea (placeholder: "Digite um texto...")  │  │
│  │                                               │  │
│  │                                               │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  [ 🔍 Classificar Sentimento ]                      │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │          ┌──────────────┐                     │  │
│  │          │  😊 POSITIVO │  ← badge colorido   │  │
│  │          └──────────────┘                     │  │
│  │          Confiança: 92%                       │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ⚠️ Requer Chrome 148+ com Prompt API habilitada   │
└─────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="classificador-sentimento">
  <h2>🎭 Classificador de Sentimento</h2>

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

  <textarea
    id="input-text"
    placeholder="Digite ou cole um texto para analisar o sentimento..."
    rows="5"
    maxlength="2000"
  ></textarea>

  <div class="actions">
    <button id="btn-classify" disabled>🔍 Classificar Sentimento</button>
    <span id="char-count">0 / 2000</span>
  </div>

  <div id="result" class="result-box" hidden>
    <div id="sentiment-badge" class="badge"></div>
    <div id="confidence-bar">
      <div id="confidence-fill"></div>
    </div>
    <p id="confidence-text"></p>
  </div>
</section>

Código JavaScript

const SENTIMENT_SCHEMA = {
  type: "object",
  properties: {
    sentiment: {
      type: "string",
      enum: ["positivo", "negativo", "neutro"]
    },
    confidence: {
      type: "number",
      minimum: 0,
      maximum: 1
    }
  },
  required: ["sentiment", "confidence"],
  additionalProperties: false
};

const BADGE_CONFIG = {
  positivo: { emoji: "😊", color: "#16a34a", bg: "#dcfce7" },
  negativo: { emoji: "😠", color: "#dc2626", bg: "#fee2e2" },
  neutro:   { emoji: "😐", color: "#6b7280", bg: "#f3f4f6" }
};

class SentimentClassifier {
  constructor() {
    this.session = null;
    this.inputEl = document.getElementById("input-text");
    this.btnEl = document.getElementById("btn-classify");
    this.resultEl = document.getElementById("result");
    this.badgeEl = document.getElementById("sentiment-badge");
    this.confidenceText = document.getElementById("confidence-text");
    this.confidenceFill = document.getElementById("confidence-fill");
    this.statusBar = document.getElementById("status-bar");
    this.statusMessage = document.getElementById("status-message");
    this.charCount = document.getElementById("char-count");

    this.init();
  }

  async init() {
    // Verificar suporte
    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 classificador de sentimento. Analise o texto e classifique como "positivo", "negativo" ou "neutro". Retorne também um score de confiança entre 0 e 1.`
        }],
        monitor(m) {
          m.addEventListener("downloadprogress", (e) => {
            const pct = Math.round(e.loaded * 100);
            document.getElementById("status-message").textContent =
              `⏳ Baixando modelo... ${pct}%`;
          });
        }
      });

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

    // Event listeners
    this.btnEl.addEventListener("click", () => this.classify());
    this.inputEl.addEventListener("input", () => this.updateCharCount());
    this.inputEl.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && e.ctrlKey) this.classify();
    });
  }

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

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

    try {
      const raw = await this.session.prompt(
        `Classifique o sentimento do seguinte texto:\n\n"${text}"`,
        { responseConstraint: SENTIMENT_SCHEMA }
      );

      const result = JSON.parse(raw);
      this.showResult(result);
    } catch (err) {
      if (err.name === "AbortError") return;
      this.showStatus(`❌ Erro: ${err.message}`, "error");
    } finally {
      this.btnEl.disabled = false;
      this.btnEl.textContent = "🔍 Classificar Sentimento";
    }
  }

  showResult({ sentiment, confidence }) {
    const config = BADGE_CONFIG[sentiment];
    const pct = Math.round(confidence * 100);

    this.badgeEl.textContent = `${config.emoji} ${sentiment.toUpperCase()}`;
    this.badgeEl.style.backgroundColor = config.bg;
    this.badgeEl.style.color = config.color;

    this.confidenceFill.style.width = `${pct}%`;
    this.confidenceFill.style.backgroundColor = config.color;
    this.confidenceText.textContent = `Confiança: ${pct}%`;

    this.resultEl.hidden = false;
  }

  updateCharCount() {
    const len = this.inputEl.value.length;
    this.charCount.textContent = `${len} / 2000`;
  }

  showStatus(msg, type) {
    this.statusBar.hidden = false;
    this.statusBar.className = `status status-${type}`;
    this.statusMessage.textContent = msg;
  }

  hideStatus() {
    this.statusBar.hidden = true;
  }
}

// Inicializar quando DOM estiver pronto
document.addEventListener("DOMContentLoaded", () => new SentimentClassifier());

Fluxo UX

  1. Página carrega → verifica suporte da Prompt API
  2. Modelo disponível → botão habilitado; se downloading, mostra progresso
  3. Usuário digita texto → contador de caracteres atualiza
  4. Clica “Classificar” (ou Ctrl+Enter) → botão desabilita, mostra “Analisando…”
  5. Resposta chega → badge aparece com cor e emoji + barra de confiança animada
  6. Erro → mensagem amigável no status bar

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API não existe (window.LanguageModel undefined)Mensagem clara + link para requisitos
Modelo indisponível (availability === "unavailable")Informa hardware/SO incompatível
Texto vazioBotão não executa, sem feedback negativo
Texto muito curto (1-2 palavras)Funciona normalmente, confiança tende a ser menor
JSON parse falhaTry/catch com mensagem genérica ao usuário
Sessão abortadaSilencioso (AbortError ignorado)
Context overflowRecria sessão automaticamente
Modelo em downloadProgress bar com percentual

CSS Essencial

.badge {
  display: inline-block;
  padding: 0.75rem 1.5rem;
  border-radius: 9999px;
  font-weight: 700;
  font-size: 1.25rem;
}

#confidence-bar {
  width: 100%;
  height: 8px;
  background: #e5e7eb;
  border-radius: 4px;
  margin-top: 1rem;
  overflow: hidden;
}

#confidence-fill {
  height: 100%;
  border-radius: 4px;
  transition: width 0.5s ease;
}

.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }