Demo: Extrator de Dados Estruturados com Prompt API
intermediárioExtraia nomes, emails, telefones e empresas de texto não-estruturado com JSON Schema constraints. IA local no navegador com responseConstraint da Prompt API.
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
- Página carrega → cria sessão com system prompt de extração
- Usuário cola texto com dados de contato misturados
- Clica “Extrair” → botão desabilita, mostra “Extraindo…”
- Resultado chega → tabela renderiza com dados organizados
- Clica “Copiar CSV” → dados vão para clipboard formatados
- 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: falseno root impede campos extras
O modelo é obrigado a retornar o formato exato, eliminando necessidade de parsing regex.