Demo: Escritor de E-mails Profissionais (Prompt API)

iniciante

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

Verificando...

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

  1. Página carrega → verifica suporte da Prompt API, cria sessão com system prompt do tom padrão (formal)
  2. Usuário seleciona tom → sessão é recriada com novo system prompt correspondente
  3. Usuário descreve o tópico → campo livre, até 500 caracteres
  4. Clica “Gerar E-mail” → botão desabilita, output aparece com streaming em tempo real
  5. E-mail completo → botões de refinamento aparecem abaixo do output
  6. Clica “Mais curto” / “Mais formal” / “Traduzir” → novo prompt na mesma sessão, streaming substitui o texto anterior
  7. Erro → mensagem no status bar, botões reabilitados

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API não existeMensagem clara + requisitos mínimos
Modelo indisponívelInforma incompatibilidade de hardware
Tópico vaziogenerate() retorna sem ação
Tópico muito vago (“e-mail”)Modelo gera algo genérico — UX aceitável
Troca de tom durante geraçãocreateSession() destrói a anterior; geração em andamento pode falhar com AbortError (tratado)
Context window estourada após muitos refinamentosRecriar sessão com createSession() e informar o usuário
Streaming interrompidoAbortError silenciado, botões reabilitados
Modelo em downloadProgress 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; }