Demo: Escritor de E-mails Profissionais (Prompt API)
inicianteGere e-mails completos com tom personalizável usando IA local no navegador. Escolha formal, casual ou assertivo e refine com um clique via Prompt API.
Visão Geral
Demo que gera e-mails profissionais completos a partir de um tópico e tom selecionado. O usuário escolhe entre formal, casual ou assertivo, descreve o assunto, e recebe um e-mail pronto com streaming em tempo real. Botões de refinamento permitem ajustar sem reescrever do zero.
Pra quem: Desenvolvedores aprendendo promptStreaming e initialPrompts com system prompts dinâmicos.
Técnica principal: initialPrompts com system prompt que define o estilo de escrita + promptStreaming para output gradual + refinamento iterativo na mesma sessão.
Wireframe
┌─────────────────────────────────────────────────────┐
│ ✉️ Escritor de E-mails Profissionais │
├─────────────────────────────────────────────────────┤
│ │
│ Tom: [ Formal ▾ ] │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ input (placeholder: "Descreva o assunto...") │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ [ ✉️ Gerar E-mail ] │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ │ │
│ │ Prezado(a) João, │ │
│ │ │ │
│ │ Escrevo para informar que... ▌ (streaming) │ │
│ │ │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ [Mais curto] [Mais formal] [Traduzir p/ inglês] │
│ │
│ ⚠️ Requer Chrome 148+ com Prompt API habilitada │
└─────────────────────────────────────────────────────┘
HTML
<section class="demo-container" id="escritor-emails">
<h2>✉️ Escritor de E-mails Profissionais</h2>
<div id="status-bar" class="status" hidden>
<span id="status-message"></span>
</div>
<div class="input-group">
<label for="select-tom">Tom:</label>
<select id="select-tom">
<option value="formal">Formal</option>
<option value="casual">Casual</option>
<option value="assertivo">Assertivo</option>
</select>
</div>
<textarea
id="input-topico"
placeholder="Descreva o assunto do e-mail... Ex: cobrar resposta de proposta enviada há 5 dias"
rows="3"
maxlength="500"
></textarea>
<div class="actions">
<button id="btn-gerar" disabled>✉️ Gerar E-mail</button>
</div>
<div id="output-area" class="output-box" hidden>
<pre id="email-output"></pre>
</div>
<div id="refine-actions" class="refine-buttons" hidden>
<button id="btn-curto" class="btn-refine">✂️ Mais curto</button>
<button id="btn-formal" class="btn-refine">👔 Mais formal</button>
<button id="btn-traduzir" class="btn-refine">🌐 Traduzir p/ inglês</button>
</div>
</section>
Código JavaScript
const SYSTEM_PROMPTS = {
formal: "Você é um escritor de e-mails corporativos. Escreva de forma polida, respeitosa e profissional. Use 'Prezado(a)', evite gírias, mantenha parágrafos curtos.",
casual: "Você é um escritor de e-mails informais. Escreva de forma leve e amigável, como se falasse com um colega próximo. Use 'Oi' ou 'E aí', pode usar expressões coloquiais.",
assertivo: "Você é um escritor de e-mails diretos e assertivos. Vá direto ao ponto, use frases curtas, tom firme mas educado. Sem rodeios, sem floreios excessivos."
};
class EmailWriter {
constructor() {
this.session = null;
this.currentEmail = "";
this.tomEl = document.getElementById("select-tom");
this.topicoEl = document.getElementById("input-topico");
this.btnGerar = document.getElementById("btn-gerar");
this.outputArea = document.getElementById("output-area");
this.emailOutput = document.getElementById("email-output");
this.refineActions = document.getElementById("refine-actions");
this.statusBar = document.getElementById("status-bar");
this.statusMessage = document.getElementById("status-message");
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... Aguarde.", "loading");
}
try {
await this.createSession();
this.btnGerar.disabled = false;
this.hideStatus();
} catch (err) {
this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
return;
}
this.btnGerar.addEventListener("click", () => this.generate());
this.tomEl.addEventListener("change", () => this.createSession());
document.getElementById("btn-curto").addEventListener("click", () =>
this.refine("Reescreva o e-mail acima de forma mais curta e objetiva, mantendo a mensagem principal.")
);
document.getElementById("btn-formal").addEventListener("click", () =>
this.refine("Reescreva o e-mail acima com tom mais formal e corporativo.")
);
document.getElementById("btn-traduzir").addEventListener("click", () =>
this.refine("Traduza o e-mail acima para inglês profissional, mantendo o tom.")
);
}
async createSession() {
if (this.session) this.session.destroy();
const tom = this.tomEl.value;
this.session = await LanguageModel.create({
initialPrompts: [{
role: "system",
content: SYSTEM_PROMPTS[tom]
}],
monitor(m) {
m.addEventListener("downloadprogress", (e) => {
const pct = Math.round(e.loaded * 100);
document.getElementById("status-message").textContent =
`⏳ Baixando modelo... ${pct}%`;
});
}
});
}
async generate() {
const topico = this.topicoEl.value.trim();
if (!topico) return;
this.setLoading(true);
this.outputArea.hidden = false;
this.refineActions.hidden = true;
this.emailOutput.textContent = "";
this.currentEmail = "";
try {
const stream = await this.session.promptStreaming(
`Escreva um e-mail sobre: ${topico}\n\nInclua saudação, corpo e despedida.`
);
for await (const chunk of stream) {
this.currentEmail = chunk;
this.emailOutput.textContent = chunk;
}
this.refineActions.hidden = false;
} catch (err) {
if (err.name !== "AbortError") {
this.showStatus(`❌ Erro: ${err.message}`, "error");
}
} finally {
this.setLoading(false);
}
}
async refine(instruction) {
this.setLoading(true);
this.emailOutput.textContent = "";
this.currentEmail = "";
try {
const stream = await this.session.promptStreaming(instruction);
for await (const chunk of stream) {
this.currentEmail = chunk;
this.emailOutput.textContent = chunk;
}
} catch (err) {
if (err.name !== "AbortError") {
this.showStatus(`❌ Erro ao refinar: ${err.message}`, "error");
}
} finally {
this.setLoading(false);
}
}
setLoading(loading) {
this.btnGerar.disabled = loading;
this.btnGerar.textContent = loading ? "⏳ Gerando..." : "✉️ Gerar E-mail";
document.querySelectorAll(".btn-refine").forEach(
btn => btn.disabled = loading
);
}
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 EmailWriter());
Fluxo UX
- Página carrega → verifica suporte da Prompt API, cria sessão com system prompt do tom padrão (formal)
- Usuário seleciona tom → sessão é recriada com novo system prompt correspondente
- Usuário descreve o tópico → campo livre, até 500 caracteres
- Clica “Gerar E-mail” → botão desabilita, output aparece com streaming em tempo real
- E-mail completo → botões de refinamento aparecem abaixo do output
- Clica “Mais curto” / “Mais formal” / “Traduzir” → novo prompt na mesma sessão, streaming substitui o texto anterior
- Erro → mensagem no status bar, botões reabilitados
Edge Cases e Tratamento de Erros
| Cenário | Tratamento |
|---|---|
| Prompt API não existe | Mensagem clara + requisitos mínimos |
| Modelo indisponível | Informa incompatibilidade de hardware |
| Tópico vazio | generate() retorna sem ação |
| Tópico muito vago (“e-mail”) | Modelo gera algo genérico — UX aceitável |
| Troca de tom durante geração | createSession() destrói a anterior; geração em andamento pode falhar com AbortError (tratado) |
| Context window estourada após muitos refinamentos | Recriar sessão com createSession() e informar o usuário |
| Streaming interrompido | AbortError silenciado, botões reabilitados |
| Modelo em download | Progress bar com percentual no status |
CSS Essencial
.input-group {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.input-group select {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #d1d5db;
font-size: 0.95rem;
}
.output-box {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1.5rem;
margin-top: 1rem;
min-height: 200px;
}
.output-box pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: inherit;
margin: 0;
line-height: 1.6;
}
.refine-buttons {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.btn-refine {
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid #d1d5db;
background: #fff;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.2s;
}
.btn-refine:hover:not(:disabled) {
background: #f3f4f6;
}
.btn-refine:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }