Chuyển đến nội dung chính

Hướng dẫn Perplexity Search API: Xây dựng công cụ kiểm tra tuyên bố theo thời gian thực ngay trên trình duyệt

Tìm hiểu cách dùng Perplexity Search API để xây dựng tiện ích Chrome xác minh phát biểu thực tế theo thời gian thực. Bao gồm thiết lập backend Node.js và hướng dẫn tích hợp API.
· 10 phút đọc

Khám phá cùng AI

Gần đây, Perplexity đã ra mắt Perplexity Search API, được thiết kế cho việc truy xuất web chi tiết, trả về các đoạn trích giàu ngữ cảnh đã được xếp hạng thay vì toàn bộ trang. API này phục vụ các tình huống thời gian thực như kiểm chứng thông tin, nghiên cứu, và các pipeline RAG, nơi tốc độ và độ chính xác là ưu tiên.

Trong hướng dẫn này, chúng ta sẽ xây dựng Claim Checker, một tiện ích mở rộng trình duyệt giúp xác minh các phát biểu thực tế trên bất kỳ trang web nào theo thời gian thực (dựa trên bằng chứng truy xuất từ web). Bạn sẽ thấy chỉ mục luôn được làm mới và ngăn xếp truy xuất độ trễ thấp của Perplexity Search API, kết hợp với tín hiệu ngữ nghĩa và tái xếp hạng nhiều giai đoạn, giúp tìm nhanh bằng chứng phù hợp nhất. Trong quá trình này, bạn sẽ học cách:

  • Khai thác Perplexity Search API để lấy bằng chứng chất lượng cao, có xếp hạng từ web và xử lý kết quả tìm kiếm bằng thuật toán sắp xếp tùy chỉnh.
  • Phát triển backend Node.js để quản lý an toàn khóa API của bạn, xử lý yêu cầu CORS, và triển khai xử lý kết quả thông minh với trích xuất miền và xếp hạng theo ngày.
  • Tạo tiện ích Chrome/Edge với content script cho phép chọn văn bản, giao diện bảng bên, và giao tiếp mượt giữa trình duyệt với máy chủ backend của bạn.
  • Hiển thị kết quả xác minh gồm URL nguồn, tiêu đề, đoạn trích, miền và ngày xuất bản trong bảng bên tương tác với xử lý tuyên bố theo thời gian thực.

Kết thúc bài, bạn sẽ có một công cụ kiểm chứng thông tin full-stack hoạt động mượt trong trình duyệt. Người dùng có thể chọn bất kỳ văn bản nào trên trang và nhận ngay kết quả xác minh kèm nguồn tham khảo, đồng thời vẫn giữ an toàn thông tin xác thực API ở phía máy chủ.

Bạn cũng có thể xem hướng dẫn về Comet Browser của Perplexity trong một bài viết riêng. 

Perplexity Search API là gì?

 Perplexity Search API là cách thân thiện với nhà phát triển để nhận kết quả tìm kiếm web nhanh và chi tiết. 

Khác với các API tìm kiếm truyền thống chỉ trả về toàn bộ trang, Perplexity cung cấp các đoạn trích và mảnh tài liệu con đã xếp hạng, chứa chính xác câu và đoạn phù hợp với truy vấn của bạn. Một số tính năng chính của API tìm kiếm này gồm:

  • Tiết kiệm chi phí: Search API có giá $5 cho mỗi 1.000 yêu cầu, không tính theo token, lý tưởng cho ứng dụng chạy kiểm tra thường xuyên hoặc theo lịch.
  • Truy xuất chất lượng cao: Kết hợp xếp hạng từ vựng và ngữ nghĩa với tái xếp hạng nhiều giai đoạn, giúp bạn nhận kết quả cực kỳ liên quan, cập nhật từ một chỉ mục khổng lồ, luôn được làm mới (~200 triệu truy vấn/ngày).
  • Độ trễ thấp: Perplexity search API phản hồi nhanh, giữ cho giao diện của bạn mượt mà ngay cả khi xác minh nhiều tuyên bố cùng lúc.

Với tiện ích của chúng ta, điều này có nghĩa là có thể truyền bất kỳ câu nào làm truy vấn và nhận về bằng chứng ủng hộ hoặc phủ nhận đáng tin cậy, kèm tiêu đề, URL và đoạn trích ngữ cảnh — hoàn hảo cho kiểm chứng theo thời gian thực.

Dự án Perplexity Search API: Tiện ích kiểm tra tuyên bố trên trình duyệt

Trong phần này, chúng ta sẽ xây dựng tiện ích mở rộng trình duyệt kiểm tra tuyên bố, xác minh ngay lập tức các phát biểu thực tế bằng Perplexity Search API. Dự án kết hợp:

  • Backend Node.js gọn nhẹ gửi văn bản đã chọn tới Perplexity Search API và xử lý kết quả
  • Tiện ích Chrome với giao diện bảng bên để hiển thị chi tiết xác minh
  • Content script thu nhận văn bản do người dùng chọn từ bất kỳ trang web nào

Kết thúc dự án, cấu trúc kho mã của bạn sẽ trông như sau:

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

Hãy đi qua từng phần của dự án và xem chúng phối hợp thế nào để mang lại xác minh tuyên bố đáng tin cậy ngay trong trình duyệt.

Bước 1: Phụ thuộc và thư viện 

Trước khi bắt đầu, hãy đảm bảo các điều kiện tiên quyết sau:

  • Một tài khoản Perplexity. Sau khi đăng ký/đăng nhập, hãy vào thanh bên trái và mở rộng phần API để truy cập nhóm API và các thiết lập liên quan.

  • Vào tab API Billing trong Cổng API của bạn và thêm phương thức thanh toán. Với bản demo này, khoảng $10 tín dụng là đủ để chi trả các yêu cầu.
  • Node.js phiên bản 18 trở lên.
  • Trình duyệt nền tảng Chromium (Google Chrome hoặc Microsoft Edge).

Tiếp theo, cài đặt các phụ thuộc cần thiết cho backend:

cd server
npm install express cors dotenv

Tạo tệp .env trong thư mục server và thêm khóa API Perplexity của bạn:

PERPLEXITY_API_KEY=your_api_key_here

Giờ chúng ta đã cài đặt xong các phụ thuộc. Tiếp theo, có thể xây dựng máy chủ backend và tiện ích mở rộng.

Bước 2: Xây dựng backend

Tệp server.js thiết lập backend Node.js gọn nhẹ bằng Express. Backend này là cầu nối giữa tiện ích trình duyệt và Perplexity Search API. Nó nhận các tuyên bố do người dùng chọn hoặc nhập, truy vấn Perplexity để lấy bằng chứng liên quan, rồi trả về phản hồi đã làm sạch, có cấu trúc, sẵn sàng hiển thị trên giao diện của tiện ích.

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

Đoạn mã trên là lớp bảo mật và logic cho tiện ích Claim Checker, thực hiện các chức năng sau:

  • Backend an toàn và có logic: Nhận một tuyên bố qua tuyến POST /check-claim, xác thực đầu vào, và gọi Perplexity Search API bằng khóa API lưu ở máy chủ.
  • Xử lý và cấu trúc hóa: Máy chủ nhận danh sách nguồn web và đoạn trích liên quan, sắp xếp theo độ mới bằng trường last_updated và ngày, chọn 3 mục hàng đầu, rồi trả về JSON gọn gàng cho tiện ích.
  • An toàn khóa: Chạy backend này giúp khóa API của bạn không lộ trên trình duyệt và cho phép bổ sung logic như xếp hạng, lọc, hoặc cache trước khi dữ liệu tới tay người dùng.
  • Thiết kế dễ mở rộng: Cách tiếp cận này giúp tiện ích an toàn hơn và dễ mở rộng cho các cải tiến tương lai như chấm điểm nâng cao, hợp nhất đa nguồn, hoặc phân tích.

Giờ chúng ta đã sẵn sàng cho máy chủ. Hãy xây dựng tiện ích Chrome.

Bước 3: Xây dựng tiện ích Chrome

Trong bước này, chúng ta sẽ xây dựng tiện ích Chrome cung cấp trải nghiệm người dùng cho Claim Checker. Tiện ích chịu trách nhiệm thu nhận tuyên bố từ bất kỳ trang web nào, gửi tới backend, và hiển thị kết quả xác minh trong giao diện trực quan.

Trước khi đi sâu vào từng tệp, dưới đây là chức năng của các thành phần lõi:

  • background.js: Chạy dưới dạng service worker, quản lý tác vụ nền như lắng nghe hành động người dùng, mở bảng bên, và gửi tuyên bố an toàn tới backend để xác minh.
  • content.js:  Chạy bên trong trang đang mở. Trích xuất văn bản bài viết hiển thị, nhận diện các tuyên bố có thể kiểm chứng, và làm nổi bật động để người dùng thấy nội dung đang được kiểm tra.
  • sidepanel.js: Điều khiển UI bảng bên tương tác hiển thị các tuyên bố trích xuất và trạng thái xác minh của chúng. Cập nhật kết quả theo thời gian thực và liên kết với vùng highlight trên trang.
  • popup.js: Vận hành pop-up truy cập nhanh khi người dùng nhấp biểu tượng tiện ích. Cung cấp cách kiểm tra nhẹ nhàng mà không cần mở bảng bên.

Bạn có thể tìm một số tệp trợ giúp thêm cho tiện ích Chrome trong kho GitHub.

Bước 3.1: Background script và xử lý sự kiện 

Dựa trên thiết lập tiện ích Chrome của chúng ta, tệp background.js là bộ điều khiển trung tâm giúp tiện ích luôn phản hồi. Nó lắng nghe hành động người dùng (như nhấp biểu tượng tiện ích), mở bảng bên khi cần, và chuyển tiếp tuyên bố tới backend để xác minh.

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

Đoạn mã trên tập trung vào tính phản hồi của tiện ích thông qua các điểm sau:

  • Trung tâm sự kiện: Background script đăng ký hai listener chính, chrome.action.onClicked và chrome.runtime.onMessage. Listener đầu phản hồi khi người dùng nhấp biểu tượng tiện ích và cố mở bảng bên. Listener thứ hai lắng nghe thông điệp tùy chỉnh như checkClaim từ content script hoặc các thành phần UI.
  • Xử lý API an toàn: Hàm checkClaim() dùng fetch để gửi tuyên bố đã chọn tới backend tại http://localhost:3000/check-claim; vì khóa API nằm ở phía máy chủ nên trình duyệt không bao giờ lộ khóa, bảo mật thông tin nhạy cảm.
  • Chống lỗi tốt: Cả hai listener đều có khối try/catch để xử lý lỗi runtime. Nếu không mở được bảng bên, script sẽ quay về dùng pop-up. Khi gọi API lỗi, tiện ích ghi log và trả về thông điệp lỗi có cấu trúc cho UI để tránh lỗi im lặng.

Với background script xử lý giao tiếp và gọi API an toàn, bước tiếp theo là tương tác trực tiếp với trang web.

Bước 3.2: Trích xuất và làm nổi bật tuyên bố

Tệp content.js chạy ngay trong trang và chịu trách nhiệm trích xuất văn bản bài viết, xác định các tuyên bố có thể kiểm chứng, và làm nổi bật trực quan cho người dùng. Nó bắc cầu giữa nội dung trang và logic nền của tiện ích bằng cách thu thập văn bản liên quan và cho phép tương tác.

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

Hãy phân tích các phần chính của content.js để hiểu cách nó trích xuất văn bản có ý nghĩa, phát hiện tuyên bố tiềm năng, và kết nối phần được tô sáng với UI của tiện ích.

  • Trích xuất văn bản bài viết: Hàm extractArticleText() quét các vùng chứa bài viết phổ biến để lấy nội dung dễ đọc chính. Điều này giúp tiện ích tập trung vào văn bản có ý nghĩa thay vì menu điều hướng hay quảng cáo.
  • Logic phát hiện tuyên bố: Hàm extractClaims() chia văn bản thành câu, áp dụng bộ lọc độ dài và cấu trúc, và dùng các từ khóa regex để phát hiện câu nhiều khả năng là tuyên bố thực tế.
  • Làm nổi bật động: Hàm highlightTextOnPage() định vị tuyên bố trong DOM và bọc bằng <span> được tạo kiểu. Nó thêm hiệu ứng hover và click để người dùng liên kết trực quan giữa tuyên bố trên trang và kết quả xác minh trong bảng bên.
  • Xử lý thông điệp: Listener chrome.runtime.onMessage điều phối các hành động như trích xuất tuyên bố, thêm highlight và xóa khi cần. Điều này giữ cho trang và tiện ích đồng bộ, đảm bảo tương tác mượt.

Với content script đảm nhận trích xuất văn bản và tô sáng tuyên bố trên trang, bước tiếp theo là trình bày các tuyên bố và kết quả xác minh cho người dùng. Đây là lúc giao diện sidepanel.js phát huy tác dụng.

Bước 3.3: Giao diện bảng bên

Tệp sidepanel.js vận hành bảng bên tương tác, nơi người dùng xem các tuyên bố đã trích xuất và kết quả xác minh. Nó kết nối content script và phản hồi từ backend vào một giao diện cuộn gọn gàng trong tiện ích.

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

Cách bảng bên hoạt động trên trình duyệt của bạn:

  • Danh sách tuyên bố động: Khi người dùng nhấp “Check Claims”, script yêu cầu danh sách tuyên bố từ tab đang hoạt động, xóa highlight cũ, và tạo danh sách tuyên bố với trạng thái chờ.
  • Cập nhật xác minh theo thời gian thực: Mỗi tuyên bố được gửi tới background script bằng chrome.runtime.sendMessage với hành động checkClaim. Khi backend trả về kết quả, UI cập nhật ngay với nguồn, miền, và đoạn trích.
  • Liên kết tương tác: Script lắng nghe thông điệp highlightClaimInPanel, nhờ đó khi nhấp vào đoạn văn được tô sáng trên trang, bảng bên sẽ cuộn tới chi tiết tuyên bố tương ứng. Tạo vòng điều hướng mượt giữa highlight và dữ liệu xác minh.

Tiếp theo, chúng ta sẽ thêm pop-up truy cập nhanh để dễ khởi chạy và sử dụng tiện ích đã xây dựng ở các bước trước.

Bước 3.4: Giao diện pop-up truy cập nhanh

Tệp popup.js là giao diện pop-up gọn nhẹ xuất hiện khi người dùng nhấp biểu tượng tiện ích. Khác với bảng bên, pop-up này cung cấp cách kiểm tra nhanh, theo nhu cầu, mà không rời trang hiện tại hay mở bảng tương tác đầy đủ.

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

Cách popup.js vận hành công cụ kiểm tra theo yêu cầu trực tiếp từ thanh công cụ của tiện ích.

  • Quét trang và hiển thị tuyên bố: Khi người dùng nhấp “Check”, pop-up yêu cầu văn bản từ tab đang hoạt động qua content script, nhanh chóng trích xuất các tuyên bố tiềm năng và liệt kê trong cửa sổ pop-up.
  • Xác minh qua backend: Mỗi tuyên bố được gửi tới background script để gọi tuyến API /check-claim. Pop-up cập nhật theo thời gian thực với phán quyết, thanh độ tin cậy, giải thích, phản biện, và liên kết nguồn do máy chủ trả về.

Giờ đây, tiện ích đã sẵn sàng để thử nghiệm. 

Lưu ý: Bạn có thể tìm toàn bộ mã nguồn cho dự án này trong kho GitHub.

Bước 4: Chạy ứng dụng

Khởi động máy chủ backend bằng lệnh sau:

npm start

Khi máy chủ đã chạy, hãy tải tiện ích Chrome:

  • Mở Chrome và nhấp vào “Extensions” (ở bên phải thanh tìm kiếm).
  • Bật “Chế độ nhà phát triển” và nhấp “Load unpacked” (góc trên bên trái). 
  • Tiếp theo, tải thư mục extensions của chúng ta chứa tất cả các tệp và chờ xác nhận.

  • Sau khi Chrome xác nhận tiện ích đã được thêm, hãy nhấp biểu tượng Claim Checker trên bất kỳ trang web nào, chọn đoạn văn bản bạn muốn xác minh và chạy kiểm tra tuyên bố.

Hãy xem kết quả:

Kết luận

Giờ bạn đã có một công cụ kiểm tra tuyên bố nhanh và tiết kiệm, vận hành hoàn toàn bằng Search API của Perplexity (không cần gọi LLM). Bản demo này làm nổi bật các thế mạnh của API như kết quả tìm kiếm mới và đáng tin cậy, mức độ liên quan ở cấp độ đoạn trích, độ trễ thấp và giá thành dễ dự đoán.

Từ đây, bạn có thể mở rộng dự án bằng cách thêm bộ lọc nâng cao, lên lịch kiểm tra tự động, hoặc tích hợp như bước tiền lọc cho pipeline RAG. Để đi xa hơn, hãy khám phá Perplexity API tài liệu và các hướng dẫn khởi động nhanh để khai thác thêm khả năng tìm kiếm và lọc nâng cao.

Aashi Dutt

Tác giả

Aashi Dutt

Tôi là Chuyên gia Google Developers trong lĩnh vực ML (Gen AI), Chuyên gia Kaggle 3x và Đại sứ Women Techmakers với hơn 3 năm kinh nghiệm trong ngành công nghệ. Tôi đồng sáng lập một startup công nghệ y tế vào năm 2020 và hiện đang theo học thạc sĩ khoa học máy tính tại Georgia Tech, chuyên sâu về học máy.

Chủ đề

Liên quan