Kursus
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_updateddan 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.onClickeddanchrome.runtime.onMessage. Yang pertama bereaksi saat pengguna mengeklik ikon ekstensi dan mencoba membuka panel samping. Yang kedua mendengarkan pesan khusus seperticheckClaimyang datang dari content script atau komponen UI. - Penanganan API yang aman: Fungsi
checkClaim()menggunakan APIfetchuntuk mengirim klaim yang dipilih ke backend dihttp://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/catchuntuk 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.onMessagemengoordinasikan 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.sendMessagedengan aksicheckClaim. 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.
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.

