Pular para o conteúdo principal

Tutorial da Perplexity Search API: crie um verificador de afirmações em tempo real no seu navegador

Aprenda a usar a Perplexity Search API para criar uma extensão do Chrome que verifica afirmações factuais em tempo real. Inclui configuração de backend em Node.js e guia de integração da API.
Atualizado 23 de set. de 2026  · 10 min lido

Explorar com IA

ChatGPTClaudePerplexity

A Perplexity lançou recentemente a Perplexity Search API, pensada para busca na web de alta precisão, retornando trechos ranqueados e ricos em contexto em vez de páginas inteiras. Ela foi criada para usos em tempo real, como checagem de fatos, pesquisa e pipelines de RAG, onde velocidade e precisão fazem a diferença.

Neste tutorial, vamos criar o Claim Checker, uma extensão de navegador que verifica, em tempo real, afirmações factuais em qualquer página (com base em evidências recuperadas da web). Você vai ver como o índice constantemente atualizado e a pilha de recuperação de baixa latência da Perplexity Search API, combinados com sinais semânticos e re-ranking em múltiplas etapas, facilitam encontrar rapidamente as evidências mais relevantes. Ao longo do caminho, você vai aprender a:

  • Usar a Perplexity Search API para buscar evidências ranqueadas e de alta qualidade na web e processar resultados com algoritmos de ordenação personalizados.
  • Desenvolver um backend em Node.js para gerenciar sua chave de API com segurança, lidar com requisições CORS e implementar processamento inteligente de resultados com extração de domínio e ranqueamento por data.
  • Criar uma extensão para Chrome/Edge com content scripts que habilitam seleção de texto, painel lateral e comunicação fluida entre o navegador e seu servidor.
  • Exibir resultados da verificação — incluindo URLs das fontes, títulos, trechos, domínios e datas de publicação — em um painel lateral interativo com processamento de afirmações em tempo real.

Ao final, você terá uma ferramenta full stack de checagem de fatos que funciona perfeitamente dentro do navegador. O usuário pode selecionar qualquer texto em uma página e receber instantaneamente resultados de verificação com fontes de apoio — mantendo as credenciais da API seguras no servidor.

Você também pode conferir nosso guia do Comet Browser da Perplexity em um artigo separado. 

O que é a Perplexity Search API?

A Perplexity Search API é uma forma amigável para desenvolvedores obterem resultados de busca na web rápidos e de alta granularidade. 

Diferente de APIs de busca tradicionais que retornam páginas inteiras, a Perplexity entrega trechos ranqueados e partes de subdocumentos contendo as frases e parágrafos exatos que batem com a sua consulta. Alguns destaques desta API de busca incluem:

  • Custo acessível: a Search API custa US$ 5 a cada 1.000 requisições, sem cobrança por tokens, ideal para apps que fazem checagens frequentes ou agendadas.
  • Recuperação de alta qualidade: combina ranqueamento lexical e semântico com re-ranking em múltiplas etapas, oferecendo resultados altamente relevantes e atuais a partir de um índice gigantesco e sempre atualizado (~200 milhões de consultas/dia).
  • Baixa latência: a API de busca da Perplexity responde rápido, mantendo a sua interface fluida mesmo ao verificar várias afirmações ao mesmo tempo.

Para nossa extensão, isso significa que podemos enviar qualquer frase como consulta e receber evidências confiáveis que confirmam ou contradizem, com títulos, URLs e trechos de contexto — perfeito para checagem de fatos em tempo real.

Projeto com a Perplexity Search API: extensão para checar afirmações no navegador

Nesta seção, vamos criar uma extensão de navegador que verifica instantaneamente afirmações factuais usando a Perplexity Search API. Este projeto combina:

  • Um backend leve em Node.js que envia o texto selecionado para a Perplexity Search API e processa os resultados
  • Uma extensão do Chrome com painel lateral para exibir os detalhes da verificação
  • Um content script que captura o texto selecionado pelo usuário em qualquer página

Ao final do projeto, a estrutura do seu repositório deve ficar assim:

ClaimChecker/
├── server/
│   ├── server.js
│   ├── package.json
│   └── .env
├── extension/
│   ├── manifest.json
│   ├── background.js
│   ├── content.js
│   ├── sidepanel.html
│   ├── sidepanel.js
│   ├── popup.html
│   └── popup.js
└── README.md

Vamos passar por cada parte do projeto e ver como tudo trabalha junto para entregar uma verificação de afirmações confiável dentro do navegador.

Passo 1: dependências e bibliotecas 

Antes de começar, garanta que você tem os pré-requisitos abaixo:

  • Uma conta na Perplexity. Depois de criar sua conta ou fazer login, vá até a barra lateral esquerda, expanda a seção API e acesse seu grupo de API e configurações relacionadas.

  • Vá até a aba API Billing no seu API Portal e adicione um método de pagamento. Para este demo, cerca de US$ 10 em créditos são suficientes para cobrir as requisições.
  • Node.js versão 18 ou superior.
  • Navegador baseado em Chromium (Google Chrome ou Microsoft Edge).

Em seguida, instale as dependências necessárias para o backend:

cd server
npm install express cors dotenv

Crie um arquivo .env no diretório do servidor e adicione sua chave da Perplexity API:

PERPLEXITY_API_KEY=your_api_key_here

Pronto, as dependências estão instaladas. Agora podemos construir nosso servidor backend e a extensão.

Passo 2: construa o backend

O arquivo server.js configura um backend enxuto em Node.js usando o Express. Esse backend funciona como ponte entre a extensão do navegador e a Perplexity Search API. Ele recebe as afirmações selecionadas ou digitadas pelo usuário, consulta a Perplexity por evidências relevantes e retorna uma resposta limpa e estruturada, pronta para exibição na interface da extensão.

const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = 3000;
app.use(cors());
app.use(express.json());
function extractDomain(url) {
  try {
    const urlObj = new URL(url);
    return urlObj.hostname.replace('www.', '');
  } catch {
    return url;
  }
}
app.post('/check-claim', async (req, res) => {
  try {
    const { claim } = req.body;
    if (!claim) {
      return res.status(400).json({ error: 'Claim is required' });
    }
    if (!process.env.PERPLEXITY_API_KEY) {
      return res.status(500).json({ 
        error: 'Perplexity API key not configured',
        sources: []
      });
    }
    const response = await fetch('https://api.perplexity.ai/search', {
      method: 'POST',
      headers: {
        'Authorization': Bearer ${process.env.PERPLEXITY_API_KEY},
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        query: claim,
        max_results: 20,
        max_tokens_per_page: 1024
      })
    });
    if (!response.ok) {
      throw new Error(Perplexity API error: ${response.status});
    }
    const data = await response.json();
    const searchResults = data.results || [];   
    if (searchResults.length === 0) {
      return res.json({
        claim,
        sources: [],
        totalSources: 0,
        explanation: 'No relevant sources found to verify this claim.',
        summary: 'No sources found for fact-checking.'
      });
    }
    const sortedResults = searchResults.sort((a, b) => {
      const dateA = new Date(a.last_updated || a.date || 0);
      const dateB = new Date(b.last_updated || b.date || 0);
      return dateB - dateA;
    });
    const sources = sortedResults.slice(0, 3).map((result) => ({
      url: result.url,
      title: result.title,
      snippet: result.snippet || '',
      domain: extractDomain(result.url),
      date: result.date || null,
      lastUpdated: result.last_updated || null
    }));
    const result = {
      claim,
      sources,
      totalSources: searchResults.length,
      explanation: ${searchResults.length} sources found, showing top 3.,
      summary: Found ${searchResults.length} relevant sources for fact-checking.
    };
    res.json(result);
  } catch (error) {
    res.status(500).json({ 
      error: error.message,
      sources: []
    });
  }
});
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});
app.listen(PORT, () => {
  console.log(Server running on http://localhost:${PORT});
  console.log('Make sure PERPLEXITY_API_KEY is set in .env file');
});

O código acima é a camada de segurança e lógica da extensão Claim Checker, que realiza as seguintes funções:

  • Backend seguro e lógico: recebe uma afirmação via rota POST /check-claim, valida a entrada e chama a Perplexity Search API usando a chave de API armazenada no servidor.
  • Processamento e estruturação: o servidor recebe uma lista de fontes e trechos relevantes, ordena por recência usando last_updated e data, seleciona os três principais e retorna um JSON limpo para a extensão.
  • Proteção da chave: rodar esse backend mantém sua chave de API oculta do navegador e permite adicionar lógica extra, como ranqueamento, filtragem ou cache, antes de os dados chegarem ao usuário.
  • Design extensível: essa abordagem deixa a extensão mais segura e fácil de evoluir com melhorias futuras, como pontuação avançada, mesclagem multi-fonte ou analytics.

Com o servidor pronto, vamos construir a extensão do Chrome.

Passo 3: construindo a extensão do Chrome

Agora vamos criar a extensão do Chrome que entrega a experiência do usuário do Claim Checker. A extensão captura afirmações em qualquer página, envia para nosso backend e exibe os resultados de verificação em uma interface intuitiva.

Antes de mergulhar em cada arquivo, veja o papel dos componentes principais:

  • background.js: roda como service worker e gerencia tarefas em segundo plano, como ouvir ações do usuário, abrir o painel lateral e enviar as afirmações com segurança para o backend.
  • content.js: esse content script roda dentro da página ativa. Ele extrai o texto visível do artigo, identifica potenciais afirmações factuais e as destaca dinamicamente para o usuário ver o que está sendo verificado.
  • sidepanel.js: controla a interface do painel lateral interativo que mostra as afirmações extraídas e seus status de verificação. Atualiza os resultados em tempo real e faz a ponte com os destaques na página.
  • popup.js: alimenta o pop-up de acesso rápido quando o usuário clica no ícone da extensão. É uma forma ágil de verificar afirmações sem abrir o painel lateral.

Você encontra alguns arquivos auxiliares extras da extensão do Chrome no repositório no GitHub.

Passo 3.1: script de background e tratamento de eventos 

Com a base da extensão, o arquivo background.js é o controlador central que mantém a extensão responsiva. Ele escuta ações do usuário (como clicar no ícone), abre o painel lateral quando necessário e encaminha as afirmações para o backend fazer a verificação.

const SERVER_URL = 'http://localhost:3000';
chrome.action.onClicked.addListener(async (tab) => {
  try {
    await chrome.sidePanel.open({ tabId: tab.id });
  } catch (error) {
    console.error('Error opening side panel:', error);
    chrome.action.setPopup({ popup: 'popup.html' });
  }
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'checkClaim') {
    checkClaim(request.claim)
      .then(result => sendResponse({ success: true, result }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true; 
  }
});
async function checkClaim(claim) {
  try {
    const response = await fetch(${SERVER_URL}/check-claim, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ claim })
    });
    if (!response.ok) {
      throw new Error(Server error: ${response.status});
    }
    return await response.json();
  } catch (error) {
    console.error('Error checking claim:', error);
    throw error;
  }
}

O código acima mantém a extensão responsiva com os seguintes pontos:

  • Hub central de eventos: o background script registra dois listeners principais, chrome.action.onClicked e chrome.runtime.onMessage. O primeiro reage ao clique no ícone da extensão e tenta abrir o painel lateral. O segundo escuta mensagens como checkClaim vindas do content script ou da UI.
  • Tratamento seguro da API: a função checkClaim() usa fetch para enviar a afirmação selecionada ao backend em http://localhost:3000/check-claim. Como a chave de API está no servidor, o navegador nunca a expõe — mantendo as credenciais sensíveis protegidas.
  • Resiliência a erros: ambos os listeners incluem try/catch para tratar erros em tempo de execução. Se o painel lateral não abrir, o script faz fallback para o pop-up. Em falhas de API, ele registra e retorna mensagens de erro estruturadas para a UI, evitando falhas silenciosas.

Com o background cuidando da comunicação e das chamadas seguras, o próximo passo é interagir com a própria página.

Passo 3.2: extraindo e destacando afirmações

O arquivo content.js roda diretamente dentro da página e é responsável por extrair o texto do artigo, identificar possíveis afirmações factuais e destacá-las visualmente para o usuário. Ele faz a ponte entre o conteúdo da página e a lógica de background da extensão, capturando o texto relevante e habilitando a interação.

function extractArticleText() {
    const selectors = [
      'article',
      '[role="article"]',
      '.article-content',
      '.post-content',
      '.entry-content',
      'main'
    ];  
    for (const selector of selectors) {
      const element = document.querySelector(selector);
      if (element) {
        return element.innerText;
      }
    }  
    return document.body.innerText;
  }  
  function extractClaims(text) {
    const sentenceMatches = text.match(/[^.!?]*[.!?]+/g) || [];
    const sentences = sentenceMatches
      .map(s => s.trim())
      .filter(s => s.length > 20 && s.length < 1000);
    const claimKeywords = [
      /\d+%/, 
      /\d+\s+(million|billion|thousand|trillion)/i, 
      /according to/i,
      /study shows/i,
      /research found/i,
      /scientists/i,
      /experts/i,
      /will|would|could|should/i,
      /increase|decrease|rise|fall/i,
      /more than|less than|at least/i,
      /proven|showed|demonstrated/i,
      /investment|invest|create|generate/i,  
      /jobs|employment|economy/i,  
      /ripple effect|impact|effect/i  
    ];  
    const claims = [];    
    for (const sentence of sentences) {
      const hasClaimKeyword = claimKeywords.some(regex => regex.test(sentence));      
      const isValidLength = sentence.length > 30 && sentence.length < 800;
      const notNavigation = !/(click here|read more|subscribe|sign up|menu|home|cookie|privacy)/i.test(sentence);
      const hasCompleteThought = /[.!?]$/.test(sentence) || sentence.length > 50;      
      if (hasClaimKeyword && isValidLength && notNavigation && hasCompleteThought) {
        let cleanSentence = sentence
          .replace(/\s+/g, ' ')  
          .replace(/^\s*[-•]\s*/, '') 
          .trim();        
        if (cleanSentence.length > 30) {
          claims.push(cleanSentence);
        }
      }
    }  
    return claims.slice(0, 10);
  }  
let highlightedElements = new Map();
let claimIdCounter = 0;
function highlightTextOnPage(claim, claimId) {
  const text = document.body.innerText;
  const claimWords = claim.toLowerCase().split(/\s+/).filter(word => word.length > 3);  
  if (claimWords.length < 3) return null;
  const sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 20);
  let bestMatch = null;
  let bestScore = 0;  
  for (const sentence of sentences) {
    const sentenceWords = sentence.toLowerCase().split(/\s+/);
    let score = 0;    
    for (const word of claimWords) {
      if (sentenceWords.some(sw => sw.includes(word) || word.includes(sw))) {
        score++;
      }
    }    
    if (score > bestScore && score >= Math.min(3, claimWords.length * 0.5)) {
      bestScore = score;
      bestMatch = sentence;
    }
  }  
  if (!bestMatch) return null;  
  const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );  
  let node;
  while (node = walker.nextNode()) {
    if (node.textContent.includes(bestMatch)) {
      const parent = node.parentElement;
      if (parent && !parent.classList.contains('claim-highlight')) {
        const highlight = document.createElement('span');
        highlight.className = 'claim-highlight';
        highlight.id = claim-${claimId};
        highlight.style.cssText = 
          background: linear-gradient(120deg, #fbbf24 0%, #f59e0b 100%);
          
          border-radius: 3px;
          box-shadow: 0 1px 3px rgba(0,0,0,0.2);
          cursor: pointer;
          transition: all 0.2s ease;
          position: relative;
        ;        
        const textContent = node.textContent;
        const index = textContent.indexOf(bestMatch);
        if (index !== -1) {
          const before = textContent.substring(0, index);
          const after = textContent.substring(index + bestMatch.length);         
          const beforeNode = document.createTextNode(before);
          const afterNode = document.createTextNode(after);          
          highlight.textContent = bestMatch;
          node.parentNode.insertBefore(beforeNode, node);
          node.parentNode.insertBefore(highlight, node);
          node.parentNode.insertBefore(afterNode, node);
          node.parentNode.removeChild(node);
          highlight.addEventListener('mouseenter', () => {
            highlight.style.transform = 'scale(1.05)';
            highlight.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
          });    
          highlight.addEventListener('mouseleave', () => {
            highlight.style.transform = 'scale(1)';
            highlight.style.boxShadow = '0 1px 3px rgba(0,0,0,0.2)';
          });   
          highlight.addEventListener('click', () => {
            chrome.runtime.sendMessage({
              action: 'highlightClaimInPanel',
              claimId: claimId
            });        
            highlight.style.background = 'linear-gradient(120deg, #10b981 0%, #059669 100%)';
            setTimeout(() => {
              highlight.style.background = 'linear-gradient(120deg, #fbbf24 0%, #f59e0b 100%)';
            }, 1000);
          });     
          highlightedElements.set(claimId, highlight);
          return claimId;
        }
      }
      break;
    }
  }  
  return null;
}
function removeHighlights() {
  highlightedElements.forEach((element, claimId) => {
    if (element && element.parentNode) {
      const parent = element.parentNode;
      const text = element.textContent;
      const textNode = document.createTextNode(text);
      parent.insertBefore(textNode, element);
      parent.removeChild(element);
    }
  });
  highlightedElements.clear();
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'extractClaims') {
    const text = extractArticleText();
    const claims = extractClaims(text);
    sendResponse({ claims, url: window.location.href });
  } else if (request.action === 'highlightClaim') {
    const { claim, claimId } = request;
    const highlightId = highlightTextOnPage(claim, claimId);
    sendResponse({ success: true, highlightId });
  } else if (request.action === 'removeHighlights') {
    removeHighlights();
    sendResponse({ success: true });
  }
  return true;
});

Vamos detalhar as partes-chave do content.js para entender como ele extrai texto relevante, detecta possíveis afirmações e conecta o conteúdo destacado com a UI da extensão.

  • Extração do texto do artigo: a função extractArticleText() varre contêineres comuns de artigos para puxar o conteúdo legível principal. Assim a extensão foca no que importa e ignora menus ou anúncios.
  • Lógica de detecção de afirmações: a função extractClaims() quebra o texto em frases, aplica filtros de comprimento e estrutura e usa palavras-chave via regex para detectar enunciados que provavelmente são afirmações factuais.
  • Destaque dinâmico: a função highlightTextOnPage() localiza a afirmação detectada no DOM e a envolve em um <span> estilizado. Ela adiciona interações de hover e clique para o usuário ligar visualmente as afirmações na página aos resultados de verificação no painel lateral.
  • Tratamento de mensagens: o listener chrome.runtime.onMessage coordena ações como extrair afirmações, adicionar destaques e removê-los quando necessário. Isso mantém página e extensão sincronizadas e garante uma interação fluida.

Com o content script extraindo e destacando as afirmações, o próximo passo é apresentar essas afirmações e seus resultados de verificação ao usuário — entra em cena a interface sidepanel.js.

Passo 3.3: UI do painel lateral

O arquivo sidepanel.js alimenta o painel lateral interativo onde o usuário vê as afirmações extraídas e seus resultados. Ele conecta o content script e as respostas do backend em uma interface limpa e rolável dentro da extensão.

let claimCounter = 0;
let currentHighlights = new Map();
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'highlightClaimInPanel') {
    const claimId = request.claimId;
    const claimElement = document.getElementById(sidepanel-${claimId});    
    if (claimElement) {
      claimElement.scrollIntoView({ 
        behavior: 'smooth', 
        block: 'center' 
      });      
      claimElement.classList.add('highlighted');      
      claimElement.style.animation = 'pulse 1s ease-in-out';     
      setTimeout(() => {
        claimElement.classList.remove('highlighted');
        claimElement.style.animation = '';
      }, 3000);
    }
  }
});
document.getElementById('check-btn').addEventListener('click', async () => {
    const button = document.getElementById('check-btn');
    const container = document.getElementById('claims-container');
    const status = document.getElementById('status');   
    button.disabled = true;
    container.innerHTML = '<div class="loading">Extracting claims...</div>';
    status.textContent = '';    
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      await chrome.tabs.sendMessage(tab.id, { action: 'removeHighlights' });
    } catch (error) {
    }   
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      const response = await chrome.tabs.sendMessage(tab.id, { action: 'extractClaims' });    
      if (!response || !response.claims || response.claims.length === 0) {
        container.innerHTML = '<div class="no-claims">No claims found on this page.</div>';
        button.disabled = false;
        return;
      }
      status.textContent = Found ${response.claims.length} claims. Checking...;
      container.innerHTML = '';      
      for (let i = 0; i < response.claims.length; i++) {
        const claim = response.claims[i];
        const claimId = claim-${++claimCounter};
        status.textContent = Checking claim ${i + 1} of ${response.claims.length}...;        
        try {
          await chrome.tabs.sendMessage(tab.id, { 
            action: 'highlightClaim', 
            claim, 
            claimId 
          });
        } catch (error) {
        }      
        const claimDiv = document.createElement('div');
        claimDiv.className = 'claim-item';
        claimDiv.id = sidepanel-${claimId};
        claimDiv.innerHTML = 
          <div class="claim-text">${escapeHtml(claim)}</div>
          <div class="loading">Verifying...</div>
        ;
        container.appendChild(claimDiv);
        chrome.runtime.sendMessage(
          { action: 'checkClaim', claim },
          (checkResponse) => {
            if (checkResponse.success) {
              const result = checkResponse.result;      
              const sourcesHtml = result.sources && result.sources.length > 0 ? 
                <div class="claim-sources">
                  ${result.sources.map(s => 
                    <div class="source-item">
                      <a href="${s.url}" target="_blank" class="source-link">
                        ${s.title || s.domain}
                      </a>
                      <div class="source-domain">
                        ${s.domain}
                        ${s.lastUpdated || s.date ?  • ${new Date(s.lastUpdated || s.date).toLocaleDateString()} : ''}
                      </div>
                      ${s.snippet ? <div class="source-snippet">${escapeHtml(s.snippet.substring(0, 150))}${s.snippet.length > 150 ? '...' : ''}</div> : ''}
                    </div>
                  ).join('')}
                </div> : 
                '<div class="claim-sources">No sources found</div>';             
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                ${sourcesHtml}
              ;          
              currentHighlights.set(claimId, claimDiv);
            } else {
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                <div class="error">Error: ${checkResponse.error}</div>
              ;
            }
          }
        );  
        if (i < response.claims.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 500));
        }
      }  
      status.textContent = 'All claims checked! Click highlighted text on the page to jump to the corresponding claim details.';
      button.disabled = false; 
    } catch (error) {
      container.innerHTML = <div class="error">Error: ${error.message}</div>;
      button.disabled = false;
      status.textContent = '';
    }
  });  
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

Veja como o painel lateral funciona no navegador:

  • Lista dinâmica de afirmações: quando o usuário clica em “Check Claims”, o script solicita as afirmações da aba ativa, limpa os destaques antigos e cria uma lista com estados temporários.
  • Atualizações em tempo real: cada afirmação é enviada ao background via chrome.runtime.sendMessage com a ação checkClaim. Quando o backend retorna, a UI atualiza na hora com fontes, domínios e trechos.
  • Vinculação interativa: o script escuta mensagens highlightClaimInPanel, então, ao clicar no texto destacado na página, o painel rola para os detalhes correspondentes. Isso cria um fluxo de navegação suave entre os destaques e os dados de verificação.

Agora, vamos adicionar um pop-up de acesso rápido para facilitar o uso da extensão criada nos passos anteriores.

Passo 3.4: pop-up de acesso rápido

O popup.js é uma interface leve que aparece quando o usuário clica no ícone da extensão. Diferente do painel lateral, ele oferece uma forma mais rápida e sob demanda de verificar afirmações sem sair da página atual nem abrir um painel completo.

document.getElementById('check-btn').addEventListener('click', async () => {
    const button = document.getElementById('check-btn');
    const container = document.getElementById('claims-container');
    const status = document.getElementById('status');   
    button.disabled = true;
    container.innerHTML = '<div class="loading">Extracting claims...</div>';
    status.textContent = '';  
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });     
      const response = await chrome.tabs.sendMessage(tab.id, { action: 'extractClaims' });  
      if (!response || !response.claims || response.claims.length === 0) {
        container.innerHTML = '<div class="error">No claims found on this page.</div>';
        button.disabled = false;
        return;
      }   
      status.textContent = Found ${response.claims.length} claims. Checking...;
      container.innerHTML = '';
      for (let i = 0; i < response.claims.length; i++) {
        const claim = response.claims[i];
        status.textContent = Checking claim ${i + 1} of ${response.claims.length}...;   
        const claimDiv = document.createElement('div');
        claimDiv.className = 'claim-item';
        claimDiv.innerHTML = 
          <div class="claim-text">${escapeHtml(claim)}</div>
          <div class="loading">Verifying...</div>
        ;
        container.appendChild(claimDiv);      
        chrome.runtime.sendMessage(
          { action: 'checkClaim', claim },
          (checkResponse) => {
            if (checkResponse.success) {
              const result = checkResponse.result;
              const confidenceBar = result.confidence ? 
                <div class="confidence-bar">
                  <div class="confidence-fill" style="width: ${result.confidence}%"></div>
                  <span class="confidence-text">${result.confidence}% confidence</span>
                </div> : '';     
              const explanation = result.explanation ? 
                <div class="claim-explanation">${escapeHtml(result.explanation)}</div> : '';    
              const rebuttal = result.rebuttal ? 
                <div class="claim-rebuttal">
                  <strong>Counter-argument:</strong> ${escapeHtml(result.rebuttal)}
                </div> : '';  
              const sourcesHtml = result.sources && result.sources.length > 0 ? 
                <div class="claim-sources">
                  <strong>Sources (${result.sources.length}):</strong>
                  ${result.sources.map(s => 
                    <div class="source-item">
                      <a href="${s.url}" target="_blank" class="source-link">
                        ${s.domain || s.title}
                      </a>
                      ${s.snippet ? <div class="source-snippet">${escapeHtml(s.snippet)}</div> : ''}
                    </div>
                  ).join('')}
                </div> : 
                '<div class="claim-sources">No sources found</div>'; 
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                <div class="claim-verdict verdict-${result.verdict.toLowerCase()}">
                  ${result.verdict}
                </div>
                ${confidenceBar}
                ${explanation}
                ${rebuttal}
                ${sourcesHtml}
              ;
            } else {
              claimDiv.innerHTML = 
                <div class="claim-text">${escapeHtml(claim)}</div>
                <div class="error">Error: ${checkResponse.error}</div>
              ;
            }
          }
        );       
        if (i < response.claims.length - 1) {
          await new Promise(resolve => setTimeout(resolve, 500));
        }
      }  
      status.textContent = 'All claims checked!';
      button.disabled = false;      
    } catch (error) {
      container.innerHTML = <div class="error">Error: ${error.message}</div>;
      button.disabled = false;
      status.textContent = '';
    }
  }); 
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

Veja como o popup.js habilita uma checagem sob demanda direto da barra da extensão:

  • Varredura da página e exibição: ao clicar em “Check”, o pop-up solicita o texto da aba ativa via content script, extrai rapidamente possíveis afirmações e as lista no pop-up.
  • Verificação no backend: cada afirmação é enviada ao background para chamar a rota /check-claim. O pop-up atualiza em tempo real com vereditos, barras de confiança, explicações, contra-argumentos e links de fontes retornados pelo servidor.

Pronto, nossa extensão está pronta para testes. 

Observação: você encontra todos os arquivos de código deste projeto no repositório no GitHub.

Passo 4: executando o app

Inicie o servidor backend com o comando:

npm start

Com o servidor rodando, carregue a extensão do Chrome:

  • Abra o Chrome e clique em “Extensões” (à direita da barra de pesquisa).
  • Ative o “Modo do desenvolvedor” e clique em “Carregar sem compactação” (canto superior esquerdo). 
  • Depois, carregue a pasta da extensão contendo todos os arquivos e aguarde a confirmação.

  • Após o Chrome confirmar a instalação, clique no ícone do Claim Checker em qualquer página, selecione o texto que deseja verificar e rode a checagem.

Vamos ver os resultados:

Conclusão

Agora você tem um verificador de afirmações rápido e econômico, totalmente baseado na Search API da Perplexity (sem chamadas a LLM). Este demo destaca os principais pontos fortes da API: resultados atualizados e confiáveis, relevância em nível de trecho, baixa latência e preço previsível.

A partir daqui, você pode estender o projeto com filtros avançados, agendar checagens automáticas ou integrá-lo como pré-filtro em pipelines de RAG. Para ir além, explore a Perplexity API docs e os guias de início rápido para liberar recursos mais avançados de busca e filtragem.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Sou Especialista Google Developers em ML (Gen AI), tricampeã no Kaggle e Embaixadora Women Techmakers, com mais de três anos de experiência na área de tecnologia. Cofundei uma startup de saúde em 2020 e atualmente faço um mestrado em ciência da computação na Georgia Tech, com foco em aprendizado de máquina.

Tópicos
Inteligência Artificial
Agentes de IA

Principais cursos da DataCamp

Curso

Introdução a agentes de IA

1 h 30 min
138.5K
Aprenda os fundamentos dos agentes de IA, seus componentes e aplicações reais — sem precisar programar.
Ver detalhesRight Arrow
Iniciar Curso
Ver maisRight Arrow