Demo: Extrator de Dados Estruturados com Prompt API

intermediário

Extraia nomes, emails, telefones e empresas de texto não-estruturado com JSON Schema constraints. IA local no navegador com responseConstraint da Prompt API.

Verificando...

Visão Geral

Demo que extrai dados de contato (nome, email, telefone, empresa) de textos bagunçados — emails, assinaturas, listas copiadas de PDFs — e organiza tudo numa tabela. Na minha experiência, esse tipo de extração é onde o responseConstraint realmente brilha.

Pra quem: Desenvolvedores que precisam extrair informações estruturadas de texto livre.

Técnica principal: responseConstraint com JSON Schema definindo array de objetos com propriedades opcionais.


Wireframe

┌──────────────────────────────────────────────────────────┐
│  📇 Extrator de Dados Estruturados                       │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  ┌────────────────────────────────────────────────────┐  │
│  │ textarea                                           │  │
│  │ "João Silva - [email protected] - (11) 99999-0000  │  │
│  │  Empresa ABC Ltda                                  │  │
│  │  Maria Santos, [email protected], TechCorp..."    │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  [ 📇 Extrair Dados ]                                    │
│                                                          │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Nome       │ Email              │ Tel         │ Emp│  │
│  ├─────────────┼────────────────────┼─────────────┼────│  │
│  │ João Silva  │ [email protected]   │ 11999990000 │ ABC│  │
│  │ Maria Santos│ [email protected]  │ —           │Tech│  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  [ 📋 Copiar como CSV ]                                  │
└──────────────────────────────────────────────────────────┘

HTML

<section class="demo-container" id="extrator-dados">
  <h2>📇 Extrator de Dados Estruturados</h2>

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

  <textarea
    id="input-text"
    placeholder="Cole aqui texto com dados de contato — emails, assinaturas, listas, cartões de visita digitados..."
    rows="8"
    maxlength="5000"
  ></textarea>

  <div class="actions">
    <button id="btn-extract" disabled>📇 Extrair Dados</button>
  </div>

  <div id="result" hidden>
    <div class="result-header">
      <h3>Dados Extraídos (<span id="count">0</span> contatos)</h3>
      <button id="btn-copy-csv">📋 Copiar CSV</button>
    </div>
    <div class="table-wrapper">
      <table id="results-table">
        <thead>
          <tr>
            <th>Nome</th>
            <th>Email</th>
            <th>Telefone</th>
            <th>Empresa</th>
          </tr>
        </thead>
        <tbody id="results-body"></tbody>
      </table>
    </div>
  </div>
</section>

Código JavaScript

const CONTACTS_SCHEMA = {
  type: "object",
  properties: {
    contacts: {
      type: "array",
      items: {
        type: "object",
        properties: {
          name:    { type: "string", description: "Nome completo da pessoa" },
          email:   { type: "string", description: "Endereço de email" },
          phone:   { type: "string", description: "Número de telefone" },
          company: { type: "string", description: "Nome da empresa" }
        },
        required: ["name"]
      }
    }
  },
  required: ["contacts"],
  additionalProperties: false
};

class DataExtractor {
  constructor() {
    this.session = null;
    this.lastResult = [];

    this.inputEl = document.getElementById("input-text");
    this.btnEl = document.getElementById("btn-extract");
    this.btnCopy = document.getElementById("btn-copy-csv");
    this.resultEl = document.getElementById("result");
    this.tbody = document.getElementById("results-body");
    this.countEl = document.getElementById("count");
    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;
    }

    try {
      this.session = await LanguageModel.create({
        expectedInputs: [{ type: "text", languages: ["pt", "en"] }],
        expectedOutputs: [{ type: "text", languages: ["en"] }],
        initialPrompts: [{
          role: "system",
          content: `Você é um extrator de dados de contato. Dado um texto, extraia todos os contatos encontrados com nome, email, telefone e empresa. Se um campo não estiver disponível, omita-o. Retorne apenas os dados encontrados, sem inventar.`
        }]
      });

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

    this.btnEl.addEventListener("click", () => this.extract());
    this.btnCopy.addEventListener("click", () => this.copyCSV());
  }

  async extract() {
    const text = this.inputEl.value.trim();
    if (!text) return;

    this.btnEl.disabled = true;
    this.btnEl.textContent = "⏳ Extraindo...";
    this.resultEl.hidden = true;
    this.tbody.innerHTML = "";

    try {
      const raw = await this.session.prompt(
        `Extraia todos os dados de contato do seguinte texto:\n\n${text}`,
        { responseConstraint: CONTACTS_SCHEMA }
      );

      const { contacts } = JSON.parse(raw);

      if (!contacts || contacts.length === 0) {
        this.showStatus("⚠️ Nenhum contato encontrado no texto.", "error");
        return;
      }

      this.lastResult = contacts;
      this.renderTable(contacts);
    } catch (err) {
      this.showStatus(`❌ Erro ao extrair: ${err.message}`, "error");
    } finally {
      this.btnEl.disabled = false;
      this.btnEl.textContent = "📇 Extrair Dados";
    }
  }

  renderTable(contacts) {
    this.tbody.innerHTML = "";
    this.countEl.textContent = contacts.length;

    for (const c of contacts) {
      const tr = document.createElement("tr");
      tr.innerHTML = `
        <td>${this.esc(c.name)}</td>
        <td>${c.email ? this.esc(c.email) : "—"}</td>
        <td>${c.phone ? this.esc(c.phone) : "—"}</td>
        <td>${c.company ? this.esc(c.company) : "—"}</td>
      `;
      this.tbody.appendChild(tr);
    }

    this.resultEl.hidden = false;
  }

  copyCSV() {
    const header = "Nome,Email,Telefone,Empresa";
    const rows = this.lastResult.map(c =>
      `"${c.name || ""}","${c.email || ""}","${c.phone || ""}","${c.company || ""}"`
    );
    const csv = [header, ...rows].join("\n");

    navigator.clipboard.writeText(csv).then(() => {
      this.btnCopy.textContent = "✅ Copiado!";
      setTimeout(() => { this.btnCopy.textContent = "📋 Copiar CSV"; }, 2000);
    });
  }

  esc(str) {
    const el = document.createElement("span");
    el.textContent = str;
    return el.innerHTML;
  }

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

Fluxo UX

  1. Página carrega → cria sessão com system prompt de extração
  2. Usuário cola texto com dados de contato misturados
  3. Clica “Extrair” → botão desabilita, mostra “Extraindo…”
  4. Resultado chega → tabela renderiza com dados organizados
  5. Clica “Copiar CSV” → dados vão para clipboard formatados
  6. Zero contatos → mensagem amigável informando que nada foi encontrado

Edge Cases e Tratamento de Erros

CenárioTratamento
Texto sem dados de contatoMensagem “Nenhum contato encontrado”
Dados parciais (só nome, sem email)Campos opcionais exibem ”—“
Formato de telefone variadoModelo extrai como string; sem normalização forçada
JSON parse falhaTry/catch com mensagem de erro
Email inválido retornado pelo modeloAceita como está (modelo pode errar)
Texto em outro idiomaexpectedInputs aceita pt/en
XSS em dados coladosEscape HTML antes de renderizar

JSON Schema — Detalhes

O schema usa required: ["name"] no item do array porque:

  • Nome é o identificador mínimo de um contato
  • Email, telefone e empresa são opcionais (nem todo contato tem todos os dados)
  • additionalProperties: false no root impede campos extras

O modelo é obrigado a retornar o formato exato, eliminando necessidade de parsing regex.