Demo: Gerenciamento de Sessão com Prompt API
intermediárioControle o ciclo de vida de sessões da Prompt API: crie, clone, conte tokens, monitore a context window e destrua sessões. Painel visual mostra uso em tempo real.
Visão Geral
Demo que expõe o ciclo de vida completo de uma sessão na Prompt API: criação, clone, contagem de tokens, monitoramento da context window e destruição controlada. Um painel lateral mostra em tempo real quantos tokens foram consumidos versus o limite disponível — essencial pra evitar que sua app quebre silenciosamente quando a janela de contexto enche.
Pra quem: Desenvolvedores que já fizeram um “Hello World” com a Prompt API e querem entender como gerenciar sessões de forma robusta em produção.
Técnicas principais:
session.countPromptTokens()— contar tokens antes de enviarsession.clone()— duplicar sessão preservando contextosession.destroy()— liberar recursos explicitamente- Monitoramento de
tokensSoFar/maxTokenspra context window management
Wireframe
┌─────────────────────────────────────────────────────────┐
│ 🧠 Gerenciamento de Sessão │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
│ │ CONTROLES │ │ MONITOR DE TOKENS │ │
│ │ │ │ │ │
│ │ [Criar Sessão] │ │ Usados: 1.204 / 4.096 │ │
│ │ [Clonar Sessão] │ │ ████████░░░░░░░ 29% │ │
│ │ [Destruir Sessão] │ │ │ │
│ │ │ │ Status: ● Ativa │ │
│ │ ─────────────────── │ │ Sessões ativas: 2 │ │
│ │ │ │ │ │
│ │ ┌─────────────────┐ │ │ Último prompt: 48 tokens │ │
│ │ │ input prompt │ │ │ │ │
│ │ └─────────────────┘ │ └──────────────────────────┘ │
│ │ [Contar Tokens] │ │
│ │ [Enviar] │ ┌──────────────────────────┐ │
│ │ │ │ LOG DE EVENTOS │ │
│ │ ─────────────────── │ │ │ │
│ │ Resposta: │ │ 10:32 Sessão criada │ │
│ │ "O céu é azul..." │ │ 10:33 Prompt: 48 tokens │ │
│ │ │ │ 10:33 Resposta gerada │ │
│ └─────────────────────┘ │ 10:34 Sessão clonada │ │
│ └──────────────────────────┘ │
│ │
│ ⚠️ Requer Chrome 148+ com Prompt API habilitada │
└─────────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="session-manager">
<h2>🧠 Gerenciamento de Sessão</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<div class="layout-grid">
<!-- Controles -->
<div class="panel panel-controls">
<h3>Controles</h3>
<div class="btn-group">
<button id="btn-create">➕ Criar Sessão</button>
<button id="btn-clone" disabled>📋 Clonar Sessão</button>
<button id="btn-destroy" disabled>🗑️ Destruir Sessão</button>
</div>
<hr />
<textarea
id="input-prompt"
placeholder="Digite um prompt para testar a sessão..."
rows="3"
disabled
></textarea>
<div class="btn-group">
<button id="btn-count" disabled>🔢 Contar Tokens</button>
<button id="btn-send" disabled>📤 Enviar</button>
</div>
<div id="response-area" class="response" hidden>
<strong>Resposta:</strong>
<p id="response-text"></p>
</div>
</div>
<!-- Monitor -->
<div class="panel panel-monitor">
<h3>Monitor de Tokens</h3>
<div class="token-display">
<span id="tokens-used">0</span>
<span class="separator">/</span>
<span id="tokens-max">—</span>
</div>
<div class="progress-bar">
<div id="progress-fill" class="progress-fill"></div>
</div>
<p id="progress-pct" class="progress-label">0%</p>
<dl class="stats">
<dt>Status</dt>
<dd id="session-status">● Inativa</dd>
<dt>Sessões ativas</dt>
<dd id="session-count">0</dd>
<dt>Último prompt</dt>
<dd id="last-token-count">— tokens</dd>
</dl>
<h3>Log de Eventos</h3>
<ul id="event-log" class="event-log"></ul>
</div>
</div>
</section>
Código JavaScript
class SessionManager {
constructor() {
this.sessions = [];
this.activeSession = null;
// DOM
this.btnCreate = document.getElementById("btn-create");
this.btnClone = document.getElementById("btn-clone");
this.btnDestroy = document.getElementById("btn-destroy");
this.btnCount = document.getElementById("btn-count");
this.btnSend = document.getElementById("btn-send");
this.inputPrompt = document.getElementById("input-prompt");
this.responseArea = document.getElementById("response-area");
this.responseText = document.getElementById("response-text");
this.tokensUsed = document.getElementById("tokens-used");
this.tokensMax = document.getElementById("tokens-max");
this.progressFill = document.getElementById("progress-fill");
this.progressPct = document.getElementById("progress-pct");
this.sessionStatus = document.getElementById("session-status");
this.sessionCount = document.getElementById("session-count");
this.lastTokenCount = document.getElementById("last-token-count");
this.eventLog = document.getElementById("event-log");
this.statusBar = document.getElementById("status-bar");
this.statusMessage = document.getElementById("status-message");
this.bindEvents();
this.checkSupport();
}
bindEvents() {
this.btnCreate.addEventListener("click", () => this.createSession());
this.btnClone.addEventListener("click", () => this.cloneSession());
this.btnDestroy.addEventListener("click", () => this.destroySession());
this.btnCount.addEventListener("click", () => this.countTokens());
this.btnSend.addEventListener("click", () => this.sendPrompt());
this.inputPrompt.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) this.sendPrompt();
});
}
async checkSupport() {
if (!("LanguageModel" in window)) {
this.showStatus("❌ Prompt API não disponível. Use Chrome 148+.", "error");
this.btnCreate.disabled = true;
return;
}
const availability = await LanguageModel.availability();
if (availability === "unavailable") {
this.showStatus("❌ Modelo não disponível neste dispositivo.", "error");
this.btnCreate.disabled = true;
}
}
async createSession() {
this.btnCreate.disabled = true;
this.log("Criando sessão...");
try {
const session = await LanguageModel.create({
initialPrompts: [{
role: "system",
content: "Você é um assistente prestativo. Responda de forma concisa em português."
}]
});
this.activeSession = session;
this.sessions.push(session);
this.updateUI();
this.log(`✅ Sessão criada (max: ${session.maxTokens} tokens)`);
} catch (err) {
this.showStatus(`❌ Erro: ${err.message}`, "error");
this.log(`❌ Falha ao criar: ${err.message}`);
} finally {
this.btnCreate.disabled = false;
}
}
async cloneSession() {
if (!this.activeSession) return;
this.log("Clonando sessão...");
try {
const cloned = await this.activeSession.clone();
this.sessions.push(cloned);
this.activeSession = cloned;
this.updateUI();
this.log("✅ Sessão clonada (contexto preservado)");
} catch (err) {
this.log(`❌ Clone falhou: ${err.message}`);
}
}
destroySession() {
if (!this.activeSession) return;
this.activeSession.destroy();
this.sessions = this.sessions.filter((s) => s !== this.activeSession);
this.log("🗑️ Sessão destruída");
// Ativar última sessão restante ou null
this.activeSession = this.sessions.at(-1) || null;
this.updateUI();
}
async countTokens() {
const text = this.inputPrompt.value.trim();
if (!text || !this.activeSession) return;
try {
const count = await this.activeSession.countPromptTokens(text);
this.lastTokenCount.textContent = `${count} tokens`;
this.log(`🔢 Prompt atual: ${count} tokens`);
} catch (err) {
this.log(`❌ Contagem falhou: ${err.message}`);
}
}
async sendPrompt() {
const text = this.inputPrompt.value.trim();
if (!text || !this.activeSession) return;
this.setPromptUIState(false);
this.responseArea.hidden = true;
try {
// Contar tokens antes de enviar
const promptTokens = await this.activeSession.countPromptTokens(text);
this.log(`📤 Enviando (${promptTokens} tokens)...`);
const response = await this.activeSession.prompt(text);
this.responseText.textContent = response;
this.responseArea.hidden = false;
this.updateTokenBar();
this.log("✅ Resposta recebida");
} catch (err) {
if (err.name === "QuotaExceededError") {
this.log("⚠️ Context window cheia! Destrua e recrie a sessão.");
this.showStatus("Context window esgotada. Destrua a sessão.", "error");
} else {
this.log(`❌ Erro: ${err.message}`);
}
} finally {
this.setPromptUIState(true);
}
}
updateTokenBar() {
if (!this.activeSession) return;
const used = this.activeSession.tokensSoFar;
const max = this.activeSession.maxTokens;
const pct = Math.round((used / max) * 100);
this.tokensUsed.textContent = used.toLocaleString("pt-BR");
this.tokensMax.textContent = max.toLocaleString("pt-BR");
this.progressFill.style.width = `${pct}%`;
this.progressPct.textContent = `${pct}%`;
// Alerta visual quando passa de 80%
this.progressFill.classList.toggle("warning", pct > 80);
}
updateUI() {
const hasSession = this.activeSession !== null;
this.btnClone.disabled = !hasSession;
this.btnDestroy.disabled = !hasSession;
this.btnCount.disabled = !hasSession;
this.btnSend.disabled = !hasSession;
this.inputPrompt.disabled = !hasSession;
this.sessionCount.textContent = this.sessions.length;
this.sessionStatus.textContent = hasSession ? "● Ativa" : "● Inativa";
this.sessionStatus.className = hasSession ? "active" : "inactive";
if (hasSession) {
this.updateTokenBar();
this.hideStatus();
} else {
this.tokensUsed.textContent = "0";
this.tokensMax.textContent = "—";
this.progressFill.style.width = "0%";
this.progressPct.textContent = "0%";
}
}
setPromptUIState(enabled) {
this.btnSend.disabled = !enabled;
this.btnCount.disabled = !enabled;
this.inputPrompt.disabled = !enabled;
this.btnSend.textContent = enabled ? "📤 Enviar" : "⏳ Gerando...";
}
log(msg) {
const li = document.createElement("li");
const time = new Date().toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
li.textContent = `${time} — ${msg}`;
this.eventLog.prepend(li);
// Manter no máximo 20 entradas
while (this.eventLog.children.length > 20) {
this.eventLog.lastChild.remove();
}
}
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 SessionManager());
Fluxo UX
- Página carrega → verifica suporte da Prompt API; se indisponível, desabilita tudo com mensagem clara
- Usuário clica “Criar Sessão” → sessão instanciada, painel de tokens mostra
maxTokens, controles habilitam - Digita prompt → pode clicar “Contar Tokens” pra ver o custo antes de enviar
- Envia prompt → tokens contados, resposta exibida, barra de progresso atualiza
tokensSoFar - Clona sessão → nova sessão com todo contexto anterior, útil pra “salvar checkpoint”
- Barra passa de 80% → alerta visual (cor muda pra amarelo/vermelho)
- Context window cheia → erro
QuotaExceededErrorcapturado, mensagem sugere destruir e recriar - Destrói sessão → recursos liberados, fallback pra última sessão ativa ou estado “Inativa”
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
LanguageModel não existe no window | Mensagem + desabilita todos os controles |
availability === "unavailable" | Informa incompatibilidade de hardware |
| Usuário tenta enviar sem sessão ativa | Botão desabilitado (prevenção por UI) |
QuotaExceededError (context window cheia) | Log + mensagem sugerindo destroy/recreate |
clone() falha (sessão já destruída) | Try/catch com log informativo |
| Múltiplas sessões abertas (vazamento) | Array rastreia todas; destroy limpa referência |
| Texto vazio no prompt | Guard clause impede execução |
countPromptTokens() falha | Catch silencioso com log |
| Sessão ativa destruída por outro código | UI atualiza pra estado “Inativa” |
CSS Essencial
.layout-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.panel {
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fafafa;
}
.btn-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 0.75rem 0;
}
.btn-group button {
padding: 0.5rem 1rem;
border-radius: 8px;
border: 1px solid #d1d5db;
background: white;
cursor: pointer;
font-size: 0.875rem;
transition: background 0.2s;
}
.btn-group button:hover:not(:disabled) {
background: #f3f4f6;
}
.btn-group button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Token progress bar */
.token-display {
font-size: 1.5rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.progress-bar {
width: 100%;
height: 12px;
background: #e5e7eb;
border-radius: 6px;
margin: 0.75rem 0 0.25rem;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #2563eb;
border-radius: 6px;
transition: width 0.4s ease;
}
.progress-fill.warning {
background: #dc2626;
}
.progress-label {
font-size: 0.75rem;
color: #6b7280;
}
/* Stats */
.stats {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
margin: 1rem 0;
font-size: 0.875rem;
}
.stats dt {
color: #6b7280;
}
.stats dd {
font-weight: 600;
margin: 0;
}
.active { color: #16a34a; }
.inactive { color: #6b7280; }
/* Event log */
.event-log {
list-style: none;
padding: 0;
margin: 0.5rem 0;
max-height: 200px;
overflow-y: auto;
font-size: 0.8rem;
font-family: monospace;
}
.event-log li {
padding: 0.25rem 0;
border-bottom: 1px solid #f3f4f6;
}
/* Response */
.response {
margin-top: 1rem;
padding: 1rem;
background: white;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
/* Responsive */
@media (max-width: 768px) {
.layout-grid {
grid-template-columns: 1fr;
}
}