Content Script Architecture
The content script (content.js) is the user-facing layer of Knowledge Tooltip. It runs on every webpage and manages all UI interactions, from text selection to tooltip rendering.
File Location
source/content.js (1,500+ lines)
Core Responsibilities
Event handling : Text selection, mouse/keyboard events
UI rendering : Tooltip shell, tabs, content areas
Message passing : Communication with background worker
State management : Active tab, cache, conversation history
Language detection : Auto-detect Arabic vs English text
Initialization
The content script initializes when the page loads:
async function init () {
const result = await chrome . storage . sync . get ([ 'enabled' , 'language' ]);
isEnabled = result . enabled !== false ;
preferredLanguage = result . language || 'auto' ;
document . addEventListener ( 'mouseup' , handleTextSelection );
document . addEventListener ( 'mousedown' , handleMouseDown );
document . addEventListener ( 'keydown' , handleKeyDown );
chrome . runtime . onMessage . addListener (( message , sender , sendResponse ) => {
if ( message . action === 'toggleExtension' ) {
isEnabled = message . enabled ;
if ( ! isEnabled ) {
removeButton ();
removeTooltip ();
}
} else if ( message . action === 'changeLanguage' ) {
preferredLanguage = message . language ;
cache . clear ();
}
});
}
The script uses document_idle run timing (manifest.json:30), ensuring the page’s DOM is fully loaded before initialization.
State Management
The content script maintains several state variables:
let button = null ; // Reference to floating action button
let tooltip = null ; // Reference to tooltip modal
let backdrop = null ; // Reference to dimmed overlay
let cache = new Map (); // LRU cache: key -> data
let debounceTimer = null ; // Debounce text selection
let isEnabled = true ; // Extension on/off state
let preferredLanguage = 'auto' ; // 'auto', 'en', or 'ar'
let currentSearchTerm = '' ; // Currently displayed term
let currentLanguage = 'en' ; // Detected language for current term
let activeTab = 'summary' ; // Current active tab
let pageContext = '' ; // Surrounding text for AI context
let aiConversation = []; // Chat history for AI tab
let aiExchangeCount = 0 ; // Track AI usage limit
const CACHE_SIZE = 30 ; // Max cached entries
const MAX_AI_EXCHANGES = 5 ; // Limit AI questions per term
Event Flow
1. Text Selection Detection
Debounced to prevent excessive triggers:
function handleTextSelection ( e ) {
clearTimeout ( debounceTimer );
debounceTimer = setTimeout (() => {
if ( ! isEnabled ) {
removeButton ();
return ;
}
const selectedText = window . getSelection (). toString (). trim ();
if ( selectedText . length >= 2 ) {
showButton ( selectedText , e );
} else {
removeButton ();
}
}, 100 ); // 100ms debounce
}
Minimum 2 characters required prevents accidental triggers on single letters or punctuation.
Positioned above the selected text:
function showButton ( text , event ) {
removeButton ();
const selection = window . getSelection ();
if ( ! selection . rangeCount ) return ;
const range = selection . getRangeAt ( 0 );
const rect = range . getBoundingClientRect ();
button = document . createElement ( 'div' );
button . className = 'wiki-tooltip-button' ;
button . innerHTML = '<!-- Brain icon SVG -->' ;
const buttonTop = rect . top + window . scrollY - 38 ;
const buttonLeft = rect . left + window . scrollX + ( rect . width / 2 ) - 16 ;
button . style . top = ` ${ buttonTop } px` ;
button . style . left = ` ${ buttonLeft } px` ;
button . addEventListener ( 'click' , ( e ) => {
e . stopPropagation ();
currentSearchTerm = text ;
currentLanguage = detectLanguage ( text );
activeTab = 'summary' ;
pageContext = capturePageContext ();
aiConversation = [];
aiExchangeCount = 0 ;
fetchAndShowSummary ( text );
});
document . body . appendChild ( button );
setTimeout (() => button . classList . add ( 'visible' ), 10 );
}
3. Language Detection
Supports automatic Arabic detection:
function detectLanguage ( text ) {
if ( preferredLanguage !== 'auto' ) {
return preferredLanguage ;
}
return containsArabic ( text ) ? 'ar' : 'en' ;
}
function containsArabic ( text ) {
const arabicRegex = / [ \u0600 - \u06FF\u0750 - \u077F\u08A0 - \u08FF ] / ;
return arabicRegex . test ( text );
}
The Arabic Unicode ranges cover standard Arabic, Arabic Supplement, and Arabic Extended-A blocks.
The tooltip is built hierarchically:
function createTooltipShell ( language ) {
removeTooltip ();
createBackdrop ();
tooltip = document . createElement ( 'div' );
tooltip . className = 'wiki-tooltip' ;
if ( language === 'ar' ) {
tooltip . setAttribute ( 'dir' , 'rtl' );
tooltip . classList . add ( 'rtl' );
}
// Header with title and close button
tooltip . appendChild ( createHeader ( language ));
// Tab bar
tooltip . appendChild ( createTabBar ( language ));
// Content area (dynamically populated)
const content = document . createElement ( 'div' );
content . className = 'wiki-tooltip-content' ;
content . id = 'wiki-tooltip-content-area' ;
tooltip . appendChild ( content );
positionTooltip ( tooltip );
document . body . appendChild ( tooltip );
setTimeout (() => tooltip . classList . add ( 'visible' ), 10 );
return content ;
}
Tab System
5 tabs defined with bilingual labels:
const TABS = [
{ id: 'summary' , label: 'Summary' , labelAr: 'ملخص' , svg: '...' },
{ id: 'define' , label: 'Define' , labelAr: 'تعريف' , svg: '...' },
{ id: 'facts' , label: 'Facts' , labelAr: 'حقائق' , svg: '...' },
{ id: 'ai' , label: 'AI' , labelAr: 'ذكاء' , svg: '...' },
{ id: 'translate' , label: 'Translate' , labelAr: 'ترجمة' , beta: true , svg: '...' }
];
Tab switching logic:
function switchTab ( tabId ) {
if ( activeTab === tabId ) return ;
activeTab = tabId ;
// Update tab UI
const tabs = tooltip . querySelectorAll ( '.wiki-tooltip-tab' );
tabs . forEach ( t => {
t . classList . toggle ( 'active' , t . dataset . tab === tabId );
});
// Load tab content
loadTabContent ( tabId );
}
API Communication Pattern
All tabs follow a consistent fetch pattern:
function loadTabContent ( tabId ) {
const contentArea = tooltip . querySelector ( '#wiki-tooltip-content-area' );
if ( ! contentArea ) return ;
// AI and Translate tabs have special handling
if ( tabId === 'ai' ) {
loadAITab ( contentArea );
return ;
}
const cleanedTerm = cleanSearchTerm ( currentSearchTerm , currentLanguage );
// Check cache first
const cached = getFromCache ( cleanedTerm , tabId );
if ( cached ) {
renderTabContent ( tabId , cached , contentArea );
return ;
}
// Show loading state
clearElement ( contentArea );
contentArea . appendChild ( createLoadingIndicator ());
// Fetch data via background worker
switch ( tabId ) {
case 'summary' :
fetchSummaryData ( cleanedTerm );
break ;
case 'define' :
fetchDefineData ( cleanedTerm );
break ;
case 'facts' :
fetchFactsData ( cleanedTerm );
break ;
}
}
Example: Fetching Wikipedia Summary
async function fetchAndShowSummary ( searchTerm ) {
const cleanedTerm = cleanSearchTerm ( searchTerm , currentLanguage );
const cacheKey = getCacheKey ( cleanedTerm , 'summary' );
// Check cache
if ( cache . has ( cacheKey )) {
const contentArea = createTooltipShell ( currentLanguage );
renderSummaryContent ( cache . get ( cacheKey ), contentArea );
return ;
}
// Show tooltip shell with loading
const contentArea = createTooltipShell ( currentLanguage );
contentArea . appendChild ( createLoadingIndicator ());
try {
// Send message to background worker
let response = await chrome . runtime . sendMessage ({
action: 'fetchWikipedia' ,
term: cleanedTerm ,
language: currentLanguage
});
// Fallback: try search if direct fetch fails
if ( ! response . success && response . error ?. includes ( '404' )) {
const searchResult = await searchWikipedia ( cleanedTerm , currentLanguage );
if ( searchResult ) {
response = await chrome . runtime . sendMessage ({
action: 'fetchWikipedia' ,
term: searchResult ,
language: currentLanguage
});
}
}
if ( ! response . success ) {
const errorMsg = currentLanguage === 'ar'
? `لم يتم العثور على مقالة في ويكيبيديا عن " ${ searchTerm } ".`
: `No Wikipedia article found for " ${ searchTerm } ".` ;
showTabError ( contentArea , errorMsg );
return ;
}
const data = response . data ;
setCache ( cleanedTerm , 'summary' , data );
// Only render if still on summary tab
if ( activeTab === 'summary' ) {
clearElement ( contentArea );
renderSummaryContent ( data , contentArea );
}
} catch ( error ) {
console . error ( 'Wikipedia fetch error:' , error );
showTabError ( contentArea , 'Connection error' );
}
}
The cache check happens before UI creation to avoid unnecessary DOM operations for cached content.
Caching Strategy
LRU (Least Recently Used) cache with 30 entries:
function getCacheKey ( term , tab ) {
return ` ${ currentLanguage } : ${ term } : ${ tab } ` ;
}
function setCache ( term , tab , data ) {
if ( cache . size >= CACHE_SIZE ) {
const firstKey = cache . keys (). next (). value ;
cache . delete ( firstKey ); // Evict oldest entry
}
cache . set ( getCacheKey ( term , tab ), data );
}
Caching prevents redundant API calls when switching between tabs for the same term.
AI Features
Context Capture
Extracts surrounding page text for AI understanding:
function capturePageContext () {
const selection = window . getSelection ();
if ( ! selection . rangeCount ) return '' ;
const range = selection . getRangeAt ( 0 );
const container = range . commonAncestorContainer ;
const parentEl = container . nodeType === Node . TEXT_NODE
? container . parentElement
: container ;
// Get text from closest meaningful block
const block = parentEl . closest ( 'p, div, article, section, td, li' ) || parentEl ;
let text = ( block . textContent || '' ). trim ();
// Limit to ~500 chars centered around selection
if ( text . length > 500 ) {
const selText = selection . toString (). trim ();
const idx = text . indexOf ( selText );
if ( idx !== - 1 ) {
const start = Math . max ( 0 , idx - 200 );
const end = Math . min ( text . length , idx + selText . length + 200 );
text = text . substring ( start , end );
} else {
text = text . substring ( 0 , 500 );
}
}
return text ;
}
AI System Prompt Building
function buildSystemPrompt ( mode ) {
const lang = currentLanguage === 'ar' ? 'Arabic' : 'English' ;
const cleanedTerm = cleanSearchTerm ( currentSearchTerm , currentLanguage );
// Get Wikipedia summary if cached
const cachedSummary = getFromCache ( cleanedTerm , 'summary' );
const summaryText = cachedSummary ?. extract || '' ;
let systemContent = '' ;
if ( mode === 'explain' ) {
systemContent = `You are a helpful assistant that explains topics in simple, plain language. Respond in ${ lang } . Keep your explanation to 2-3 short sentences that anyone can understand. Avoid jargon.` ;
} else {
systemContent = `You are a helpful assistant that answers questions about topics. Respond in ${ lang } . Keep answers concise (2-4 sentences). Be accurate and helpful.` ;
}
if ( summaryText ) {
systemContent += ` \n\n Wikipedia summary for " ${ cleanedTerm } ": \n ${ summaryText } ` ;
}
if ( pageContext ) {
systemContent += ` \n\n Context from the page where the user found this term: \n ${ pageContext } ` ;
}
return systemContent ;
}
The system prompt includes Wikipedia summary and page context to provide grounded, accurate AI responses.
Debouncing
100ms delay prevents excessive button creation during text selection drag:
debounceTimer = setTimeout (() => {
// ... selection logic
}, 100 );
Lazy Tab Loading
Tabs only fetch data when activated, not on initial tooltip creation.
DOM Manipulation
Efficient element clearing:
function clearElement ( el ) {
while ( el . firstChild ) {
el . removeChild ( el . firstChild );
}
}
Intelligent Positioning
function positionTooltip ( tooltip ) {
if ( ! button ) return ;
const buttonRect = button . getBoundingClientRect ();
const tooltipWidth = 400 ;
const tooltipMargin = 10 ;
let top = buttonRect . bottom + window . scrollY + tooltipMargin ;
let left = buttonRect . left + window . scrollX - ( tooltipWidth / 2 ) + ( buttonRect . width / 2 );
// Prevent overflow right
if ( left + tooltipWidth > window . innerWidth ) {
left = window . innerWidth - tooltipWidth - tooltipMargin ;
}
// Prevent overflow left
if ( left < tooltipMargin ) {
left = tooltipMargin ;
}
// Flip above if too close to bottom
if ( top + 300 > window . innerHeight + window . scrollY ) {
top = buttonRect . top + window . scrollY - 310 ;
}
tooltip . style . top = ` ${ top } px` ;
tooltip . style . left = ` ${ left } px` ;
}
Next Steps
Background Worker Learn how API calls are handled
Message Passing Communication protocol details