Demo: Buscador de Sinônimos com IA Local (Prompt API)

iniciante

Digite uma palavra e receba sinônimos em PT-BR direto no navegador. Demonstra system prompt para restringir output e chips selecionáveis com a Prompt API.

Verificando...

Visão Geral

Demo que recebe uma palavra em português e devolve sinônimos contextualmente relevantes, apresentados como chips clicáveis. Tudo roda no navegador via Prompt API — sem servidor, sem dicionário externo.

O truque está no system prompt: instruímos o modelo a responder exclusivamente com sinônimos separados por vírgula, sem explicações. Isso mantém o output limpo e previsível sem precisar de JSON Schema.

Pra quem: Quem está começando com a Prompt API e quer ver como um system prompt bem escrito resolve 80% dos problemas de formatação de output.

Técnica principal: initialPrompts com role "system" para restringir o modelo a um formato específico (lista de palavras separadas por vírgula). Parsing simples com .split(",").


Wireframe

┌─────────────────────────────────────────────────────┐
│  📝 Buscador de Sinônimos                           │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌─────────────────────────────────┐  ┌──────────┐ │
│  │ input (placeholder: "feliz")    │  │  Buscar  │ │
│  └─────────────────────────────────┘  └──────────┘ │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │                                               │  │
│  │  [alegre] [contente] [satisfeito] [radiante]  │  │
│  │  [jubiloso] [animado]                         │  │
│  │                                               │  │
│  │  ← chips clicáveis (copiam ao clicar)         │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  💡 Clique em um sinônimo para copiar               │
│  ⚠️ Requer Chrome 138+ com Prompt API habilitada    │
└─────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="buscador-sinonimos">
  <h2>📝 Buscador de Sinônimos</h2>

  <div id="status-bar" class="status" hidden>
    <span id="status-message"></span>
  </div>

  <form id="form-sinonimos" class="search-form">
    <input
      type="text"
      id="input-word"
      placeholder="Digite uma palavra... ex: feliz"
      maxlength="50"
      autocomplete="off"
      required
    />
    <button type="submit" id="btn-search" disabled>Buscar</button>
  </form>

  <div id="results" class="results-area" hidden>
    <p id="results-label" class="results-label"></p>
    <div id="chips-container" class="chips"></div>
  </div>

  <p id="copy-feedback" class="copy-feedback" hidden>✓ Copiado!</p>
</section>

Código JavaScript

class SynonymFinder {
  constructor() {
    this.session = null;
    this.form = document.getElementById("form-sinonimos");
    this.inputEl = document.getElementById("input-word");
    this.btnEl = document.getElementById("btn-search");
    this.resultsEl = document.getElementById("results");
    this.resultsLabel = document.getElementById("results-label");
    this.chipsContainer = document.getElementById("chips-container");
    this.copyFeedback = document.getElementById("copy-feedback");
    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 138+.", "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 {
      this.session = await LanguageModel.create({
        initialPrompts: [{
          role: "system",
          content: `Você é um dicionário de sinônimos em português brasileiro. 
Quando o usuário enviar uma palavra, responda APENAS com sinônimos dessa palavra separados por vírgula. 
Regras:
- Retorne entre 4 e 8 sinônimos
- Apenas palavras em português brasileiro
- Sem explicações, sem numeração, sem frases
- Se a palavra não existir ou não tiver sinônimos, responda exatamente: SEM_SINONIMOS`
        }],
        monitor(m) {
          m.addEventListener("downloadprogress", (e) => {
            const pct = Math.round(e.loaded * 100);
            document.getElementById("status-message").textContent =
              `⏳ Baixando modelo... ${pct}%`;
          });
        }
      });

      this.btnEl.disabled = false;
      this.hideStatus();
    } catch (err) {
      this.showStatus(`❌ Erro ao criar sessão: ${err.message}`, "error");
      return;
    }

    this.form.addEventListener("submit", (e) => {
      e.preventDefault();
      this.search();
    });
  }

  async search() {
    const word = this.inputEl.value.trim().toLowerCase();
    if (!word) return;

    this.btnEl.disabled = true;
    this.btnEl.textContent = "Buscando...";
    this.resultsEl.hidden = true;

    try {
      const raw = await this.session.prompt(word);
      const cleaned = raw.trim();

      if (cleaned === "SEM_SINONIMOS") {
        this.showStatus(`Não encontrei sinônimos para "${word}".`, "error");
        return;
      }

      const synonyms = cleaned
        .split(",")
        .map(s => s.trim().toLowerCase())
        .filter(s => s.length > 0 && s !== word);

      if (synonyms.length === 0) {
        this.showStatus(`Não encontrei sinônimos para "${word}".`, "error");
        return;
      }

      this.hideStatus();
      this.renderChips(word, synonyms);
    } catch (err) {
      if (err.name === "AbortError") return;
      this.showStatus(`❌ Erro: ${err.message}`, "error");
    } finally {
      this.btnEl.disabled = false;
      this.btnEl.textContent = "Buscar";
    }
  }

  renderChips(word, synonyms) {
    this.resultsLabel.textContent = `Sinônimos de "${word}":`;
    this.chipsContainer.innerHTML = "";

    synonyms.forEach(synonym => {
      const chip = document.createElement("button");
      chip.className = "chip";
      chip.textContent = synonym;
      chip.type = "button";
      chip.addEventListener("click", () => this.copyToClipboard(synonym));
      this.chipsContainer.appendChild(chip);
    });

    this.resultsEl.hidden = false;
  }

  async copyToClipboard(text) {
    try {
      await navigator.clipboard.writeText(text);
      this.copyFeedback.hidden = false;
      setTimeout(() => { this.copyFeedback.hidden = true; }, 1500);
    } catch {
      // Fallback silencioso
    }
  }

  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 SynonymFinder());

Fluxo UX

  1. Página carrega → verifica se LanguageModel existe no window
  2. Sessão criada → botão “Buscar” habilitado; se modelo em download, mostra progresso
  3. Usuário digita palavra → submete com Enter ou clique no botão
  4. Busca executa → botão desabilita, texto muda pra “Buscando…”
  5. Resposta chega → chips aparecem com animação sutil (fade in)
  6. Clique no chip → copia palavra pro clipboard, toast ”✓ Copiado!” por 1.5s
  7. Nova busca → substitui chips anteriores

Edge Cases e Tratamento de Erros

CenárioTratamento
Prompt API não existeMensagem + orientação sobre versão do Chrome
Modelo indisponívelInforma incompatibilidade de hardware
Campo vaziorequired no HTML impede submit
Palavra inexistenteModelo retorna SEM_SINONIMOS → mensagem amigável
Modelo retorna frase em vez de lista.split(",") gera 1 chip com a frase inteira — não quebra a UI
Modelo repete a palavra originalFiltrada no .filter(s => s !== word)
Clipboard API bloqueada (HTTP)Try/catch silencioso, chip funciona normalmente
Sessão expira / context overflowImprovável (input curto), mas erro genérico capturado

CSS Essencial

.search-form {
  display: flex;
  gap: 0.5rem;
}

.search-form input {
  flex: 1;
  padding: 0.75rem 1rem;
  font-size: 1rem;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  transition: border-color 0.2s;
}

.search-form input:focus {
  outline: none;
  border-color: #3b82f6;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.chip {
  padding: 0.5rem 1rem;
  background: #eff6ff;
  color: #1d4ed8;
  border: 1px solid #bfdbfe;
  border-radius: 9999px;
  font-size: 0.9rem;
  cursor: pointer;
  transition: background 0.15s, transform 0.1s;
}

.chip:hover {
  background: #dbeafe;
  transform: translateY(-1px);
}

.chip:active {
  transform: translateY(0);
}

.results-label {
  font-size: 0.85rem;
  color: #6b7280;
  margin-bottom: 0.25rem;
}

.copy-feedback {
  font-size: 0.8rem;
  color: #16a34a;
  margin-top: 0.5rem;
}

.status-error { color: #dc2626; }
.status-loading { color: #2563eb; }