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á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;
  }
}