Zum Hauptinhalt springen

Perplexity Search API Tutorial: Baue einen Echtzeit-Claim-Checker direkt im Browser

Lerne, wie du mit der Perplexity Search API eine Chrome-Erweiterung baust, die Faktenbehauptungen in Echtzeit prüft. Inklusive Node.js-Backend und API-Integrationsguide.
· 10 Min. lesen

Entdecke KI

Perplexity hat kürzlich die Perplexity Search API veröffentlicht. Sie ist auf fein abgestimmte Webrecherche ausgelegt und liefert statt kompletter Seiten sortierte, kontextsatte Snippets. Ideal für Echtzeitanwendungen wie Fact-Checking, Recherche und RAG-Pipelines, bei denen Geschwindigkeit und Präzision zählen.

In diesem Tutorial bauen wir Claim Checker, eine Browser-Erweiterung, die Faktenbehauptungen auf jeder Webseite in Echtzeit überprüft (basierend auf abgerufenen Webbelegen). Du siehst, wie der ständig aktualisierte Index der Perplexity Search API mit latenzarmer Retrieval-Architektur, semantischen Signalen und mehrstufigem Re-Ranking schnell die relevantesten Belege findet. Unterwegs lernst du, wie du:

  • Die Perplexity Search API nutzt, um hochwertige, sortierte Evidenz aus dem Web zu holen und Suchergebnisse mit eigenen Sortieralgorithmen weiterzuverarbeiten.
  • Ein Node.js-Backend entwickelst, das deinen API-Schlüssel sicher verwaltet, CORS-Anfragen verarbeitet und intelligente Ergebnislogik mit Domain-Extraktion und datumsbasierter Sortierung implementiert.
  • Eine Chrome/Edge-Erweiterung mit Content Scripts erstellst, die Textauswahl, Sidepanel-Oberfläche und nahtlose Kommunikation zwischen Browser und Backend ermöglichen.
  • Prüfergebnisse mit Quell-URLs, Titeln, Snippets, Domains und Veröffentlichungsdaten in einem interaktiven Sidepanel in Echtzeit darstellst.

Am Ende hast du ein Full-Stack-Tool zum Faktencheck direkt im Browser. Nutzende können beliebigen Text auf einer Seite markieren und sofort verifizierte Ergebnisse mit Quellen sehen, während die API-Zugangsdaten sicher serverseitig bleiben.

Schau dir außerdem unseren Guide zum Perplexity Comet Browser in einem separaten Artikel an. 

Was ist die Perplexity Search API?

Die Perplexity Search API ist ein entwicklerfreundlicher Weg, schnelle und fein granulare Websuchergebnisse zu erhalten. 

Im Gegensatz zu klassischen Search-APIs, die nur komplette Seiten liefern, gibt Perplexity sortierte Snippets und Sub-Dokument-Chunks zurück – mit genau den Sätzen und Absätzen, die zu deiner Anfrage passen. Wichtige Merkmale dieser Search API:

  • Kosteneffizient: 5 $ pro 1.000 Requests ohne tokenbasierte Abrechnung – ideal für Apps mit häufigen oder geplanten Checks.
  • Hochwertiges Retrieval: Kombination aus lexikalischem und semantischem Ranking mit mehrstufigem Re-Ranking. So erhältst du hochrelevante, aktuelle Ergebnisse aus einem riesigen, kontinuierlich aktualisierten Index (~200 Mio. Queries/Tag).
  • Niedrige Latenz: Die Perplexity Search API antwortet schnell – deine UI bleibt reaktionsfreudig, selbst wenn du mehrere Claims parallel prüfst.

Für unsere Erweiterung heißt das: Wir können jeden Satz als Query senden und erhalten verlässliche, stützende oder widersprechende Evidenz zurück – inklusive Titel, URLs und Kontextschnipsel. Perfekt für Fact-Checking in Echtzeit.

Perplexity Search API Projekt: Browser-Claim-Checker-Erweiterung

In diesem Abschnitt bauen wir eine Browser-Erweiterung, die Faktenbehauptungen mit der Perplexity Search API sofort überprüft. Das Projekt kombiniert:

  • Ein schlankes Node.js-Backend, das markierten Text an die Perplexity Search API sendet und Ergebnisse aufbereitet
  • Eine Chrome-Erweiterung mit Sidepanel-Oberfläche zur Anzeige der Prüfdetails
  • Ein Content Script, das vom Nutzer ausgewählten Text auf jeder Webseite erfasst

Am Ende sollte deine Repository-Struktur so aussehen:

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

Gehen wir die Teile des Projekts durch und schauen, wie sie zusammenspielen, um zuverlässige Claim-Verifizierung direkt im Browser zu liefern.

Schritt 1: Abhängigkeiten und Libraries 

Bevor du beginnst, stelle sicher, dass Folgendes vorhanden ist:

  • Ein Perplexity-Konto. Nach Registrierung oder Login findest du in der linken Seitenleiste den API-Bereich mit deiner API-Gruppe und den zugehörigen Einstellungen.

  • Gehe im API-Portal zum Tab „API Billing“ und hinterlege deine Zahlungsmethode. Für diese Demo reichen etwa 10 $ Guthaben für die Requests aus.
  • Node.js in Version 18 oder höher.
  • Chromium-basierter Browser (Google Chrome oder Microsoft Edge).

Installiere anschließend die benötigten Backend-Abhängigkeiten:

cd server
npm install express cors dotenv

Lege im Server-Verzeichnis eine .env-Datei an und füge deinen Perplexity-API-Schlüssel hinzu:

PERPLEXITY_API_KEY=your_api_key_here

Jetzt sind alle Abhängigkeiten installiert. Als Nächstes bauen wir Backend-Server und Erweiterung.

Schritt 2: Backend aufsetzen

Die Datei server.js richtet ein schlankes Node.js-Backend mit Express ein. Das Backend dient als Brücke zwischen der Browser-Erweiterung und der Perplexity Search API. Es erhält vom Nutzer markierte oder eingegebene Claims, fragt Perplexity nach passender Evidenz und liefert eine bereinigte, strukturierte Antwort für die UI der Erweiterung zurück.

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

Der obige Code ist die Sicherheits- und Logikschicht für die Claim-Checker-Erweiterung und übernimmt Folgendes:

  • Sicheres, logisches Backend: Nimmt Claims über die Route POST /check-claim entgegen, validiert die Eingabe und ruft die Perplexity Search API mit einem serverseitig gespeicherten API-Schlüssel auf.
  • Verarbeitung und Strukturierung: Der Server empfängt relevante Webquellen und Snippets, sortiert sie nach Aktualität über last_updated und Datum, wählt die Top 3 aus und gibt eine saubere JSON-Antwort für die Erweiterung zurück.
  • Key-Sicherheit: Das Backend hält deinen API-Schlüssel aus dem Browser heraus und erlaubt zusätzliche Logik wie Ranking, Filterung oder Caching, bevor Daten die Nutzer erreichen.
  • Erweiterbares Design: So bleibt die Erweiterung sicher und lässt sich leicht um Features wie Advanced Scoring, Source-Merging oder Analytics ausbauen.

Unser Server steht. Jetzt bauen wir die Chrome-Erweiterung.

Schritt 3: Die Chrome-Erweiterung bauen

In diesem Schritt erstellen wir die Chrome-Erweiterung für das Nutzererlebnis des Claim Checkers. Die Erweiterung erfasst Claims auf jeder Webseite, sendet sie an unser Backend und zeigt die Prüfergebnisse in einer intuitiven Oberfläche an.

Bevor wir in die Dateien eintauchen, hier die Kernkomponenten:

  • background.js: Läuft als Service Worker und steuert Hintergrundaufgaben wie das Reagieren auf Nutzeraktionen, das Öffnen des Sidepanels und das sichere Senden von Claims an das Backend zur Verifizierung.
  • content.js: Dieses Content Script läuft in der aktiven Webseite. Es extrahiert sichtbaren Artikeltext, erkennt potenzielle Faktenbehauptungen und markiert sie dynamisch, damit Nutzende sehen, was geprüft wird.
  • sidepanel.js: Steuert das interaktive Sidepanel, in dem extrahierte Claims und deren Prüfergebnisse erscheinen. Aktualisiert Ergebnisse in Echtzeit und verknüpft sie mit den Hervorhebungen auf der Seite.
  • popup.js: Treibende Kraft des Schnellzugriffs-Pop-ups beim Klick auf das Erweiterungssymbol. Bietet eine leichte Möglichkeit, Claims zu prüfen, ohne das Sidepanel zu öffnen.

Einige zusätzliche Helferdateien für die Chrome-Erweiterung findest du im GitHub-Repository.

Schritt 3.1: Background Script und Event-Handling 

Aufbauend auf unserem Chrome-Setup ist background.js der zentrale Controller, der die Erweiterung reaktionsschnell hält. Er lauscht auf Nutzeraktionen (z. B. Klick aufs Icon), öffnet bei Bedarf das Sidepanel und leitet Claims zur Verifizierung ans Backend weiter.

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

Der Code sorgt für die Reaktionsfähigkeit der Erweiterung durch Folgendes:

  • Zentrale Ereignissteuerung: Das Background Script registriert zwei Listener: chrome.action.onClicked und chrome.runtime.onMessage. Ersterer reagiert auf den Klick aufs Erweiterungssymbol und versucht, das Sidepanel zu öffnen. Zweiterer lauscht auf Nachrichten wie checkClaim aus Content Script oder UI-Komponenten.
  • Sichere API-Anbindung: Die Funktion checkClaim() sendet den ausgewählten Claim per fetch an http://localhost:3000/check-claim. Weil der API-Schlüssel serverseitig liegt, wird er nie im Browser offengelegt.
  • Robuste Fehlerbehandlung: Beide Listener nutzen try/catch, um Laufzeitfehler abzufangen. Lässt sich das Sidepanel nicht öffnen, fällt das Script auf das Pop-up zurück. Schlägt die API-Anfrage fehl, gibt es strukturierte Fehlermeldungen an die UI weiter, statt still zu scheitern.

Mit dem Background Script für Kommunikation und sichere API-Calls ist der nächste Schritt die Interaktion mit der Webseite selbst.

Schritt 3.2: Claims extrahieren und hervorheben

Die Datei content.js läuft direkt in der Webseite. Sie extrahiert Artikeltext, erkennt potenzielle Faktenbehauptungen und hebt sie für die Nutzenden sichtbar hervor. So schlägt sie die Brücke zwischen Seiteninhalt und Background-Logik, indem sie relevanten Text erfasst und Interaktion ermöglicht.

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

Die wichtigsten Teile von content.js im Überblick – wie Text extrahiert, Claims erkannt und mit der UI verknüpft werden:

  • Artikeltextextraktion: extractArticleText() durchsucht gängige Artikelcontainer und zieht den Hauptinhalt. So fokussiert die Erweiterung sinnvollen Text statt Navigation oder Werbung.
  • Claim-Erkennung: extractClaims() zerlegt den Text in Sätze, filtert nach Länge und Struktur und nutzt Regex-Stichwörter, um wahrscheinliche Faktenbehauptungen zu erkennen.
  • Dynamisches Highlighting: highlightTextOnPage() findet den Claim im DOM und umschließt ihn mit einem gestylten <span>. Hover- und Klick-Interaktionen verknüpfen die Hervorhebung mit den Ergebnissen im Sidepanel.
  • Nachrichtensteuerung: Der Listener chrome.runtime.onMessage koordiniert Aktionen wie Claim-Extraktion, Hervorhebungen setzen und wieder entfernen. So bleiben Seite und Erweiterung synchron.

Nachdem das Content Script die Texterfassung und Hervorhebung übernimmt, präsentieren wir die Claims und ihre Prüfergebnisse im sidepanel.js.

Schritt 3.3: Sidepanel-UI

Die Datei sidepanel.js steuert das interaktive Sidepanel, in dem Nutzende Claims und ihre Ergebnisse ansehen. Sie verbindet Content Script und Backend-Antworten mit einer übersichtlichen, scrollbaren Oberfläche.

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

So funktioniert das Sidepanel in deinem Browser:

  • Dynamische Claim-Liste: Beim Klick auf „Check Claims“ fordert das Script Claims vom aktiven Tab an, entfernt alte Hervorhebungen und erstellt eine Liste mit Platzhaltern.
  • Echtzeit-Updates: Jeder Claim wird via chrome.runtime.sendMessage mit der Aktion checkClaim ans Background Script gesendet. Nach der Serverantwort aktualisiert die UI sofort Quellen, Domains und Snippets.
  • Interaktive Verknüpfung: Der Listener für highlightClaimInPanel sorgt dafür, dass ein Klick auf hervorgehobenen Text zur passenden Claim-Detailansicht im Sidepanel springt. Das schafft einen flüssigen Navigations-Loop zwischen Seite und Ergebnissen.

Als Nächstes ergänzen wir ein Schnellzugriffs-Pop-up, um die Nutzung der Erweiterung noch einfacher zu machen.

Schritt 3.4: Schnellzugriffs-Pop-up-UI

popup.js steuert ein leichtgewichtiges Pop-up, das erscheint, wenn Nutzende auf das Erweiterungssymbol klicken. Im Unterschied zum Sidepanel bietet es eine schnelle On-Demand-Prüfung, ohne die aktuelle Seite zu verlassen oder ein volles Panel zu öffnen.

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

So liefert popup.js einen On-Demand-Claim-Check direkt in der Toolbar der Erweiterung.

  • Seitenscan und Anzeige: Beim Klick auf „Check“ fordert das Pop-up Text vom aktiven Tab an, extrahiert schnell potenzielle Claims und listet sie im Pop-up auf.
  • Verifizierung im Backend: Jeder Claim wird über die Route /check-claim geprüft. Das Pop-up aktualisiert sich in Echtzeit mit Urteilen, Konfidenz-Balken, Erläuterungen, Gegenargumenten und Quelllinks.

Damit ist unsere Erweiterung bereit zum Testen. 

Hinweis: Alle Code-Dateien zu diesem Projekt findest du im GitHub-Repository.

Schritt 4: App starten

Starte den Backend-Server mit folgendem Befehl:

npm start

Sobald der Server läuft, lade die Chrome-Erweiterung:

  • Öffne Chrome und klicke auf „Extensions“ (rechts neben der Suchleiste).
  • Aktiviere den „Developer mode“ und klicke oben links auf „Load unpacked“.
  • Lade anschließend unseren Extensions-Ordner mit allen Dateien und warte auf die Bestätigung.

  • Nach der Bestätigung klickst du auf einer beliebigen Webseite auf das Claim-Checker-Icon, markierst den Text zur Überprüfung und startest den Check.

Schauen wir uns die Ergebnisse an:

Fazit

Du hast jetzt einen schnellen und günstigen Claim Checker, der vollständig von der Perplexity Search API angetrieben wird (keine LLM-Calls nötig). Die Demo zeigt die Stärken der API: frische, verlässliche Suchergebnisse, Relevanz auf Snippet-Ebene, geringe Latenz und planbare Kosten.

Von hier aus kannst du das Projekt erweitern – etwa mit fortgeschrittenen Filtern, geplanten automatischen Checks oder als Pre-Filter für RAG-Pipelines. Mehr Möglichkeiten findest du in den Perplexity API Docs und Quickstarts, um erweiterte Such- und Filterfunktionen zu nutzen.

Aashi Dutt

Autor

Aashi Dutt

Ich bin Google Developers Expertin für ML (Gen AI), dreifache Kaggle-Expertin und Women-Techmakers-Botschafterin mit über drei Jahren Erfahrung in der Tech-Branche. 2020 habe ich ein Health-Tech-Startup mitgegründet und absolviere derzeit einen Master in Informatik an der Georgia Tech mit Schwerpunkt Machine Learning.

Themen

Ähnlich