Demo: Gerenciamento de Sessão com Prompt API

intermediário

Controle 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.

Verificando...

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 enviar
  • session.clone() — duplicar sessão preservando contexto
  • session.destroy() — liberar recursos explicitamente
  • Monitoramento de tokensSoFar / maxTokens pra 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

  1. Página carrega → verifica suporte da Prompt API; se indisponível, desabilita tudo com mensagem clara
  2. Usuário clica “Criar Sessão” → sessão instanciada, painel de tokens mostra maxTokens, controles habilitam
  3. Digita prompt → pode clicar “Contar Tokens” pra ver o custo antes de enviar
  4. Envia prompt → tokens contados, resposta exibida, barra de progresso atualiza tokensSoFar
  5. Clona sessão → nova sessão com todo contexto anterior, útil pra “salvar checkpoint”
  6. Barra passa de 80% → alerta visual (cor muda pra amarelo/vermelho)
  7. Context window cheia → erro QuotaExceededError capturado, mensagem sugere destruir e recriar
  8. Destrói sessão → recursos liberados, fallback pra última sessão ativa ou estado “Inativa”

Edge Cases e Tratamento de Erros

CenárioTratamento
LanguageModel não existe no windowMensagem + desabilita todos os controles
availability === "unavailable"Informa incompatibilidade de hardware
Usuário tenta enviar sem sessão ativaBotã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 promptGuard clause impede execução
countPromptTokens() falhaCatch silencioso com log
Sessão ativa destruída por outro códigoUI 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;
  }
}