Lewati ke konten utama

Tutorial Perplexity Search API: Bangun Pemeriksa Klaim Real-Time di Peramban Anda

Pelajari cara menggunakan Perplexity Search API untuk membangun ekstensi Chrome yang memverifikasi klaim faktual secara real-time. Termasuk panduan penyiapan backend Node.js dan integrasi API.
Diperbarui 28 Sep 2026  · 10 mnt Baca

Jelajahi bersama AI

ChatGPTClaudePerplexity

Baru-baru ini, Perplexity merilis Perplexity Search API yang dirancang untuk pengambilan web terperinci yang mengembalikan cuplikan berperingkat dan kaya konteks alih-alih halaman penuh. API ini dibuat untuk kebutuhan real-time seperti pemeriksaan fakta, riset, dan pipeline RAG, di mana kecepatan dan presisi sangat penting.

Dalam tutorial ini, kita akan membangun Claim Checker, sebuah ekstensi peramban yang memverifikasi pernyataan faktual pada halaman web apa pun secara real-time (berdasarkan bukti web yang diambil). Anda akan melihat bagaimana indeks yang selalu diperbarui dan tumpukan pengambilan berlatensi rendah milik Perplexity Search API, dikombinasikan dengan sinyal semantik dan re-ranking multi-tahap, memudahkan untuk menemukan bukti paling relevan dengan cepat. Sepanjang proses, Anda akan mempelajari cara:

  • Memanfaatkan Perplexity Search API untuk mengambil bukti berkualitas tinggi yang sudah berperingkat dari web dan memproses hasil pencarian dengan algoritma pengurutan kustom.
  • Mengembangkan backend Node.js untuk mengelola kunci API Anda secara aman, menangani permintaan CORS, dan menerapkan pemrosesan hasil yang cerdas dengan ekstraksi domain dan pemeringkatan berbasis tanggal.
  • Membuat ekstensi Chrome/Edge dengan content script yang memungkinkan pemilihan teks, antarmuka panel samping, dan komunikasi mulus antara peramban dan server backend Anda.
  • Menampilkan hasil verifikasi, termasuk URL sumber, judul, cuplikan, domain, dan tanggal publikasi dalam panel samping interaktif dengan pemrosesan klaim real-time.

Di akhir, Anda akan memiliki alat pemeriksa fakta full-stack yang bekerja mulus di dalam peramban Anda. Pengguna dapat memilih teks apa pun pada halaman web dan langsung mendapatkan hasil verifikasi dengan sumber pendukung, sambil tetap menjaga kredensial API mereka aman di sisi server.

Anda juga dapat melihat panduan untuk Comet Browser milik Perplexity pada artikel terpisah. 

Apa itu Perplexity Search API?

Perplexity Search API adalah cara ramah-pengembang untuk mendapatkan hasil pencarian web yang cepat dan terperinci. 

Berbeda dengan API pencarian tradisional yang hanya mengembalikan halaman penuh, Perplexity memberikan cuplikan berperingkat dan potongan sub-dokumen yang memuat kalimat dan paragraf yang persis cocok dengan kueri Anda. Beberapa fitur kunci dari search API ini meliputi:

  • Efisien biaya: Search API seharga $5 per 1.000 permintaan tanpa penagihan berbasis token, sehingga ideal untuk aplikasi yang menjalankan pemeriksaan sering atau terjadwal.
  • Pengambilan berkualitas tinggi: Menggabungkan perankingan leksikal dan semantik dengan re-ranking multi-tahap, sehingga Anda mendapatkan hasil yang sangat relevan dan mutakhir dari indeks masif yang terus diperbarui (~200 juta kueri/hari).
  • Latensi rendah: Perplexity Search API mengembalikan kueri dengan cepat, menjaga UI Anda tetap responsif bahkan saat memverifikasi beberapa klaim sekaligus.

Untuk ekstensi kita, ini berarti kita dapat mengirim kalimat apa pun sebagai kueri dan mendapatkan bukti pendukung atau yang bertentangan secara andal, lengkap dengan judul, URL, dan cuplikan konteks—sempurna untuk pemeriksaan fakta real-time.

Proyek Perplexity Search API: Ekstensi Browser Claim Checker

Pada bagian ini, kita akan membangun ekstensi pemeriksa klaim di peramban yang langsung memverifikasi klaim faktual menggunakan Perplexity Search API. Proyek ini menggabungkan:

  • Backend Node.js ringan yang mengirim teks terpilih ke Perplexity Search API dan memproses hasilnya
  • Ekstensi Chrome dengan antarmuka panel samping untuk menampilkan detail verifikasi
  • Content script yang menangkap teks yang dipilih pengguna dari halaman web mana pun

Di akhir proyek, struktur repositori Anda seharusnya terlihat seperti ini:

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

Mari kita telusuri setiap bagian proyek ini dan lihat bagaimana semuanya bekerja sama untuk menghadirkan verifikasi klaim yang andal di dalam peramban.

Langkah 1: Dependensi dan pustaka 

Sebelum mulai, pastikan prasyarat berikut tersedia:

  • Akun Perplexity. Setelah mendaftar atau masuk, buka bilah sisi kiri dan perluas bagian API untuk mengakses grup API Anda dan pengaturan terkait.

  • Buka tab API Billing di API Portal Anda dan tambahkan metode pembayaran. Untuk demo ini, sekitar $10 dalam kredit sudah cukup untuk menutupi permintaan.
  • Node.js versi 18 atau lebih baru.
  • Peramban berbasis Chromium (Google Chrome atau Microsoft Edge).

Selanjutnya, instal dependensi yang diperlukan untuk backend:

cd server
npm install express cors dotenv

Buat berkas .env di direktori server dan tambahkan kunci API Perplexity Anda:

PERPLEXITY_API_KEY=your_api_key_here

Sekarang semua dependensi sudah terpasang.  Selanjutnya, kita dapat membangun server backend dan ekstensi.

Langkah 2: Membangun backend

Berkas server.js menyiapkan backend Node.js ringan menggunakan Express. Backend ini bertindak sebagai jembatan antara ekstensi peramban dan Perplexity Search API. Ia menerima klaim yang dipilih atau dimasukkan pengguna, mengkueri Perplexity untuk bukti yang relevan, dan mengembalikan respons yang dibersihkan dan terstruktur siap tampil di UI ekstensi.

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

Kode di atas adalah lapisan keamanan dan logika untuk ekstensi Claim Checker, yang menjalankan fungsi-fungsi berikut:

  • Backend yang aman dan logis: Menerima klaim melalui rute POST /check-claim, memvalidasi masukan, dan memanggil Perplexity Search API menggunakan kunci API yang disimpan di server.
  • Pemrosesan dan penataan: Server menerima daftar sumber web dan cuplikan yang relevan, mengurutkannya berdasarkan kebaruan menggunakan bidang last_updated dan tanggal, memilih tiga teratas, dan mengembalikan respons JSON yang rapi untuk ekstensi.
  • Keamanan kunci: Menjalankan backend ini menjaga kunci API Anda tersembunyi dari peramban dan memungkinkan Anda menambahkan logika tambahan, seperti pemeringkatan, pemfilteran, atau caching, sebelum data sampai ke pengguna.
  • Desain yang mudah dikembangkan: Pendekatan ini membuat ekstensi lebih aman dan mudah diperluas untuk peningkatan di masa mendatang seperti penilaian lanjutan, penggabungan multi-sumber, atau analitik.

Sekarang server kita sudah siap. Mari kita bangun ekstensi Chrome.

Langkah 3: Membangun ekstensi Chrome

Pada langkah ini, kita akan membangun ekstensi Chrome yang menjalankan pengalaman pengguna Claim Checker. Ekstensi bertanggung jawab untuk menangkap klaim dari halaman web mana pun, mengirimkannya ke backend kita, dan menampilkan hasil verifikasi dalam antarmuka yang intuitif.

Sebelum membahas setiap berkas, berikut fungsi komponen intinya:

  • background.js: Script ini berjalan sebagai service worker dan mengelola tugas latar belakang seperti mendengarkan aksi pengguna, membuka panel samping, dan mengirim klaim ke backend untuk verifikasi secara aman.
  • content.js:  Content script ini berjalan di dalam halaman web aktif. Ia mengekstrak teks artikel yang terlihat, mengidentifikasi potensi klaim faktual, dan menyorotnya secara dinamis agar pengguna dapat melihat apa yang sedang diperiksa.
  • sidepanel.js: Mengontrol UI panel samping interaktif yang menampilkan klaim yang diekstrak dan status verifikasinya. Memperbarui hasil secara real-time dan menautkannya ke sorotan di halaman.
  • popup.js: Menjalankan popup akses cepat saat pengguna mengeklik ikon ekstensi. Menyediakan cara ringan untuk memeriksa klaim tanpa membuka panel samping.

Anda dapat menemukan beberapa berkas pembantu tambahan untuk ekstensi Chrome di repositori GitHub.

Langkah 3.1: Background script dan penanganan event 

Melanjutkan penyiapan ekstensi Chrome kita, berkas background.js adalah pengendali pusat yang menjaga ekstensi tetap responsif. Ia mendengarkan aksi pengguna (seperti mengklik ikon ekstensi), membuka panel samping saat diperlukan, dan meneruskan klaim ke backend untuk verifikasi.

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

Kode di atas berfokus pada responsivitas ekstensi dengan memasukkan hal-hal berikut:

  • Hub peristiwa pusat: Background script mendaftarkan dua pendengar utama, chrome.action.onClicked dan chrome.runtime.onMessage. Yang pertama bereaksi saat pengguna mengeklik ikon ekstensi dan mencoba membuka panel samping. Yang kedua mendengarkan pesan khusus seperti checkClaim yang datang dari content script atau komponen UI.
  • Penanganan API yang aman: Fungsi checkClaim() menggunakan API fetch untuk mengirim klaim yang dipilih ke backend di http://localhost:3000/check-claim; karena kunci API disimpan di sisi server, peramban tidak pernah mengeksposnya, menjaga kredensial sensitif tetap aman.
  • Ketahanan terhadap error: Kedua pendengar event menyertakan blok try/catch untuk menangani error runtime. Jika panel samping tidak dapat dibuka, script kembali mengatur popup. Saat panggilan API gagal, script mencatat dan mengembalikan pesan error terstruktur ke UI agar pengguna tidak mengalami kegagalan senyap.

Dengan background script menangani komunikasi dan panggilan API yang aman, langkah berikutnya adalah berinteraksi langsung dengan halaman web.

Langkah 3.2: Mengekstrak dan menyorot klaim

Berkas content.js berjalan langsung di dalam halaman web dan bertanggung jawab untuk mengekstrak teks artikel, mengidentifikasi potensi klaim faktual, dan menyorotnya secara visual untuk pengguna. Ia menjembatani konten halaman dan logika latar belakang ekstensi dengan menangkap teks yang relevan dan memungkinkan interaksi pengguna.

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

Mari kita uraikan bagian-bagian kunci dari script content.js untuk memahami bagaimana ia mengekstrak teks yang bermakna, mendeteksi potensi klaim, dan menghubungkan konten yang disorot ke UI ekstensi.

  • Ekstraksi teks artikel: Fungsi extractArticleText() memindai kontainer artikel umum untuk mengambil konten utama yang dapat dibaca. Ini membantu memastikan ekstensi berfokus pada teks yang bermakna alih-alih menu navigasi atau iklan.
  • Logika deteksi klaim: Fungsi extractClaims() memecah teks menjadi kalimat, menerapkan filter panjang dan struktur, serta menggunakan kata kunci berbasis regex untuk mendeteksi pernyataan yang kemungkinan merupakan klaim faktual.
  • Penyorotan dinamis: Fungsi highlightTextOnPage() menemukan klaim yang terdeteksi dalam DOM dan membungkusnya dengan <span> bergaya. Ia menambahkan interaksi hover dan klik agar pengguna dapat menautkan secara visual klaim di halaman ke hasil verifikasinya di panel samping.
  • Penanganan pesan: Pendengar chrome.runtime.onMessage mengoordinasikan aksi seperti mengekstrak klaim, menambahkan sorotan, dan menghapusnya saat diperlukan. Ini menjaga halaman dan ekstensi tetap tersinkronisasi dan memastikan interaksi pengguna yang mulus.

Dengan content script yang menangani ekstraksi teks dan penyorotan klaim pada halaman, langkah berikutnya adalah menyajikan klaim tersebut dan hasil verifikasinya kepada pengguna. Di sinilah antarmuka sidepanel.js berperan.

Langkah 3.3: UI panel samping

Berkas sidepanel.js menjalankan panel samping interaktif tempat pengguna dapat melihat klaim yang diekstrak dan hasil verifikasinya. Ia menghubungkan content script dan respons backend ke antarmuka yang bersih dan dapat di-scroll di dalam ekstensi.

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

Berikut cara kerja panel samping di peramban Anda:

  • Daftar klaim dinamis: Saat pengguna mengeklik tombol “Check Claims”, script meminta klaim dari tab aktif, menghapus sorotan lama, dan membuat daftar klaim dengan status placeholder.
  • Pembaruan verifikasi real-time: Setiap klaim dikirim ke background script menggunakan chrome.runtime.sendMessage dengan aksi checkClaim. Saat backend mengembalikan hasil, UI langsung diperbarui dengan sumber, domain, dan cuplikan.
  • Penautan interaktif: Script mendengarkan pesan highlightClaimInPanel, sehingga mengeklik teks yang disorot pada halaman akan menggulir ke detail klaim yang sesuai di panel samping. Ini menciptakan alur navigasi mulus antara sorotan halaman dan data verifikasi.

Selanjutnya, kita akan menambahkan popup akses cepat untuk memudahkan peluncuran dan penggunaan ekstensi yang dibangun pada langkah sebelumnya.

Langkah 3.4: UI popup akses cepat

Berkas popup.js adalah antarmuka popup ringan yang muncul saat pengguna mengeklik ikon ekstensi. Berbeda dengan panel samping, popup ini menyediakan cara yang lebih cepat dan sesuai kebutuhan untuk memeriksa klaim tanpa meninggalkan halaman saat ini atau membuka panel interaktif penuh.

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

Berikut cara script popup.js menjalankan pemeriksa klaim sesuai permintaan langsung dari toolbar ekstensi.

  • Pindai halaman dan tampilkan klaim: Saat pengguna mengeklik tombol “Check”, popup meminta teks dari tab aktif melalui content script, dengan cepat mengekstrak potensi klaim, dan menampilkannya di jendela popup.
  • Verifikasi backend: Setiap klaim dikirim ke background script untuk memanggil rute API /check-claim. Popup diperbarui secara real-time dengan putusan, bilah kepercayaan, penjelasan, sanggahan, dan tautan sumber yang dikembalikan server.

Sekarang, ekstensi kita siap diuji. 

Catatan: Anda dapat menemukan semua berkas kode untuk proyek ini di repositori GitHub.

Langkah 4: Menjalankan aplikasi

Mulai server backend dengan menjalankan perintah berikut:

npm start

Setelah server berjalan, muat ekstensi Chrome:

  • Buka Chrome dan klik “Extensions” (di sisi kanan bilah pencarian).
  • Aktifkan “Developer mode” dan klik “Load unpacked” (bagian kiri atas). 
  • Selanjutnya, muat folder ekstensi kita yang berisi semua berkas dan tunggu konfirmasi.

  • Setelah Chrome mengonfirmasi ekstensi telah ditambahkan, klik ikon Claim Checker pada halaman web mana pun, pilih teks yang ingin Anda verifikasi, dan jalankan pemeriksaan klaim.

Mari kita lihat hasilnya:

Kesimpulan

Sekarang Anda memiliki pemeriksa klaim yang cepat dan hemat biaya yang sepenuhnya didukung oleh Perplexity Search API (tanpa panggilan LLM). Demo ini menyoroti kekuatan utama API, seperti hasil pencarian yang segar dan andal, relevansi pada tingkat cuplikan, latensi rendah, dan harga yang dapat diprediksi.

Dari sini, Anda dapat memperluas proyek dengan menambahkan filter lanjutan, menjadwalkan pemeriksaan otomatis, atau mengintegrasikannya sebagai pra-filter untuk pipeline RAG. Untuk melangkah lebih jauh, jelajahi Perplexity API docs dan panduan quickstart untuk membuka lebih banyak kemampuan pencarian dan pemfilteran tingkat lanjut.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Saya adalah Google Developers Expert di ML (Gen AI), Kaggle 3x Expert, dan Women Techmakers Ambassador dengan pengalaman lebih dari 3 tahun di bidang teknologi. Saya ikut mendirikan startup health-tech pada 2020 dan sedang menempuh studi magister ilmu komputer di Georgia Tech dengan spesialisasi machine learning.

Topik
Kecerdasan Buatan
Agen AI

Kursus Teratas di DataCamp

Kursus

Pengantar Agen Kecerdasan Buatan

1 jam 30 menit
142.8K
Pelajari dasar-dasar agen AI, komponennya, serta penerapannya di dunia nyata — tanpa perlu coding.
Lihat DetailRight Arrow
Mulai Kursus
Lihat SelengkapnyaRight Arrow
Terkait

blogs

12 Alternatif ChatGPT Terbaik yang Bisa Anda Coba pada 2026

Artikel ini menyajikan daftar alternatif ChatGPT yang akan meningkatkan produktivitas Anda.
Javier Canales Luna's photo

Javier Canales Luna

14 mnt

blogs

40 Pertanyaan Wawancara DBMS Teratas di 2026

Kuasai pertanyaan wawancara basis data, dari konsep SQL dasar hingga skenario desain sistem tingkat lanjut. Panduan mendalam ini mencakup semua yang Anda perlukan untuk sukses di wawancara DBMS dan meraih peran berikutnya.
Dario Radečić's photo

Dario Radečić

15 mnt

blogs

Spaghetti Plot dan Jalur Badai

Temukan alasan mengapa Anda sebaiknya (tidak) menggunakan spaghetti plot untuk menyampaikan ketidakpastian jalur prediksi badai serta dampaknya terhadap interpretasi.
Hugo Bowne-Anderson's photo

Hugo Bowne-Anderson

13 mnt

blogs

Tutorial Korelasi di R

Dapatkan pengenalan dasar-dasar korelasi di R: pelajari lebih lanjut tentang koefisien korelasi, matriks korelasi, plotting korelasi, dan sebagainya.
David Woods's photo

David Woods

13 mnt

Lihat SelengkapnyaLihat Selengkapnya