Skip to main content

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

  1. Event handling: Text selection, mouse/keyboard events
  2. UI rendering: Tooltip shell, tabs, content areas
  3. Message passing: Communication with background worker
  4. State management: Active tab, cache, conversation history
  5. Language detection: Auto-detect Arabic vs English text

Initialization

The content script initializes when the page loads:
content.js:50
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:
content.js:8
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:
content.js:74
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.

2. Button Display

Positioned above the selected text:
content.js:94
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:
content.js:146
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.

Tooltip Structure

The tooltip is built hierarchically:
content.js:206
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:
content.js:28
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:
content.js:294
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:
content.js:310
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

content.js:394
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:
content.js:171
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:
content.js:847
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

content.js:906
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\nWikipedia summary for "${cleanedTerm}":\n${summaryText}`;
  }

  if (pageContext) {
    systemContent += `\n\nContext 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.

Performance Optimizations

Debouncing

100ms delay prevents excessive button creation during text selection drag:
content.js:76
debounceTimer = setTimeout(() => {
  // ... selection logic
}, 100);

Lazy Tab Loading

Tabs only fetch data when activated, not on initial tooltip creation.

DOM Manipulation

Efficient element clearing:
content.js:42
function clearElement(el) {
  while (el.firstChild) {
    el.removeChild(el.firstChild);
  }
}

Intelligent Positioning

content.js:1453
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

Build docs developers (and LLMs) love