Accéder au contenu principal

Tutoriel Perplexity Search API : créez un vérificateur d’affirmations en temps réel dans votre navigateur

Apprenez à utiliser la Perplexity Search API pour créer une extension Chrome qui vérifie les affirmations factuelles en temps réel. Inclut la mise en place du backend Node.js et un guide d’intégration API.
· 10 min lire

Explorez l'IA

Perplexity a récemment lancé sa Perplexity Search API, conçue pour une exploration du web fine et granulaire qui renvoie des extraits classés et riches en contexte plutôt que des pages entières. Elle est pensée pour les usages en temps réel — vérification des faits, recherche, pipelines RAG — où vitesse et précision sont essentielles.

Dans ce tutoriel, nous allons construire Claim Checker, une extension de navigateur qui vérifie en temps réel les affirmations factuelles de n’importe quelle page (sur la base de preuves issues du web). Vous verrez comment l’index en mise à jour continue et la pile de recherche à faible latence de la Perplexity Search API, combinés à des signaux sémantiques et à un re-ranking multi-étapes, facilitent l’identification rapide des preuves les plus pertinentes. Au passage, vous apprendrez à :

  • Exploiter la Perplexity Search API pour récupérer des preuves web de haute qualité, classées, et traiter les résultats de recherche avec des algorithmes de tri personnalisés.
  • Développer un backend Node.js pour gérer en toute sécurité votre clé d’API, prendre en charge les requêtes CORS et implémenter un traitement intelligent des résultats avec extraction de domaine et classement par date.
  • Créer une extension Chrome/Edge avec des scripts de contenu permettant la sélection de texte, une interface en panneau latéral et une communication fluide entre le navigateur et votre serveur backend.
  • Afficher les résultats de vérification — URL sources, titres, extraits, domaines et dates de publication — dans un panneau latéral interactif avec traitement des affirmations en temps réel.

À la fin, vous disposerez d’un outil full-stack de vérification des faits qui s’intègre parfaitement à votre navigateur. Les utilisateurs peuvent sélectionner n’importe quel texte sur une page et obtenir instantanément des résultats de vérification avec sources à l’appui, tout en gardant les identifiants API en sécurité côté serveur.

Vous pouvez également consulter notre guide du Comet Browser de Perplexity dans un article séparé. 

Qu’est-ce que la Perplexity Search API ?

La Perplexity Search API est une solution conçue pour les développeurs afin d’obtenir des résultats de recherche web rapides et très précis.

Contrairement aux API de recherche traditionnelles qui renvoient des pages entières, Perplexity livre des extraits classés et des segments sous-documentaires contenant les phrases et paragraphes correspondant exactement à votre requête. Parmi ses points forts :

  • Économique : la Search API est facturée 5 $ pour 1 000 requêtes, sans tarification au jeton, ce qui la rend idéale pour des applications exécutant des contrôles fréquents ou planifiés.
  • Récupération de haute qualité : combinaison de classement lexical et sémantique avec re-ranking multi-étapes pour fournir des résultats très pertinents et à jour, issus d’un index massif et rafraîchi en continu (~200 M de requêtes/jour).
  • Faible latence : la Perplexity Search API renvoie rapidement les résultats, ce qui maintient une UI fluide, même lors de la vérification simultanée de plusieurs affirmations.

Pour notre extension, cela signifie que nous pouvons transmettre n’importe quelle phrase comme requête et récupérer des preuves fiables à l’appui — ou contradictoires — avec titres, URL et extraits de contexte, parfaits pour la vérification des faits en temps réel.

Projet Perplexity Search API : extension de vérification d’affirmations dans le navigateur

Dans cette section, nous allons créer une extension de navigateur qui vérifie instantanément les affirmations factuelles grâce à la Perplexity Search API. Ce projet combine :

  • Un backend Node.js léger qui envoie le texte sélectionné à la Perplexity Search API et traite les résultats
  • Une extension Chrome avec un panneau latéral pour afficher les détails de la vérification
  • Un script de contenu qui capture le texte sélectionné par l’utilisateur sur n’importe quelle page

Au terme du projet, la structure de votre dépôt devrait ressembler à ceci :

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

Passons en revue chaque partie du projet pour voir comment elles s’articulent afin de délivrer une vérification fiable des affirmations directement dans le navigateur.

Étape 1 : dépendances et bibliothèques 

Avant de commencer, assurez-vous d’avoir les prérequis suivants :

  • Un compte Perplexity. Une fois inscrit(e) ou connecté(e), ouvrez le menu latéral gauche et développez la section API pour accéder à votre groupe d’API et aux paramètres associés.

  • Rendez-vous dans l’onglet API Billing de votre portail API et ajoutez un mode de paiement. Pour cette démo, environ 10 $ de crédits suffisent pour couvrir les requêtes.
  • Node.js version 18 ou supérieure.
  • Navigateur basé sur Chromium (Google Chrome ou Microsoft Edge).

Installez ensuite les dépendances nécessaires pour le backend :

cd server
npm install express cors dotenv

Créez un fichier .env dans le dossier server et ajoutez votre clé API Perplexity :

PERPLEXITY_API_KEY=your_api_key_here

Toutes les dépendances sont en place. Nous pouvons maintenant construire notre serveur backend et l’extension.

Étape 2 : construire le backend

Le fichier server.js met en place un backend Node.js léger avec Express. Ce backend fait le lien entre l’extension et la Perplexity Search API. Il reçoit les affirmations sélectionnées ou saisies par l’utilisateur, interroge Perplexity pour trouver des preuves pertinentes et renvoie une réponse nettoyée et structurée prête à être affichée dans l’UI de l’extension.

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');
});

Ce code constitue la couche de sécurité et de logique de l’extension Claim Checker et assure les fonctions suivantes :

  • Backend sécurisé et logique : il accepte une affirmation via la route POST /check-claim, valide l’entrée et appelle la Perplexity Search API avec une clé d’API stockée côté serveur.
  • Traitement et structuration : le serveur reçoit une liste de sources web et d’extraits pertinents, les trie par fraîcheur via last_updated et la date, en sélectionne trois et renvoie une réponse JSON propre pour l’extension.
  • Protection de la clé : en exécutant ce backend, votre clé d’API reste cachée du navigateur et vous pouvez ajouter de la logique supplémentaire — classement, filtrage, cache — avant que les données n’atteignent l’utilisateur.
  • Conception extensible : cette approche rend l’extension plus sûre et facile à faire évoluer (scoring avancé, fusion multi-sources, analytics, etc.).

Le serveur est prêt. Passons à l’extension Chrome.

Étape 3 : créer l’extension Chrome

Dans cette étape, nous allons créer l’extension Chrome qui porte l’expérience utilisateur de Claim Checker. L’extension capture des affirmations sur n’importe quelle page, les envoie à notre backend et affiche les résultats de vérification dans une interface intuitive.

Avant de plonger dans chaque fichier, voici le rôle des composants clés :

  • background.js : s’exécute comme service worker et gère les tâches d’arrière-plan : écoute des actions utilisateurs, ouverture du panneau latéral, envoi sécurisé des affirmations au backend pour vérification.
  • content.js : script de contenu exécuté dans la page active. Il extrait le texte lisible, identifie des affirmations factuelles potentielles et les surligne dynamiquement pour que l’utilisateur voie ce qui est vérifié.
  • sidepanel.js : contrôle l’UI du panneau latéral interactif qui affiche les affirmations extraites et leur statut de vérification. Il met à jour les résultats en temps réel et les relie aux surbrillances sur la page.
  • popup.js : alimente le pop-up d’accès rapide quand l’utilisateur clique sur l’icône de l’extension. Il permet de vérifier des affirmations à la volée sans ouvrir le panneau latéral.

Vous trouverez quelques fichiers utilitaires supplémentaires pour l’extension Chrome dans le référentiel GitHub.

Étape 3.1 : script d’arrière-plan et gestion des événements 

Dans la continuité de notre configuration, le fichier background.js agit comme contrôleur central pour garder l’extension réactive. Il écoute les actions de l’utilisateur (clic sur l’icône), ouvre le panneau latéral si nécessaire et transmet les affirmations au backend pour vérification.

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;
  }
}

Ce code améliore la réactivité de l’extension grâce aux éléments suivants :

  • Hub d’événements central : le script d’arrière-plan enregistre deux écouteurs principaux, chrome.action.onClicked et chrome.runtime.onMessage. Le premier réagit au clic sur l’icône et tente d’ouvrir le panneau latéral. Le second écoute des messages comme checkClaim provenant des scripts de contenu ou composants d’UI.
  • Gestion sécurisée de l’API : la fonction checkClaim() utilise fetch pour envoyer l’affirmation sélectionnée au backend sur http://localhost:3000/check-claim. La clé d’API étant stockée côté serveur, elle n’est jamais exposée dans le navigateur : les identifiants sensibles restent protégés.
  • Résilience aux erreurs : les deux écouteurs intègrent des blocs try/catch. Si l’ouverture du panneau échoue, le script bascule sur le pop-up. En cas d’échec d’appel API, il journalise et renvoie des erreurs structurées à l’UI, évitant les défaillances silencieuses.

Avec la communication et les appels API sécurisés gérés en arrière-plan, passons à l’interaction avec la page web elle‑même.

Étape 3.2 : extraction et surbrillance des affirmations

Le fichier content.js s’exécute directement dans la page et extrait le texte principal, détecte des affirmations potentielles et les met en évidence visuellement. Il fait le lien entre le contenu de la page et la logique de l’extension en capturant le texte pertinent et en permettant l’interaction utilisateur.

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;
});

Décomposons les éléments clés du script content.js pour comprendre comment il extrait le texte pertinent, détecte les affirmations et relie le contenu surligné à l’interface de l’extension.

  • Extraction du texte de l’article : la fonction extractArticleText() parcourt les conteneurs d’article courants pour récupérer le contenu lisible principal, afin d’éviter les menus de navigation ou publicités.
  • Logique de détection des affirmations : extractClaims() segmente le texte en phrases, applique des filtres de longueur et de structure, et utilise des expressions régulières pour repérer des formulations susceptibles d’être des affirmations factuelles.
  • Mise en évidence dynamique : highlightTextOnPage() localise l’affirmation détectée dans le DOM et l’entoure d’un <span> stylisé, avec interactions au survol et au clic pour relier visuellement l’affirmation à ses résultats dans le panneau latéral.
  • Gestion des messages : l’écouteur chrome.runtime.onMessage coordonne l’extraction, l’ajout et la suppression des surbrillances selon les besoins, gardant la page et l’extension synchronisées pour une expérience fluide.

Avec le script de contenu en charge de l’extraction et de la mise en évidence sur la page, présentons ces affirmations et leurs résultats de vérification à l’utilisateur via l’interface sidepanel.js.

Étape 3.3 : UI du panneau latéral

Le fichier sidepanel.js alimente le panneau latéral interactif où les utilisateurs visualisent les affirmations extraites et leurs résultats. Il relie le script de contenu et les réponses du backend dans une interface claire et défilante au sein de l’extension.

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;
  }

Voici le fonctionnement du panneau latéral dans votre navigateur :

  • Liste d’affirmations dynamique : au clic sur « Check Claims », le script demande les affirmations de l’onglet actif, efface les anciennes surbrillances et crée une liste avec des états temporaires.
  • Mises à jour en temps réel : chaque affirmation est envoyée au script d’arrière-plan via chrome.runtime.sendMessage avec l’action checkClaim. À la réponse du backend, l’UI se met à jour instantanément avec sources, domaines et extraits.
  • Liaison interactive : le script écoute les messages highlightClaimInPanel : un clic sur le texte surligné dans la page fait défiler le panneau jusqu’aux détails correspondants, pour une navigation fluide entre surbrillances et résultats.

Ajoutons maintenant un pop-up d’accès rapide pour faciliter le lancement et l’usage de l’extension construite aux étapes précédentes.

Étape 3.4 : pop-up d’accès rapide

Le popup.js est une interface légère qui apparaît au clic sur l’icône de l’extension. Contrairement au panneau latéral, ce pop-up propose une vérification à la demande, sans quitter la page ni ouvrir un panneau complet.

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;
  }

Voici comment le script popup.js permet une vérification à la demande depuis la barre d’outils de l’extension :

  • Analyse de page et affichage des affirmations : au clic sur « Check », le pop-up demande le texte de l’onglet actif via le script de contenu, extrait rapidement des affirmations potentielles et les liste dans la fenêtre.
  • Vérification côté backend : chaque affirmation est transmise au script d’arrière-plan pour appeler la route /check-claim. Le pop-up se met à jour en temps réel avec verdicts, barres de confiance, explications, contre‑arguments et liens sources renvoyés par le serveur.

Notre extension est maintenant prête à être testée. 

Remarque : vous pouvez retrouver tous les fichiers du projet dans le référentiel GitHub.

Étape 4 : exécuter l’application

Démarrez le serveur backend avec la commande suivante :

npm start

Une fois le serveur lancé, chargez l’extension Chrome :

  • Ouvrez Chrome et cliquez sur « Extensions » (à droite de la barre de recherche).
  • Activez le « Mode développeur » puis cliquez sur « Charger l’extension non empaquetée » (en haut à gauche). 
  • Chargez ensuite le dossier de l’extension contenant tous les fichiers et attendez la confirmation.

  • Une fois l’extension ajoutée, cliquez sur l’icône Claim Checker depuis n’importe quelle page, sélectionnez le texte à vérifier et lancez l’analyse.

Voyons les résultats :

Conclusion

Vous disposez désormais d’un vérificateur d’affirmations rapide et économique, entièrement propulsé par la Search API de Perplexity (aucun appel à un LLM requis). Cette démo met en avant ses atouts : fraîcheur et fiabilité des résultats, pertinence au niveau des extraits, faible latence et tarification prévisible.

À partir d’ici, vous pouvez étendre le projet avec des filtres avancés, planifier des vérifications automatiques ou l’intégrer comme pré‑filtre dans des pipelines RAG. Pour aller plus loin, explorez la Perplexity API docs et les guides de démarrage rapide pour débloquer des capacités de recherche et de filtrage plus avancées.

Aashi Dutt

Auteur

Aashi Dutt

Je suis experte Google Developers en ML (Gen AI), triple experte Kaggle et ambassadrice Women Techmakers, avec plus de trois ans d’expérience dans la tech. J’ai cofondé une startup dans le domaine de la santé en 2020 et je poursuis actuellement un master en informatique à Georgia Tech, avec une spécialisation en apprentissage automatique.

Sujets

Contenus associés

blog

Comprendre les TPU et les GPU dans l'IA : Un guide complet