Demo: Resumidor de Textos com Streaming e IA On-Device
inicianteResuma artigos e textos longos em 3-5 frases com streaming em tempo real usando a Prompt API. Inferência local via Gemini Nano — sem custo e sem enviar dados.
Visão Geral
Demo que resume textos longos (artigos, emails, documentos) em 3-5 frases, mostrando o resultado em tempo real via streaming. O efeito de “digitação” ao vivo é mais satisfatório do que parece — o usuário sente que algo tá acontecendo.
Pra quem: Desenvolvedores que querem aprender streaming com a Prompt API.
Técnica principal: promptStreaming() retornando ReadableStream com chunks concatenados progressivamente.
Wireframe
┌─────────────────────────────────────────────────────────┐
│ 📝 Resumidor de Texto │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ textarea grande │ │
│ │ (placeholder: "Cole aqui o texto para │ │
│ │ resumir... Artigos, emails, documentos") │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ Caracteres: 1.450 [ ✂️ Resumir Texto ] │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 📋 Resumo: │ │
│ │ │ │
│ │ O texto aborda as mudanças climáticas e seu │ │
│ │ impacto na agricultura brasileira. Os dados │ │
│ │ mostram que...█ ← cursor streaming │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ⚠️ Requer Chrome 148+ com Prompt API habilitada │
└─────────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="resumidor">
<h2>📝 Resumidor de Texto</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<textarea
id="input-text"
placeholder="Cole aqui o texto para resumir... Artigos, emails, documentos."
rows="10"
maxlength="10000"
></textarea>
<div class="actions">
<span id="char-count">0 caracteres</span>
<button id="btn-summarize" disabled>✂️ Resumir Texto</button>
<button id="btn-stop" hidden>⏹️ Parar</button>
</div>
<div id="result" class="result-box" hidden>
<h3>📋 Resumo:</h3>
<p id="summary-output" class="streaming-text"></p>
</div>
</section>
Código JavaScript
class TextSummarizer {
constructor() {
this.session = null;
this.controller = null;
this.inputEl = document.getElementById("input-text");
this.btnEl = document.getElementById("btn-summarize");
this.btnStop = document.getElementById("btn-stop");
this.resultEl = document.getElementById("result");
this.outputEl = document.getElementById("summary-output");
this.statusBar = document.getElementById("status-bar");
this.statusMessage = document.getElementById("status-message");
this.charCount = document.getElementById("char-count");
this.init();
}
async init() {
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...", "loading");
}
try {
this.session = await LanguageModel.create({
expectedInputs: [{ type: "text", languages: ["pt", "en"] }],
expectedOutputs: [{ type: "text", languages: ["pt"] }],
initialPrompts: [{
role: "system",
content: `Você é um resumidor especialista. Resuma o texto fornecido em 3 a 5 frases concisas em português brasileiro. Mantenha as informações mais importantes. Não adicione opinião. Não repita o título.`
}],
monitor(m) {
m.addEventListener("downloadprogress", (e) => {
document.getElementById("status-message").textContent =
`⏳ Baixando modelo... ${Math.round(e.loaded * 100)}%`;
});
}
});
this.btnEl.disabled = false;
this.hideStatus();
} catch (err) {
this.showStatus(`❌ Erro: ${err.message}`, "error");
return;
}
this.btnEl.addEventListener("click", () => this.summarize());
this.btnStop.addEventListener("click", () => this.stop());
this.inputEl.addEventListener("input", () => this.updateCharCount());
this.inputEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) this.summarize();
});
}
async summarize() {
const text = this.inputEl.value.trim();
if (!text) return;
if (text.length < 50) {
this.showStatus("⚠️ Texto muito curto para resumir (mínimo 50 caracteres).", "error");
return;
}
this.controller = new AbortController();
this.btnEl.hidden = true;
this.btnStop.hidden = false;
this.resultEl.hidden = false;
this.outputEl.textContent = "";
this.outputEl.classList.add("streaming");
try {
const stream = this.session.promptStreaming(
`Resuma o seguinte texto em 3 a 5 frases:\n\n${text}`,
{ signal: this.controller.signal }
);
for await (const chunk of stream) {
this.outputEl.textContent = chunk;
}
} catch (err) {
if (err.name !== "AbortError") {
this.showStatus(`❌ Erro: ${err.message}`, "error");
}
} finally {
this.outputEl.classList.remove("streaming");
this.btnEl.hidden = false;
this.btnStop.hidden = true;
}
}
stop() {
this.controller?.abort();
}
updateCharCount() {
const len = this.inputEl.value.length;
this.charCount.textContent = `${len.toLocaleString("pt-BR")} caracteres`;
}
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 TextSummarizer());
Fluxo UX
- Página carrega → verifica Prompt API, cria sessão com system prompt de sumarização
- Usuário cola texto → contador mostra quantidade de caracteres
- Clica “Resumir” (ou Ctrl+Enter) → botão troca para “Parar”, área de resultado aparece
- Streaming inicia → texto aparece progressivamente com cursor piscante via CSS
- Streaming completo → cursor desaparece, botão “Resumir” retorna
- Clica “Parar” → AbortController cancela, resultado parcial permanece visível
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Texto com menos de 50 caracteres | Aviso amigável, não executa |
| Texto com 10.000+ caracteres | maxlength no HTML impede; se context overflow, session trata |
| Usuário clica “Parar” durante streaming | AbortController cancela; resultado parcial visível |
| Sessão perde contexto (context overflow) | Modelo descarta mensagens antigas automaticamente |
| Modelo retorna texto em inglês | System prompt força PT-BR; expectedOutputs reforça |
| Múltiplos cliques rápidos | Botão hidden durante execução impede duplo-click |
CSS Essencial
.streaming-text {
white-space: pre-wrap;
line-height: 1.6;
min-height: 80px;
}
.streaming::after {
content: "█";
animation: blink 0.7s steps(1) infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
#btn-stop {
background: #dc2626;
color: white;
}
Notas de Implementação
- O
promptStreaming()retorna umReadableStreamonde cada chunk é a resposta completa até aquele ponto (não deltas). Basta atribuirtextContent = chunkdiretamente. - O system prompt é definido uma vez na sessão. Cada chamada a
promptStreaming()reutiliza o contexto. - Para textos muito grandes que excedem a janela de contexto, considere truncar ou dividir em partes.