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ário Tratamento
Texto sem dados de contato Mensagem “Nenhum contato encontrado”
Dados parciais (só nome, sem email) Campos opcionais exibem “—”
Formato de telefone variado Modelo extrai como string; sem normalização forçada
JSON parse falha Try/catch com mensagem de erro
Email inválido retornado pelo modelo Aceita como está (modelo pode errar)
Texto em outro idioma expectedInputs aceita pt/en
XSS em dados colados Escape 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.