Demo: Classificador de Sentimento com IA Local (Prompt API)
inicianteClassifique textos como positivo, negativo ou neutro usando IA on-device no navegador. Structured output com JSON Schema via Prompt API garante consistência.
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
- Página carrega → verifica suporte da Prompt API
- Modelo disponível → botão habilitado; se downloading, mostra progresso
- Usuário digita texto → contador de caracteres atualiza
- Clica “Classificar” (ou Ctrl+Enter) → botão desabilita, mostra “Analisando…”
- Resposta chega → badge aparece com cor e emoji + barra de confiança animada
- Erro → mensagem amigável no status bar
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
Prompt API não existe (window.LanguageModel undefined) | Mensagem clara + link para requisitos |
Modelo indisponível (availability === "unavailable") | Informa hardware/SO incompatível |
| Texto vazio | Botão não executa, sem feedback negativo |
| Texto muito curto (1-2 palavras) | Funciona normalmente, confiança tende a ser menor |
| JSON parse falha | Try/catch com mensagem genérica ao usuário |
| Sessão abortada | Silencioso (AbortError ignorado) |
| Context overflow | Recria sessão automaticamente |
| Modelo em download | Progress 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; }