/**
* LinkScout Combined Content Script
* Smart Analysis. Simple Answers.
* Combines highlighting, sidebar, and chunk analysis from both extensions
*/
// Configuration
const CONFIG = {
API_ENDPOINT: 'http://localhost:5000/api/v1/analyze-chunks',
MIN_TEXT_LENGTH: 100,
REQUEST_TIMEOUT: 180000, // 3 minutes
AUTO_SCAN_DELAY: 3000
};
// State
let isAnalyzing = false;
let analysisResults = null;
let sidebarOpen = false;
let highlightedElements = [];
// Initialize
console.log('🔍 LinkScout content script loaded');
// Listen for messages from popup/background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('📨 Message received:', request.action);
if (request.action === 'ping') {
sendResponse({ success: true, ready: true });
return true;
}
if (request.action === 'analyzeCurrentPage') {
analyzeAndDisplay().then(sendResponse);
return true;
}
if (request.action === 'highlightSuspicious') {
if (analysisResults) {
highlightSuspiciousContent(analysisResults);
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No analysis results' });
}
return true;
}
if (request.action === 'clearHighlights') {
clearAllHighlights();
sendResponse({ success: true });
return true;
}
if (request.action === 'showResults') {
if (analysisResults) {
openSidebar();
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No results' });
}
return true;
}
if (request.action === 'getResults') {
sendResponse({
success: analysisResults !== null,
result: analysisResults
});
return true;
}
if (request.action === 'scrollToParagraph') {
const chunkIndex = request.chunkIndex;
console.log('📍 Scrolling to paragraph:', chunkIndex);
scrollToChunk(chunkIndex);
sendResponse({ success: true });
return true;
}
});
// ============= MAIN ANALYSIS FUNCTION =============
async function analyzeAndDisplay() {
if (isAnalyzing) {
return { success: false, error: 'Analysis in progress' };
}
isAnalyzing = true;
try {
// Extract content
const extracted = window.ContentExtractorV2
? window.ContentExtractorV2.extractFullContent()
: extractContentFallback();
if (!extracted || extracted.paragraphs.length === 0) {
throw new Error('No content found on page');
}
console.log(`📊 Analyzing ${extracted.paragraphs.length} paragraphs...`);
// Show loading indicator
showLoadingNotification(extracted.paragraphs.length);
// Prepare payload
const payload = {
paragraphs: extracted.paragraphs.map(p => ({
index: p.index,
text: p.text,
type: p.type
})),
title: extracted.title,
url: window.location.href,
source: window.location.hostname,
html: document.documentElement.outerHTML // ✅ ADD HTML FOR IMAGE ANALYSIS
};
// Send to server
console.log('📡 Sending POST request to:', CONFIG.API_ENDPOINT);
console.log('📦 Payload size:', JSON.stringify(payload).length, 'bytes');
const response = await fetch(CONFIG.API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
console.log('📨 Response status:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
console.error('❌ Server error response:', errorText);
throw new Error(`Server error: ${response.status} - ${errorText.substring(0, 100)}`);
}
const result = await response.json();
// Store results
analysisResults = {
...result,
extractedContent: extracted
};
// Hide loading
hideLoadingNotification();
// Show completion notification
showCompletionNotification(result.overall);
// Automatically highlight suspicious content
if (result.chunks) {
highlightSuspiciousContent(result);
}
// Open sidebar with results
openSidebar();
console.log('✅ Analysis complete:', result.verdict);
return { success: true, result: analysisResults };
} catch (error) {
console.error('❌ Analysis error:', error);
console.error(' Error type:', error.name);
console.error(' Error message:', error.message);
console.error(' Error stack:', error.stack);
hideLoadingNotification();
showErrorNotification(error.message);
return { success: false, error: error.message };
} finally {
isAnalyzing = false;
}
}
// ============= CONTENT EXTRACTION =============
function extractContentFallback() {
const paragraphs = [];
const elements = document.querySelectorAll('p, h1, h2, h3, article');
elements.forEach((el, index) => {
const text = el.textContent.trim();
if (text.length > CONFIG.MIN_TEXT_LENGTH) {
paragraphs.push({
index,
text,
type: el.tagName.toLowerCase(),
element: el
});
}
});
const title = document.querySelector('title')?.textContent ||
document.querySelector('h1')?.textContent ||
'Untitled';
return { title, paragraphs };
}
// ============= HIGHLIGHTING =============
function highlightSuspiciousContent(result) {
clearAllHighlights();
console.log('🎨 [HIGHLIGHT] Starting highlighting process...');
console.log('🎨 [HIGHLIGHT] Result object:', result);
if (!result) {
console.error('❌ [HIGHLIGHT] No result object provided!');
return;
}
if (!result.chunks) {
console.error('❌ [HIGHLIGHT] No chunks property in result!');
console.log('🎨 [HIGHLIGHT] Available properties:', Object.keys(result));
return;
}
console.log(`🎨 [HIGHLIGHT] Total chunks received: ${result.chunks.length}`);
// ✅ STRICTER: Only highlight genuinely suspicious paragraphs (60+)
const suspiciousChunks = result.chunks.filter(c => c.suspicious_score >= 60);
console.log(`🎨 [HIGHLIGHT] Chunks with score >= 60: ${suspiciousChunks.length}`);
if (suspiciousChunks.length > 0) {
console.log('🎨 [HIGHLIGHT] Suspicious chunks:', suspiciousChunks.map(c => ({
index: c.index,
score: c.suspicious_score,
preview: c.text_preview?.substring(0, 50),
fullTextLength: c.text?.length || 0
})));
}
if (suspiciousChunks.length === 0) {
console.log('✅ No suspicious content to highlight');
return;
}
console.log(`🎨 Highlighting ${suspiciousChunks.length} suspicious items`);
suspiciousChunks.forEach(chunk => {
const text = chunk.text_preview || chunk.text;
if (!text) {
console.warn('⚠️ Chunk has no text:', chunk);
return;
}
console.log(`🔍 Looking for chunk ${chunk.index}: "${text.substring(0, 80)}..."`);
// Find elements containing this text
const elements = findElementsContainingText(text);
if (elements.length === 0) {
console.warn(`❌ Could not find element for chunk ${chunk.index}`);
}
elements.forEach(element => {
highlightElement(element, chunk.suspicious_score, chunk.index);
});
});
// Show notification
const notification = createNotification();
notification.style.background = '#3b82f6';
notification.innerHTML = `
🎨
${suspiciousChunks.length} Suspicious Paragraphs Highlighted
Click sidebar paragraphs to jump to them
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 4000);
}
function findElementsContainingText(searchText) {
// Use more of the text for better matching
const searchLower = searchText.toLowerCase();
const searchStart = searchLower.substring(0, 150); // First 150 chars
const searchEnd = searchLower.substring(Math.max(0, searchLower.length - 150)); // Last 150 chars
console.log(`🔍 [MATCH] Searching for text (${searchText.length} chars)`);
console.log(`🔍 [MATCH] Start: "${searchStart.substring(0, 50)}..."`);
// Strategy 1: Find exact paragraph match
const allParagraphs = Array.from(document.querySelectorAll('p, li, blockquote, td, div[class*="paragraph"]'));
let bestMatch = null;
let bestScore = -1;
for (const para of allParagraphs) {
// Skip LinkScout elements
if (para.closest('#linkscout-sidebar, [id*="linkscout"]')) continue;
const paraText = para.textContent.toLowerCase();
// Check if contains search text (start OR end)
const containsStart = paraText.includes(searchStart.substring(0, 80));
const containsEnd = searchEnd.length > 80 && paraText.includes(searchEnd.substring(searchEnd.length - 80));
if (containsStart || containsEnd) {
// Score: closer length = better match
const lengthRatio = Math.min(paraText.length, searchText.length) / Math.max(paraText.length, searchText.length);
let score = lengthRatio * 1000;
// Bonus for exact match
if (Math.abs(paraText.length - searchText.length) < 50) {
score += 500;
}
if (score > bestScore) {
bestScore = score;
bestMatch = para;
}
}
}
if (bestMatch) {
console.log(`✅ Found best match: ${bestMatch.tagName}, length: ${bestMatch.textContent.length}, score: ${bestScore.toFixed(1)}`);
return [bestMatch];
}
// Strategy 2: Try finding parent div (as fallback)
const allDivs = Array.from(document.querySelectorAll('div[class*="content"], div[class*="article"], div[class*="text"], div[class*="paragraph"]'));
for (const div of allDivs) {
if (div.closest('#linkscout-sidebar, [id*="linkscout"]')) continue;
const divText = div.textContent.toLowerCase();
if (divText.includes(searchLower.substring(0, 100)) && divText.length < searchText.length * 2) {
console.log(`✅ Found fallback div match: ${div.className}`);
return [div];
}
}
console.log('❌ No match found for text:', searchText.substring(0, 50));
return [];
}
function highlightElement(element, score, chunkIndex) {
if (!element || highlightedElements.includes(element)) return;
// Skip sidebar elements
if (element.id && element.id.includes('linkscout')) return;
if (element.closest('#linkscout-sidebar')) return;
// Determine color based on score (stricter thresholds)
let bgColor, borderColor;
if (score >= 70) {
bgColor = 'rgba(239, 68, 68, 0.15)';
borderColor = '#ef4444';
} else if (score >= 60) { // Changed from 40 to 60
bgColor = 'rgba(245, 158, 11, 0.15)';
borderColor = '#f59e0b';
} else {
bgColor = 'rgba(59, 130, 246, 0.15)';
borderColor = '#3b82f6';
}
// Store original style
const originalStyle = {
background: element.style.background,
borderLeft: element.style.borderLeft,
paddingLeft: element.style.paddingLeft
};
element.setAttribute('data-linkscout-original-style', JSON.stringify(originalStyle));
// Mark with chunk index if provided
if (chunkIndex !== undefined) {
element.setAttribute('data-linkscout-chunk', chunkIndex);
}
// Apply highlight
element.style.background = bgColor;
element.style.borderLeft = `4px solid ${borderColor}`;
element.style.paddingLeft = '12px';
element.style.transition = 'all 0.3s ease';
// Add to highlighted list
highlightedElements.push(element);
// Add tooltip
element.setAttribute('title', `LinkScout: ${score}% suspicious - Click sidebar paragraph to jump here`);
element.classList.add('linkscout-highlighted');
}
function clearAllHighlights() {
highlightedElements.forEach(element => {
try {
const originalStyle = element.getAttribute('data-linkscout-original-style');
if (originalStyle) {
const styles = JSON.parse(originalStyle);
element.style.background = styles.background;
element.style.borderLeft = styles.borderLeft;
element.style.paddingLeft = styles.paddingLeft;
element.removeAttribute('data-linkscout-original-style');
}
element.removeAttribute('title');
element.classList.remove('linkscout-highlighted');
} catch (e) {
console.error('Error clearing highlight:', e);
}
});
highlightedElements = [];
console.log('✅ All highlights cleared');
}
// ============= SIDEBAR =============
function openSidebar() {
let sidebar = document.getElementById('linkscout-sidebar');
if (sidebar) {
sidebar.style.display = 'block';
updateSidebarContent();
} else {
createSidebar();
}
sidebarOpen = true;
}
function createSidebar() {
const sidebar = document.createElement('div');
sidebar.id = 'linkscout-sidebar';
sidebar.style.cssText = `
position: fixed;
top: 0;
right: 0;
width: 400px;
height: 100vh;
background: white;
box-shadow: -4px 0 12px rgba(0,0,0,0.15);
z-index: 999999;
overflow-y: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
animation: slideIn 0.3s ease;
`;
sidebar.innerHTML = `
`;
document.body.appendChild(sidebar);
updateSidebarContent();
}
function updateSidebarContent() {
const content = document.getElementById('linkscout-sidebar-content');
if (!content || !analysisResults) return;
const result = analysisResults;
const overall = result.overall || {};
const percentage = overall.suspicious_score || 0;
const pretrained = result.pretrained_models || {};
const research = result.research || {};
const linguistic = result.linguistic_fingerprint || {};
const claims = result.claim_verification || {};
const source = result.source_credibility || {};
const propaganda = result.propaganda_analysis || {};
const entities = result.entity_verification || {};
const contradictions = result.contradiction_analysis || {};
const network = result.network_analysis || {};
// Determine colors
let bgColor, textColor, verdict, icon;
if (percentage > 70) {
bgColor = '#FF3B30';
textColor = '#FFFFFF';
verdict = '🚨 FAKE NEWS';
icon = '🚨';
} else if (percentage > 40) {
bgColor = '#FFCC00';
textColor = '#000000';
verdict = '⚠️ SUSPICIOUS';
icon = '⚠️';
} else {
bgColor = '#34C759';
textColor = '#FFFFFF';
verdict = '✅ LOOKS SAFE';
icon = '✅';
}
let html = `
${icon}
${verdict}
Suspicious: ${percentage}%
${overall.total_paragraphs || 0}
Analyzed
${(overall.fake_paragraphs || 0) + (overall.suspicious_paragraphs || 0)}
Flagged
${overall.credibility_score || (100 - percentage)}%
Credible
${result.combined_analysis ? `
🎯 OVERALL CREDIBILITY
Risk Score
${(result.combined_analysis.overall_score || 0).toFixed(0)}/100
${result.combined_analysis.verdict || 'UNKNOWN'}
0 - Highly Credible
100 - Not Credible
${overall.fake_paragraphs || 0}
High Risk
${overall.suspicious_paragraphs || 0}
Medium Risk
` : ''}
${pretrained.categories && Array.isArray(pretrained.categories) && pretrained.categories.length > 0 ? `
🏷️ ARTICLE CATEGORIES
${pretrained.categories.map(cat => `${cat}`).join(' ')}
` : ''}
${research.research_findings ? `
🤖 GROQ AI RESEARCH REPORT
${research.research_findings.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\n/g, '
')}
` : ''}
${research.detailed_analysis ? `
🔬 DETAILED ANALYSIS
${research.detailed_analysis.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\n/g, '
')}
` : ''}
${research.final_conclusion ? `
✅ FINAL CONCLUSION
${research.final_conclusion.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\n/g, '
')}
` : ''}
${pretrained.named_entities && Array.isArray(pretrained.named_entities) && pretrained.named_entities.length > 0 ? `
👥 KEY ENTITIES
${pretrained.named_entities.map(entity => `${entity}`).join(' ')}
` : ''}
${research.google_results && research.google_results.length > 0 ? `
🔗 GOOGLE SEARCH RESULTS
${research.google_results.slice(0, 5).map((r, i) => `
`).join('')}
` : ''}
${result.chunks && result.chunks.filter(c => c.suspicious_score >= 60).length > 0 ? `
🚨 Suspicious Paragraphs (${result.chunks.filter(c => c.suspicious_score >= 60).length})
${result.chunks.filter(c => c.suspicious_score >= 60).map(chunk => `
`).join('')}
` : `
✅ All Clear
No suspicious content detected! All paragraphs appear credible.
`}
Powered by LinkScout AI
${pretrained.fake_probability !== undefined ? '✓ 8 ML Models Active' : ''}
${research.research_findings ? '✓ Groq AI Active' : ''}
`;
content.innerHTML = html;
// Add close button functionality
setTimeout(() => {
const closeBtn = document.getElementById('linkscout-sidebar-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
const sidebar = document.getElementById('linkscout-sidebar');
if (sidebar) {
sidebar.style.display = 'none';
sidebarOpen = false;
// Clear highlights when closing sidebar
clearAllHighlights();
}
});
}
// Add click-to-scroll functionality for suspicious paragraphs
document.querySelectorAll('.linkscout-sidebar-chunk').forEach(chunkDiv => {
chunkDiv.addEventListener('click', () => {
const chunkIndex = parseInt(chunkDiv.getAttribute('data-chunk-index'));
scrollToChunk(chunkIndex);
});
});
}, 100);
}
// ============= SCROLL TO CHUNK =============
function scrollToChunk(chunkIndex) {
console.log(`📍 Scrolling to chunk ${chunkIndex}`);
if (!analysisResults || !analysisResults.chunks) {
console.error('No analysis results available');
return;
}
const chunk = analysisResults.chunks.find(c => c.index === chunkIndex);
if (!chunk) {
console.error(`Chunk ${chunkIndex} not found`);
return;
}
// Try to find the element by data attribute first (if already highlighted)
let element = document.querySelector(`[data-linkscout-chunk="${chunkIndex}"]`);
// If not found, search by text content
if (!element) {
const searchText = chunk.text || chunk.text_preview;
if (!searchText) {
console.error('No text to search for');
return;
}
// Search for paragraph containing this text - use more specific selectors
const allParagraphs = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, blockquote, li');
for (let candidate of allParagraphs) {
// Skip sidebar elements
if (candidate.id && candidate.id.includes('linkscout')) continue;
if (candidate.closest('#linkscout-sidebar')) continue;
const candidateText = candidate.textContent.trim();
// Skip very short elements
if (candidateText.length < 30) continue;
// Check if text matches (compare first 150 chars for better accuracy)
const searchSnippet = searchText.substring(0, 150).trim();
const candidateSnippet = candidateText.substring(0, 150).trim();
// Use more precise matching
if (candidateSnippet === searchSnippet || candidateText.includes(searchSnippet)) {
element = candidate;
console.log(`✅ Found matching element: <${element.tagName}> with text: "${candidateSnippet.substring(0, 50)}..."`);
break;
}
}
}
if (element) {
// Clear ALL previous highlights first
clearAllHighlights();
// Mark element with data attribute
element.setAttribute('data-linkscout-chunk', chunkIndex);
// Highlight ONLY this specific element
highlightElement(element, chunk.suspicious_score, chunkIndex);
// Scroll to element
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Flash animation - use a pulsing blue effect for the specific paragraph
const flashAnimation = () => {
let pulseCount = 0;
const pulseInterval = setInterval(() => {
if (pulseCount >= 3) {
clearInterval(pulseInterval);
return;
}
// Pulse effect
element.style.boxShadow = '0 0 25px rgba(59, 130, 246, 0.8)';
element.style.transform = 'scale(1.01)';
setTimeout(() => {
element.style.boxShadow = 'none';
element.style.transform = 'scale(1)';
}, 300);
pulseCount++;
}, 600);
};
setTimeout(flashAnimation, 300);
console.log(`✅ Scrolled to and highlighted chunk ${chunkIndex}`);
} else {
console.error(`❌ Could not find element for chunk ${chunkIndex}`);
console.log('Chunk text:', chunk.text_preview || chunk.text);
alert(`Could not locate paragraph ${chunkIndex + 1} on the page. The content may have changed.`);
}
}
// ============= NOTIFICATIONS =============
function showLoadingNotification(paragraphCount) {
const notification = createNotification();
notification.style.background = '#3b82f6';
notification.innerHTML = `
Analyzing ${paragraphCount} paragraphs...
`;
document.body.appendChild(notification);
}
function showCompletionNotification(overall) {
const notification = createNotification();
const verdict = overall.verdict || 'unknown';
const color = verdict === 'fake' ? '#ef4444' : verdict === 'suspicious' ? '#f59e0b' : '#10b981';
const icon = verdict === 'fake' ? '🚨' : verdict === 'suspicious' ? '⚠️' : '✅';
notification.style.background = color;
notification.innerHTML = `
${icon}
Analysis Complete
${verdict.toUpperCase()}
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 4000);
}
function showErrorNotification(message) {
const notification = createNotification();
notification.style.background = '#ef4444';
notification.innerHTML = `
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 4000);
}
function hideLoadingNotification() {
const notification = document.getElementById('linkscout-notification');
if (notification) notification.remove();
}
function createNotification() {
const existing = document.getElementById('linkscout-notification');
if (existing) existing.remove();
const notification = document.createElement('div');
notification.id = 'linkscout-notification';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #3b82f6;
color: white;
padding: 14px 18px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-family: -apple-system, sans-serif;
font-size: 13px;
z-index: 1000000;
animation: slideDown 0.3s ease;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes slideDown {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`;
notification.appendChild(style);
return notification;
}
console.log('✅ LinkScout content script ready - Smart Analysis. Simple Answers.');