From 1fb999084bc3db3df67607d5b2932e6287227262 Mon Sep 17 00:00:00 2001 From: rcholic Date: Mon, 29 Dec 2025 18:21:24 +0000 Subject: [PATCH] chore: sync extension files from sentience-chrome v2.0.7 --- src/extension/background.js | 274 +++- src/extension/content.js | 294 +++- src/extension/injected_api.js | 1413 ++++++++++++++--- src/extension/manifest.json | 14 +- src/extension/pkg/README.md | 165 +- src/extension/pkg/sentience_core.d.ts | 9 + src/extension/pkg/sentience_core.js | 16 + src/extension/pkg/sentience_core_bg.wasm | Bin 98167 -> 102522 bytes src/extension/pkg/sentience_core_bg.wasm.d.ts | 1 + src/extension/release.json | 115 ++ src/extension/test-content.js | 4 + 11 files changed, 2049 insertions(+), 256 deletions(-) create mode 100644 src/extension/release.json create mode 100644 src/extension/test-content.js diff --git a/src/extension/background.js b/src/extension/background.js index bb5ad6fa..811303f8 100644 --- a/src/extension/background.js +++ b/src/extension/background.js @@ -1,63 +1,233 @@ -// background.js - Service Worker for screenshot capture -// Chrome extensions can only capture screenshots from the background script -// Listen for screenshot requests from content script +// background.js - Service Worker with WASM (CSP-Immune!) +// This runs in an isolated environment, completely immune to page CSP policies + +// ✅ STATIC IMPORTS at top level - Required for Service Workers! +// Dynamic import() is FORBIDDEN in ServiceWorkerGlobalScope +import init, { analyze_page, analyze_page_with_options, prune_for_api } from './pkg/sentience_core.js'; + +console.log('[Sentience Background] Initializing...'); + +// Global WASM initialization state +let wasmReady = false; +let wasmInitPromise = null; + +/** + * Initialize WASM module - called once on service worker startup + * Uses static imports (not dynamic import()) which is required for Service Workers + */ +async function initWASM() { + if (wasmReady) return; + if (wasmInitPromise) return wasmInitPromise; + + wasmInitPromise = (async () => { + try { + console.log('[Sentience Background] Loading WASM module...'); + + // Define the js_click_element function that WASM expects + // In Service Workers, use 'globalThis' instead of 'window' + // In background context, we can't actually click, so we log a warning + globalThis.js_click_element = (_id) => { + console.warn('[Sentience Background] js_click_element called in background (ignored)'); + }; + + // Initialize WASM - this calls the init() function from the static import + // The init() function handles fetching and instantiating the .wasm file + await init(); + + wasmReady = true; + console.log('[Sentience Background] ✓ WASM ready!'); + console.log('[Sentience Background] Available functions: analyze_page, analyze_page_with_options, prune_for_api'); + } catch (error) { + console.error('[Sentience Background] WASM initialization failed:', error); + throw error; + } + })(); + + return wasmInitPromise; +} + +// Initialize WASM on service worker startup +initWASM().catch(err => { + console.error('[Sentience Background] Failed to initialize WASM:', err); +}); + +/** + * Message handler for all extension communication + * Includes global error handling to prevent extension crashes + */ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'captureScreenshot') { - handleScreenshotCapture(sender.tab.id, request.options) - .then(screenshot => { - sendResponse({ success: true, screenshot }); - }) - .catch(error => { - console.error('[Sentience] Screenshot capture failed:', error); - sendResponse({ - success: false, - error: error.message || 'Screenshot capture failed' - }); - }); + // Global error handler to prevent extension crashes + try { + // Handle screenshot requests (existing functionality) + if (request.action === 'captureScreenshot') { + handleScreenshotCapture(sender.tab.id, request.options) + .then(screenshot => { + sendResponse({ success: true, screenshot }); + }) + .catch(error => { + console.error('[Sentience Background] Screenshot capture failed:', error); + sendResponse({ + success: false, + error: error.message || 'Screenshot capture failed' + }); + }); + return true; // Async response + } + + // Handle WASM processing requests (NEW!) + if (request.action === 'processSnapshot') { + handleSnapshotProcessing(request.rawData, request.options) + .then(result => { + sendResponse({ success: true, result }); + }) + .catch(error => { + console.error('[Sentience Background] Snapshot processing failed:', error); + sendResponse({ + success: false, + error: error.message || 'Snapshot processing failed' + }); + }); + return true; // Async response + } - // Return true to indicate we'll send response asynchronously - return true; - } + // Unknown action + console.warn('[Sentience Background] Unknown action:', request.action); + sendResponse({ success: false, error: 'Unknown action' }); + return false; + } catch (error) { + // Catch any synchronous errors that might crash the extension + console.error('[Sentience Background] Fatal error in message handler:', error); + try { + sendResponse({ + success: false, + error: `Fatal error: ${error.message || 'Unknown error'}` + }); + } catch (e) { + // If sendResponse already called, ignore + } + return false; + } }); /** - * Capture screenshot of the active tab - * @param {number} tabId - Tab ID to capture - * @param {Object} options - Screenshot options - * @returns {Promise} Base64-encoded PNG data URL + * Handle screenshot capture (existing functionality) */ -async function handleScreenshotCapture(tabId, options = {}) { - try { - const { - format = 'png', // 'png' or 'jpeg' - quality = 90 // JPEG quality (0-100), ignored for PNG - } = options; - - // Capture visible tab as data URL - const dataUrl = await chrome.tabs.captureVisibleTab(null, { - format: format, - quality: quality - }); - - console.log(`[Sentience] Screenshot captured: ${format}, size: ${dataUrl.length} bytes`); - - return dataUrl; - } catch (error) { - console.error('[Sentience] Screenshot error:', error); - throw new Error(`Failed to capture screenshot: ${error.message}`); - } +async function handleScreenshotCapture(_tabId, options = {}) { + try { + const { + format = 'png', + quality = 90 + } = options; + + const dataUrl = await chrome.tabs.captureVisibleTab(null, { + format: format, + quality: quality + }); + + console.log(`[Sentience Background] Screenshot captured: ${format}, size: ${dataUrl.length} bytes`); + return dataUrl; + } catch (error) { + console.error('[Sentience Background] Screenshot error:', error); + throw new Error(`Failed to capture screenshot: ${error.message}`); + } } /** - * Optional: Add viewport-specific capture (requires additional setup) - * This would allow capturing specific regions, not just visible area + * Handle snapshot processing with WASM (NEW!) + * This is where the magic happens - completely CSP-immune! + * Includes safeguards to prevent crashes and hangs. + * + * @param {Array} rawData - Raw element data from injected_api.js + * @param {Object} options - Snapshot options (limit, filter, etc.) + * @returns {Promise} Processed snapshot result */ -async function captureRegion(tabId, region) { - // For region capture, you'd need to: - // 1. Capture full visible tab - // 2. Use Canvas API to crop to region - // 3. Return cropped image - - // Not implemented in this basic version - throw new Error('Region capture not yet implemented'); +async function handleSnapshotProcessing(rawData, options = {}) { + const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs + const startTime = performance.now(); + + try { + // Safety check: limit element count to prevent hangs + if (!Array.isArray(rawData)) { + throw new Error('rawData must be an array'); + } + + if (rawData.length > MAX_ELEMENTS) { + console.warn(`[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.`); + rawData = rawData.slice(0, MAX_ELEMENTS); + } + + // Ensure WASM is initialized + await initWASM(); + if (!wasmReady) { + throw new Error('WASM module not initialized'); + } + + console.log(`[Sentience Background] Processing ${rawData.length} elements with options:`, options); + + // Run WASM processing using the imported functions directly + // Wrap in try-catch with timeout protection + let analyzedElements; + try { + // Use a timeout wrapper to prevent infinite hangs + const wasmPromise = new Promise((resolve, reject) => { + try { + let result; + if (options.limit || options.filter) { + result = analyze_page_with_options(rawData, options); + } else { + result = analyze_page(rawData); + } + resolve(result); + } catch (e) { + reject(e); + } + }); + + // Add timeout protection (18 seconds - less than content.js timeout) + analyzedElements = await Promise.race([ + wasmPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000) + ) + ]); + } catch (e) { + const errorMsg = e.message || 'Unknown WASM error'; + console.error(`[Sentience Background] WASM analyze_page failed: ${errorMsg}`, e); + throw new Error(`WASM analyze_page failed: ${errorMsg}`); + } + + // Prune elements for API (prevents 413 errors on large sites) + let prunedRawData; + try { + prunedRawData = prune_for_api(rawData); + } catch (e) { + console.warn('[Sentience Background] prune_for_api failed, using original data:', e); + prunedRawData = rawData; + } + + const duration = performance.now() - startTime; + console.log(`[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)`); + + return { + elements: analyzedElements, + raw_elements: prunedRawData + }; + } catch (error) { + const duration = performance.now() - startTime; + console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error); + throw error; + } } + +console.log('[Sentience Background] Service worker ready'); + +// Global error handlers to prevent extension crashes +self.addEventListener('error', (event) => { + console.error('[Sentience Background] Global error caught:', event.error); + event.preventDefault(); // Prevent extension crash +}); + +self.addEventListener('unhandledrejection', (event) => { + console.error('[Sentience Background] Unhandled promise rejection:', event.reason); + event.preventDefault(); // Prevent extension crash +}); diff --git a/src/extension/content.js b/src/extension/content.js index de24fa5a..62ae4086 100644 --- a/src/extension/content.js +++ b/src/extension/content.js @@ -1,22 +1,298 @@ -// content.js - ISOLATED WORLD -console.log('[Sentience] Bridge loaded.'); +// content.js - ISOLATED WORLD (Bridge between Main World and Background) +console.log('[Sentience Bridge] Loaded.'); -// 1. Pass Extension ID to Main World (So WASM knows where to load from) +// Detect if we're in a child frame (for iframe support) +const isChildFrame = window !== window.top; +if (isChildFrame) { + console.log('[Sentience Bridge] Running in child frame:', window.location.href); +} + +// 1. Pass Extension ID to Main World (So API knows where to find resources) document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id; -// 2. Proxy for Screenshots (The only thing Isolated World needs to do) +// 2. Message Router - Handles all communication between page and background window.addEventListener('message', (event) => { // Security check: only accept messages from same window - if (event.source !== window || event.data.type !== 'SENTIENCE_SCREENSHOT_REQUEST') return; + if (event.source !== window) return; + + // Route different message types + switch (event.data.type) { + case 'SENTIENCE_SCREENSHOT_REQUEST': + handleScreenshotRequest(event.data); + break; + + case 'SENTIENCE_SNAPSHOT_REQUEST': + handleSnapshotRequest(event.data); + break; + + case 'SENTIENCE_SHOW_OVERLAY': + handleShowOverlay(event.data); + break; + + case 'SENTIENCE_CLEAR_OVERLAY': + handleClearOverlay(); + break; + + default: + // Ignore unknown message types + break; + } +}); +/** + * Handle screenshot requests (existing functionality) + */ +function handleScreenshotRequest(data) { chrome.runtime.sendMessage( - { action: 'captureScreenshot', options: event.data.options }, + { action: 'captureScreenshot', options: data.options }, (response) => { window.postMessage({ type: 'SENTIENCE_SCREENSHOT_RESULT', - requestId: event.data.requestId, - screenshot: response?.success ? response.screenshot : null + requestId: data.requestId, + screenshot: response?.success ? response.screenshot : null, + error: response?.error }, '*'); } ); -}); \ No newline at end of file +} + +/** + * Handle snapshot processing requests (NEW!) + * Sends raw DOM data to background worker for WASM processing + * Includes timeout protection to prevent extension crashes + */ +function handleSnapshotRequest(data) { + const startTime = performance.now(); + const TIMEOUT_MS = 20000; // 20 seconds (longer than injected_api timeout) + let responded = false; + + // Timeout protection: if background doesn't respond, send error + const timeoutId = setTimeout(() => { + if (!responded) { + responded = true; + const duration = performance.now() - startTime; + console.error(`[Sentience Bridge] ⚠️ WASM processing timeout after ${duration.toFixed(1)}ms`); + window.postMessage({ + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: 'WASM processing timeout - background script may be unresponsive', + duration: duration + }, '*'); + } + }, TIMEOUT_MS); + + try { + chrome.runtime.sendMessage( + { + action: 'processSnapshot', + rawData: data.rawData, + options: data.options + }, + (response) => { + if (responded) return; // Already responded via timeout + responded = true; + clearTimeout(timeoutId); + + const duration = performance.now() - startTime; + + // Handle Chrome extension errors (e.g., background script crashed) + if (chrome.runtime.lastError) { + console.error('[Sentience Bridge] Chrome runtime error:', chrome.runtime.lastError.message); + window.postMessage({ + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: `Chrome runtime error: ${chrome.runtime.lastError.message}`, + duration: duration + }, '*'); + return; + } + + if (response?.success) { + console.log(`[Sentience Bridge] ✓ WASM processing complete in ${duration.toFixed(1)}ms`); + window.postMessage({ + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + elements: response.result.elements, + raw_elements: response.result.raw_elements, + duration: duration + }, '*'); + } else { + console.error('[Sentience Bridge] WASM processing failed:', response?.error); + window.postMessage({ + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: response?.error || 'Processing failed', + duration: duration + }, '*'); + } + } + ); + } catch (error) { + if (!responded) { + responded = true; + clearTimeout(timeoutId); + const duration = performance.now() - startTime; + console.error('[Sentience Bridge] Exception sending message:', error); + window.postMessage({ + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: `Failed to send message: ${error.message}`, + duration: duration + }, '*'); + } + } +} + +// ============================================================================ +// Visual Overlay - Shadow DOM Implementation +// ============================================================================ + +const OVERLAY_HOST_ID = 'sentience-overlay-host'; +let overlayTimeout = null; + +/** + * Show visual overlay highlighting elements using Shadow DOM + * @param {Object} data - Message data with elements and targetElementId + */ +function handleShowOverlay(data) { + const { elements, targetElementId } = data; + + if (!elements || !Array.isArray(elements)) { + console.warn('[Sentience Bridge] showOverlay: elements must be an array'); + return; + } + + removeOverlay(); + + // Create host with Shadow DOM for CSS isolation + const host = document.createElement('div'); + host.id = OVERLAY_HOST_ID; + host.style.cssText = ` + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + pointer-events: none !important; + z-index: 2147483647 !important; + margin: 0 !important; + padding: 0 !important; + `; + document.body.appendChild(host); + + // Attach shadow root (closed mode for security and CSS isolation) + const shadow = host.attachShadow({ mode: 'closed' }); + + // Calculate max importance for scaling + const maxImportance = Math.max(...elements.map(e => e.importance || 0), 1); + + elements.forEach((element) => { + const bbox = element.bbox; + if (!bbox) return; + + const isTarget = element.id === targetElementId; + const isPrimary = element.visual_cues?.is_primary || false; + const importance = element.importance || 0; + + // Color: Red (target), Blue (primary), Green (regular) + let color; + if (isTarget) color = '#FF0000'; + else if (isPrimary) color = '#0066FF'; + else color = '#00FF00'; + + // Scale opacity and border width based on importance + const importanceRatio = maxImportance > 0 ? importance / maxImportance : 0.5; + const borderOpacity = isTarget ? 1.0 : (isPrimary ? 0.9 : Math.max(0.4, 0.5 + importanceRatio * 0.5)); + const fillOpacity = borderOpacity * 0.2; + const borderWidth = isTarget ? 2 : (isPrimary ? 1.5 : Math.max(0.5, Math.round(importanceRatio * 2))); + + // Convert fill opacity to hex for background-color + const hexOpacity = Math.round(fillOpacity * 255).toString(16).padStart(2, '0'); + + // Create box with semi-transparent fill + const box = document.createElement('div'); + box.style.cssText = ` + position: absolute; + left: ${bbox.x}px; + top: ${bbox.y}px; + width: ${bbox.width}px; + height: ${bbox.height}px; + border: ${borderWidth}px solid ${color}; + background-color: ${color}${hexOpacity}; + box-sizing: border-box; + opacity: ${borderOpacity}; + pointer-events: none; + `; + + // Add badge showing importance score + if (importance > 0 || isPrimary) { + const badge = document.createElement('span'); + badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`; + badge.style.cssText = ` + position: absolute; + top: -18px; + left: 0; + background: ${color}; + color: white; + font-size: 11px; + font-weight: bold; + padding: 2px 6px; + font-family: Arial, sans-serif; + border-radius: 3px; + opacity: 0.95; + white-space: nowrap; + pointer-events: none; + `; + box.appendChild(badge); + } + + // Add target emoji for target element + if (isTarget) { + const targetIndicator = document.createElement('span'); + targetIndicator.textContent = '🎯'; + targetIndicator.style.cssText = ` + position: absolute; + top: -18px; + right: 0; + font-size: 16px; + pointer-events: none; + `; + box.appendChild(targetIndicator); + } + + shadow.appendChild(box); + }); + + console.log(`[Sentience Bridge] Overlay shown for ${elements.length} elements`); + + // Auto-remove after 5 seconds + overlayTimeout = setTimeout(() => { + removeOverlay(); + console.log('[Sentience Bridge] Overlay auto-cleared after 5 seconds'); + }, 5000); +} + +/** + * Clear overlay manually + */ +function handleClearOverlay() { + removeOverlay(); + console.log('[Sentience Bridge] Overlay cleared manually'); +} + +/** + * Remove overlay from DOM + */ +function removeOverlay() { + const existing = document.getElementById(OVERLAY_HOST_ID); + if (existing) { + existing.remove(); + } + + if (overlayTimeout) { + clearTimeout(overlayTimeout); + overlayTimeout = null; + } +} + +// console.log('[Sentience Bridge] Ready - Extension ID:', chrome.runtime.id); diff --git a/src/extension/injected_api.js b/src/extension/injected_api.js index 8f6eb156..45c43370 100644 --- a/src/extension/injected_api.js +++ b/src/extension/injected_api.js @@ -1,33 +1,56 @@ -// injected_api.js - MAIN WORLD +// injected_api.js - MAIN WORLD (NO WASM! CSP-Resistant!) +// This script ONLY collects raw DOM data and sends it to background for processing (async () => { - // 1. Get Extension ID (Wait for content.js to set it) + // console.log('[SentienceAPI] Initializing (CSP-Resistant Mode)...'); + + // Wait for Extension ID from content.js const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; let extId = getExtensionId(); - - // Safety poller for async loading race conditions + if (!extId) { await new Promise(resolve => { const check = setInterval(() => { extId = getExtensionId(); if (extId) { clearInterval(check); resolve(); } }, 50); + setTimeout(() => resolve(), 5000); // Max 5s wait }); } - const EXT_URL = `chrome-extension://${extId}/`; - console.log('[SentienceAPI.com] Initializing from:', EXT_URL); + if (!extId) { + console.error('[SentienceAPI] Failed to get extension ID'); + return; + } + + // console.log('[SentienceAPI] Extension ID:', extId); + // Registry for click actions (still needed for click() function) window.sentience_registry = []; - let wasmModule = null; - // --- HELPER: Deep Walker --- + // --- HELPER: Deep Walker with Native Filter --- function getAllElements(root = document) { const elements = []; - const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + const filter = { + acceptNode: function(node) { + // Skip metadata and script/style tags + if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD'].includes(node.tagName)) { + return NodeFilter.FILTER_REJECT; + } + // Skip deep SVG children + if (node.parentNode && node.parentNode.tagName === 'SVG' && node.tagName !== 'SVG') { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + } + }; + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); while(walker.nextNode()) { const node = walker.currentNode; - elements.push(node); - if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot)); + if (node.isConnected) { + elements.push(node); + if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot)); + } } return elements; } @@ -40,7 +63,163 @@ return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100); } - // --- HELPER: Viewport Check (NEW) --- + // --- HELPER: Safe Class Name Extractor (Handles SVGAnimatedString) --- + function getClassName(el) { + if (!el || !el.className) return ''; + + // Handle string (HTML elements) + if (typeof el.className === 'string') return el.className; + + // Handle SVGAnimatedString (SVG elements) + if (typeof el.className === 'object') { + if ('baseVal' in el.className && typeof el.className.baseVal === 'string') { + return el.className.baseVal; + } + if ('animVal' in el.className && typeof el.className.animVal === 'string') { + return el.className.animVal; + } + // Fallback: convert to string + try { + return String(el.className); + } catch (e) { + return ''; + } + } + + return ''; + } + + // --- HELPER: Paranoid String Converter (Handles SVGAnimatedString) --- + function toSafeString(value) { + if (value === null || value === undefined) return null; + + // 1. If it's already a primitive string, return it + if (typeof value === 'string') return value; + + // 2. Handle SVG objects (SVGAnimatedString, SVGAnimatedNumber, etc.) + if (typeof value === 'object') { + // Try extracting baseVal (standard SVG property) + if ('baseVal' in value && typeof value.baseVal === 'string') { + return value.baseVal; + } + // Try animVal as fallback + if ('animVal' in value && typeof value.animVal === 'string') { + return value.animVal; + } + // Fallback: Force to string (prevents WASM crash even if data is less useful) + // This prevents the "Invalid Type" crash, even if the data is "[object SVGAnimatedString]" + try { + return String(value); + } catch (e) { + return null; + } + } + + // 3. Last resort cast for primitives + try { + return String(value); + } catch (e) { + return null; + } + } + + // --- HELPER: Get SVG Fill/Stroke Color --- + // For SVG elements, get the fill or stroke color (SVGs use fill/stroke, not backgroundColor) + function getSVGColor(el) { + if (!el || el.tagName !== 'SVG') return null; + + const style = window.getComputedStyle(el); + + // Try fill first (most common for SVG icons) + const fill = style.fill; + if (fill && fill !== 'none' && fill !== 'transparent' && fill !== 'rgba(0, 0, 0, 0)') { + // Convert fill to rgb() format if needed + const rgbaMatch = fill.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (rgbaMatch) { + const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0; + if (alpha >= 0.9) { + return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; + } + } else if (fill.startsWith('rgb(')) { + return fill; + } + } + + // Fallback to stroke if fill is not available + const stroke = style.stroke; + if (stroke && stroke !== 'none' && stroke !== 'transparent' && stroke !== 'rgba(0, 0, 0, 0)') { + const rgbaMatch = stroke.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (rgbaMatch) { + const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0; + if (alpha >= 0.9) { + return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; + } + } else if (stroke.startsWith('rgb(')) { + return stroke; + } + } + + return null; + } + + // --- HELPER: Get Effective Background Color --- + // Traverses up the DOM tree to find the nearest non-transparent background color + // For SVGs, also checks fill/stroke properties + // This handles rgba(0,0,0,0) and transparent values that browsers commonly return + function getEffectiveBackgroundColor(el) { + if (!el) return null; + + // For SVG elements, use fill/stroke instead of backgroundColor + if (el.tagName === 'SVG') { + const svgColor = getSVGColor(el); + if (svgColor) return svgColor; + } + + let current = el; + const maxDepth = 10; // Prevent infinite loops + let depth = 0; + + while (current && depth < maxDepth) { + const style = window.getComputedStyle(current); + + // For SVG elements in the tree, also check fill/stroke + if (current.tagName === 'SVG') { + const svgColor = getSVGColor(current); + if (svgColor) return svgColor; + } + + const bgColor = style.backgroundColor; + + if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') { + // Check if it's rgba with alpha < 1 (semi-transparent) + const rgbaMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (rgbaMatch) { + const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0; + // If alpha is high enough (>= 0.9), consider it opaque enough + if (alpha >= 0.9) { + // Convert to rgb() format for Gateway compatibility + return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; + } + // If semi-transparent, continue up the tree + } else if (bgColor.startsWith('rgb(')) { + // Already in rgb() format, use it + return bgColor; + } else { + // Named color or other format, return as-is + return bgColor; + } + } + + // Move up the DOM tree + current = current.parentElement; + depth++; + } + + // Fallback: return null if nothing found + return null; + } + + // --- HELPER: Viewport Check --- function isInViewport(rect) { return ( rect.top < window.innerHeight && rect.bottom > 0 && @@ -48,19 +227,30 @@ ); } - // --- HELPER: Occlusion Check (NEW) --- - function isOccluded(el, rect) { - // Fast center-point check + // --- HELPER: Occlusion Check (Optimized to avoid layout thrashing) --- + // Only checks occlusion for elements likely to be occluded (high z-index, positioned) + // This avoids forced reflow for most elements, dramatically improving performance + function isOccluded(el, rect, style) { + // Fast path: Skip occlusion check for most elements + // Only check for elements that are likely to be occluded (overlays, modals, tooltips) + const zIndex = parseInt(style.zIndex, 10); + const position = style.position; + + // Skip occlusion check for normal flow elements (vast majority) + // Only check for positioned elements or high z-index (likely overlays) + if (position === 'static' && (isNaN(zIndex) || zIndex <= 10)) { + return false; // Assume not occluded for performance + } + + // For positioned/high z-index elements, do the expensive check const cx = rect.x + rect.width / 2; const cy = rect.y + rect.height / 2; - - // If point is off-screen, elementFromPoint returns null, assume NOT occluded for safety + if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return false; const topEl = document.elementFromPoint(cx, cy); if (!topEl) return false; - - // It's visible if the top element is us, or contains us, or we contain it + return !(el === topEl || el.contains(topEl) || topEl.contains(el)); } @@ -76,45 +266,91 @@ }; window.addEventListener('message', listener); window.postMessage({ type: 'SENTIENCE_SCREENSHOT_REQUEST', requestId, options }, '*'); + setTimeout(() => { + window.removeEventListener('message', listener); + resolve(null); + }, 10000); // 10s timeout + }); + } + + // --- HELPER: Snapshot Processing Bridge (NEW!) --- + function processSnapshotInBackground(rawData, options) { + return new Promise((resolve, reject) => { + const requestId = Math.random().toString(36).substring(7); + const TIMEOUT_MS = 25000; // 25 seconds (longer than content.js timeout) + let resolved = false; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + window.removeEventListener('message', listener); + reject(new Error('WASM processing timeout - extension may be unresponsive. Try reloading the extension.')); + } + }, TIMEOUT_MS); + + const listener = (e) => { + if (e.data.type === 'SENTIENCE_SNAPSHOT_RESULT' && e.data.requestId === requestId) { + if (resolved) return; // Already handled + resolved = true; + clearTimeout(timeout); + window.removeEventListener('message', listener); + + if (e.data.error) { + reject(new Error(e.data.error)); + } else { + resolve({ + elements: e.data.elements, + raw_elements: e.data.raw_elements, + duration: e.data.duration + }); + } + } + }; + + window.addEventListener('message', listener); + + try { + window.postMessage({ + type: 'SENTIENCE_SNAPSHOT_REQUEST', + requestId, + rawData, + options + }, '*'); + } catch (error) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + window.removeEventListener('message', listener); + reject(new Error(`Failed to send snapshot request: ${error.message}`)); + } + } }); } - // --- HELPER: Get Raw HTML for Turndown/External Processing --- - // Returns cleaned HTML that can be processed by Turndown or other Node.js libraries + // --- HELPER: Raw HTML Extractor (unchanged) --- function getRawHTML(root) { const sourceRoot = root || document.body; const clone = sourceRoot.cloneNode(true); - - // Remove unwanted elements by tag name (simple and reliable) + const unwantedTags = ['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg']; unwantedTags.forEach(tag => { const elements = clone.querySelectorAll(tag); elements.forEach(el => { - if (el.parentNode) { - el.parentNode.removeChild(el); - } + if (el.parentNode) el.parentNode.removeChild(el); }); }); - // Remove invisible elements from original DOM and find matching ones in clone - // We'll use a simple approach: mark elements in original, then remove from clone + // Remove invisible elements const invisibleSelectors = []; - const walker = document.createTreeWalker( - sourceRoot, - NodeFilter.SHOW_ELEMENT, - null, - false - ); - + const walker = document.createTreeWalker(sourceRoot, NodeFilter.SHOW_ELEMENT, null, false); let node; while (node = walker.nextNode()) { const tag = node.tagName.toLowerCase(); if (tag === 'head' || tag === 'title') continue; - + const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden' || (node.offsetWidth === 0 && node.offsetHeight === 0)) { - // Build a selector for this element let selector = tag; if (node.id) { selector = `#${node.id}`; @@ -128,30 +364,25 @@ } } - // Remove invisible elements from clone (if we can find them) invisibleSelectors.forEach(selector => { try { const elements = clone.querySelectorAll(selector); elements.forEach(el => { - if (el.parentNode) { - el.parentNode.removeChild(el); - } + if (el.parentNode) el.parentNode.removeChild(el); }); } catch (e) { // Invalid selector, skip } }); - // Resolve relative URLs in links and images + // Resolve relative URLs const links = clone.querySelectorAll('a[href]'); links.forEach(link => { const href = link.getAttribute('href'); if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('#')) { try { link.setAttribute('href', new URL(href, document.baseURI).href); - } catch (e) { - // Keep original href if URL parsing fails - } + } catch (e) {} } }); @@ -161,32 +392,24 @@ if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:')) { try { img.setAttribute('src', new URL(src, document.baseURI).href); - } catch (e) { - // Keep original src if URL parsing fails - } + } catch (e) {} } }); return clone.innerHTML; } - // --- HELPER: Simple Markdown Converter (Lightweight) --- - // Uses getRawHTML() and then converts to markdown for consistency + // --- HELPER: Markdown Converter (unchanged) --- function convertToMarkdown(root) { - // Get cleaned HTML first const rawHTML = getRawHTML(root); - - // Create a temporary container to parse the HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = rawHTML; - + let markdown = ''; - let insideLink = false; // Track if we're inside an tag + let insideLink = false; function walk(node) { if (node.nodeType === Node.TEXT_NODE) { - // Keep minimal whitespace to prevent words merging - // Strip newlines inside text nodes to prevent broken links const text = node.textContent.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' '); if (text.trim()) markdown += text; return; @@ -201,13 +424,12 @@ if (tag === 'h2') markdown += '\n## '; if (tag === 'h3') markdown += '\n### '; if (tag === 'li') markdown += '\n- '; - // IMPORTANT: Don't add newlines for block elements when inside a link if (!insideLink && (tag === 'p' || tag === 'div' || tag === 'br')) markdown += '\n'; if (tag === 'strong' || tag === 'b') markdown += '**'; if (tag === 'em' || tag === 'i') markdown += '_'; if (tag === 'a') { markdown += '['; - insideLink = true; // Mark that we're entering a link + insideLink = true; } // Children @@ -219,25 +441,21 @@ // Suffix if (tag === 'a') { - // Get absolute URL from href attribute (already resolved in getRawHTML) const href = node.getAttribute('href'); if (href) markdown += `](${href})`; else markdown += ']'; - insideLink = false; // Mark that we're exiting the link + insideLink = false; } if (tag === 'strong' || tag === 'b') markdown += '**'; if (tag === 'em' || tag === 'i') markdown += '_'; - // IMPORTANT: Don't add newlines for block elements when inside a link (suffix section too) if (!insideLink && (tag === 'h1' || tag === 'h2' || tag === 'h3' || tag === 'p' || tag === 'div')) markdown += '\n'; } walk(tempDiv); - - // Cleanup: remove excessive newlines return markdown.replace(/\n{3,}/g, '\n\n').trim(); } - // --- HELPER: Raw Text Extractor --- + // --- HELPER: Text Extractor (unchanged) --- function convertToText(root) { let text = ''; function walk(node) { @@ -247,22 +465,20 @@ } if (node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toLowerCase(); - // Skip nav/footer/header/script/style/noscript/iframe/svg if (['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg'].includes(tag)) return; const style = window.getComputedStyle(node); if (style.display === 'none' || style.visibility === 'hidden') return; - - // Block level elements get a newline + const isBlock = style.display === 'block' || style.display === 'flex' || node.tagName === 'P' || node.tagName === 'DIV'; if (isBlock) text += ' '; - + if (node.shadowRoot) { Array.from(node.shadowRoot.childNodes).forEach(walk); } else { node.childNodes.forEach(walk); } - + if (isBlock) text += '\n'; } } @@ -270,155 +486,597 @@ return text.replace(/\n{3,}/g, '\n\n').trim(); } - // Load WASM - try { - const wasmUrl = EXT_URL + 'pkg/sentience_core.js'; - const module = await import(wasmUrl); - const imports = { - env: { - js_click_element: (id) => { - const el = window.sentience_registry[id]; - if (el) { el.click(); el.focus(); } + // --- HELPER: Clean null/undefined fields --- + function cleanElement(obj) { + if (Array.isArray(obj)) { + return obj.map(cleanElement); + } + if (obj !== null && typeof obj === 'object') { + const cleaned = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== null && value !== undefined) { + if (typeof value === 'object') { + const deepClean = cleanElement(value); + if (Object.keys(deepClean).length > 0) { + cleaned[key] = deepClean; + } + } else { + cleaned[key] = value; + } } } - }; - await module.default(undefined, imports); - wasmModule = module; - - // Verify functions are available - if (!wasmModule.analyze_page) { - console.error('[SentienceAPI.com] WASM functions not available'); - } else { - console.log('[SentienceAPI.com] ✓ API Ready!'); - console.log('[SentienceAPI.com] Available functions:', Object.keys(wasmModule).filter(k => k.startsWith('analyze'))); + return cleaned; } - } catch (e) { - console.error('[SentienceAPI.com] WASM Load Failed:', e); + return obj; } - // REMOVED: Headless detection - no longer needed (license system removed) - - // --- GLOBAL API --- - window.sentience = { - // 1. Geometry snapshot (existing) - snapshot: async (options = {}) => { - if (!wasmModule) return { error: "WASM not ready" }; + // --- HELPER: Extract Raw Element Data (for Golden Set) --- + function extractRawElementData(el) { + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + + return { + tag: el.tagName, + rect: { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: Math.round(rect.height) + }, + styles: { + cursor: style.cursor || null, + backgroundColor: style.backgroundColor || null, + color: style.color || null, + fontWeight: style.fontWeight || null, + fontSize: style.fontSize || null, + display: style.display || null, + position: style.position || null, + zIndex: style.zIndex || null, + opacity: style.opacity || null, + visibility: style.visibility || null + }, + attributes: { + role: el.getAttribute('role') || null, + type: el.getAttribute('type') || null, + ariaLabel: el.getAttribute('aria-label') || null, + id: el.id || null, + className: el.className || null + } + }; + } - const rawData = []; - // Remove textMap as we include text in rawData - window.sentience_registry = []; + // --- HELPER: Generate Unique CSS Selector (for Golden Set) --- + function getUniqueSelector(el) { + if (!el || !el.tagName) return ''; + + // If element has a unique ID, use it + if (el.id) { + return `#${el.id}`; + } + + // Try data attributes or aria-label for uniqueness + for (const attr of el.attributes) { + if (attr.name.startsWith('data-') || attr.name === 'aria-label') { + const value = attr.value ? attr.value.replace(/"/g, '\\"') : ''; + return `${el.tagName.toLowerCase()}[${attr.name}="${value}"]`; + } + } + + // Build path with classes and nth-child for uniqueness + const path = []; + let current = el; + + while (current && current !== document.body && current !== document.documentElement) { + let selector = current.tagName.toLowerCase(); - const nodes = getAllElements(); + // If current element has ID, use it and stop + if (current.id) { + selector = `#${current.id}`; + path.unshift(selector); + break; + } - nodes.forEach((el, idx) => { - if (!el.getBoundingClientRect) return; - const rect = el.getBoundingClientRect(); - if (rect.width < 5 || rect.height < 5) return; + // Add class if available + if (current.className && typeof current.className === 'string') { + const classes = current.className.trim().split(/\s+/).filter(c => c); + if (classes.length > 0) { + // Use first class for simplicity + selector += `.${classes[0]}`; + } + } + + // Add nth-of-type if needed for uniqueness + if (current.parentElement) { + const siblings = Array.from(current.parentElement.children); + const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName); + const index = sameTagSiblings.indexOf(current); + if (index > 0 || sameTagSiblings.length > 1) { + selector += `:nth-of-type(${index + 1})`; + } + } + + path.unshift(selector); + current = current.parentElement; + } + + return path.join(' > ') || el.tagName.toLowerCase(); + } + + // --- HELPER: Wait for DOM Stability (SPA Hydration) --- + // Waits for the DOM to stabilize before taking a snapshot + // Useful for React/Vue apps that render empty skeletons before hydration + async function waitForStability(options = {}) { + const { + minNodeCount = 500, + quietPeriod = 200, // milliseconds + maxWait = 5000 // maximum wait time + } = options; - window.sentience_registry[idx] = el; + const startTime = Date.now(); + + return new Promise((resolve) => { + // Check if DOM already has enough nodes + const nodeCount = document.querySelectorAll('*').length; + if (nodeCount >= minNodeCount) { + // DOM seems ready, but wait for quiet period to ensure stability + let lastChange = Date.now(); + const observer = new MutationObserver(() => { + lastChange = Date.now(); + }); - // Calculate properties for Fat Payload - const textVal = getText(el); - const inView = isInViewport(rect); - // Only check occlusion if visible (Optimization) - const occluded = inView ? isOccluded(el, rect) : false; - - const style = window.getComputedStyle(el); - rawData.push({ - id: idx, - tag: el.tagName.toLowerCase(), - rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, - styles: { - display: style.display, - visibility: style.visibility, - opacity: style.opacity, - z_index: style.zIndex || "0", - bg_color: style.backgroundColor, - color: style.color, - cursor: style.cursor, - font_weight: style.fontWeight, - font_size: style.fontSize - }, - attributes: { - role: el.getAttribute('role'), - type_: el.getAttribute('type'), - aria_label: el.getAttribute('aria-label'), - href: el.href, - class: el.className - }, - // Pass to WASM - text: textVal || null, - in_viewport: inView, - is_occluded: occluded + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false + }); + + const checkStable = () => { + const timeSinceLastChange = Date.now() - lastChange; + const totalWait = Date.now() - startTime; + + if (timeSinceLastChange >= quietPeriod) { + observer.disconnect(); + resolve(); + } else if (totalWait >= maxWait) { + observer.disconnect(); + console.warn('[SentienceAPI] DOM stability timeout - proceeding anyway'); + resolve(); + } else { + setTimeout(checkStable, 50); + } + }; + + checkStable(); + } else { + // DOM doesn't have enough nodes yet, wait for them + const observer = new MutationObserver(() => { + const currentCount = document.querySelectorAll('*').length; + const totalWait = Date.now() - startTime; + + if (currentCount >= minNodeCount) { + observer.disconnect(); + // Now wait for quiet period + let lastChange = Date.now(); + const quietObserver = new MutationObserver(() => { + lastChange = Date.now(); + }); + + quietObserver.observe(document.body, { + childList: true, + subtree: true, + attributes: false + }); + + const checkQuiet = () => { + const timeSinceLastChange = Date.now() - lastChange; + const totalWait = Date.now() - startTime; + + if (timeSinceLastChange >= quietPeriod) { + quietObserver.disconnect(); + resolve(); + } else if (totalWait >= maxWait) { + quietObserver.disconnect(); + console.warn('[SentienceAPI] DOM stability timeout - proceeding anyway'); + resolve(); + } else { + setTimeout(checkQuiet, 50); + } + }; + + checkQuiet(); + } else if (totalWait >= maxWait) { + observer.disconnect(); + console.warn('[SentienceAPI] DOM node count timeout - proceeding anyway'); + resolve(); + } }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false + }); + + // Timeout fallback + setTimeout(() => { + observer.disconnect(); + console.warn('[SentienceAPI] DOM stability max wait reached - proceeding'); + resolve(); + }, maxWait); + } + }); + } + + // --- HELPER: Collect Iframe Snapshots (Frame Stitching) --- + // Recursively collects snapshot data from all child iframes + // This enables detection of elements inside iframes (e.g., Stripe forms) + // + // NOTE: Cross-origin iframes cannot be accessed due to browser security (Same-Origin Policy). + // Only same-origin iframes will return snapshot data. Cross-origin iframes will be skipped + // with a warning. For cross-origin iframes, users must manually switch frames using + // Playwright's page.frame() API. + async function collectIframeSnapshots(options = {}) { + const iframeData = new Map(); // Map of iframe element -> snapshot data + + // Find all iframe elements in current document + const iframes = Array.from(document.querySelectorAll('iframe')); + + if (iframes.length === 0) { + return iframeData; + } + + console.log(`[SentienceAPI] Found ${iframes.length} iframe(s), requesting snapshots...`); + // Request snapshot from each iframe + const iframePromises = iframes.map((iframe, idx) => { + // OPTIMIZATION: Skip common ad domains to save time + const src = iframe.src || ''; + if (src.includes('doubleclick') || src.includes('googleadservices') || src.includes('ads system')) { + console.log(`[SentienceAPI] Skipping ad iframe: ${src.substring(0, 30)}...`); + return Promise.resolve(null); + } + + return new Promise((resolve) => { + const requestId = `iframe-${idx}-${Date.now()}`; + + // 1. EXTENDED TIMEOUT (Handle slow children) + const timeout = setTimeout(() => { + console.warn(`[SentienceAPI] ⚠️ Iframe ${idx} snapshot TIMEOUT (id: ${requestId})`); + resolve(null); + }, 5000); // Increased to 5s to handle slow processing + + // 2. ROBUST LISTENER with debugging + const listener = (event) => { + // Debug: Log all SENTIENCE_IFRAME_SNAPSHOT_RESPONSE messages to see what's happening + if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE') { + // Only log if it's not our request (for debugging) + if (event.data?.requestId !== requestId) { + // console.log(`[SentienceAPI] Received response for different request: ${event.data.requestId} (expected: ${requestId})`); + } + } + + // Check if this is the response we're waiting for + if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE' && + event.data?.requestId === requestId) { + + clearTimeout(timeout); + window.removeEventListener('message', listener); + + if (event.data.error) { + console.warn(`[SentienceAPI] Iframe ${idx} returned error:`, event.data.error); + resolve(null); + } else { + const elementCount = event.data.snapshot?.raw_elements?.length || 0; + console.log(`[SentienceAPI] ✓ Received ${elementCount} elements from Iframe ${idx} (id: ${requestId})`); + resolve({ + iframe: iframe, + data: event.data.snapshot, + error: null + }); + } + } + }; + + window.addEventListener('message', listener); + + // 3. SEND REQUEST with error handling + try { + if (iframe.contentWindow) { + // console.log(`[SentienceAPI] Sending request to Iframe ${idx} (id: ${requestId})`); + iframe.contentWindow.postMessage({ + type: 'SENTIENCE_IFRAME_SNAPSHOT_REQUEST', + requestId: requestId, + options: { + ...options, + collectIframes: true // Enable recursion for nested iframes + } + }, '*'); // Use '*' for cross-origin, but browser will enforce same-origin policy + } else { + console.warn(`[SentienceAPI] Iframe ${idx} contentWindow is inaccessible (Cross-Origin?)`); + clearTimeout(timeout); + window.removeEventListener('message', listener); + resolve(null); + } + } catch (error) { + console.error(`[SentienceAPI] Failed to postMessage to Iframe ${idx}:`, error); + clearTimeout(timeout); + window.removeEventListener('message', listener); + resolve(null); + } }); + }); + + // Wait for all iframe responses + const results = await Promise.all(iframePromises); + + // Store iframe data + results.forEach((result, idx) => { + if (result && result.data && !result.error) { + iframeData.set(iframes[idx], result.data); + console.log(`[SentienceAPI] ✓ Collected snapshot from iframe ${idx}`); + } else if (result && result.error) { + console.warn(`[SentienceAPI] Iframe ${idx} snapshot error:`, result.error); + } else if (!result) { + console.warn(`[SentienceAPI] Iframe ${idx} returned no data (timeout or error)`); + } + }); + + return iframeData; + } - // FREE TIER: No license checks - extension provides basic geometry data - // Pro/Enterprise tiers will be handled server-side (future work) - - // 1. Get Geometry from WASM - let result; - try { - if (options.limit || options.filter) { - result = wasmModule.analyze_page_with_options(rawData, options); - } else { - result = wasmModule.analyze_page(rawData); + // --- HELPER: Handle Iframe Snapshot Request (for child frames) --- + // When a parent frame requests snapshot, this handler responds with local snapshot + // NOTE: Recursion is safe because querySelectorAll('iframe') only finds direct children. + // Iframe A can ask Iframe B, but won't go back up to parent (no circular dependency risk). + function setupIframeSnapshotHandler() { + window.addEventListener('message', async (event) => { + // Security: only respond to snapshot requests from parent frames + if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_REQUEST') { + const { requestId, options } = event.data; + + try { + // Generate snapshot for this iframe's content + // Allow recursive collection - querySelectorAll('iframe') only finds direct children, + // so Iframe A will ask Iframe B, but won't go back up to parent (safe recursion) + // waitForStability: false makes performance better - i.e. don't wait for children frames + const snapshotOptions = { ...options, collectIframes: true, waitForStability: options.waitForStability === false ? false : false }; + const snapshot = await window.sentience.snapshot(snapshotOptions); + + // Send response back to parent + if (event.source && event.source.postMessage) { + event.source.postMessage({ + type: 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE', + requestId: requestId, + snapshot: snapshot, + error: null + }, '*'); + } + } catch (error) { + // Send error response + if (event.source && event.source.postMessage) { + event.source.postMessage({ + type: 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE', + requestId: requestId, + snapshot: null, + error: error.message + }, '*'); + } } - } catch (e) { - return { status: "error", error: e.message }; } + }); + } + + // Setup iframe handler when script loads (only once) + if (!window.sentience_iframe_handler_setup) { + setupIframeSnapshotHandler(); + window.sentience_iframe_handler_setup = true; + } - // Hydration step removed as WASM now returns populated structs + // --- GLOBAL API --- + window.sentience = { + // 1. Geometry snapshot (NEW ARCHITECTURE - No WASM in Main World!) + snapshot: async (options = {}) => { + try { + // Step 0: Wait for DOM stability if requested (for SPA hydration) + if (options.waitForStability !== false) { + await waitForStability(options.waitForStability || {}); + } + + // Step 1: Collect raw DOM data (Main World - CSP can't block this!) + const rawData = []; + window.sentience_registry = []; - // Capture Screenshot - let screenshot = null; - if (options.screenshot) { - screenshot = await captureScreenshot(options.screenshot); - } + const nodes = getAllElements(); + + nodes.forEach((el, idx) => { + if (!el.getBoundingClientRect) return; + const rect = el.getBoundingClientRect(); + if (rect.width < 5 || rect.height < 5) return; + + window.sentience_registry[idx] = el; + + const textVal = getText(el); + const inView = isInViewport(rect); + + // Get computed style once (needed for both occlusion check and data collection) + const style = window.getComputedStyle(el); + + // Only check occlusion for elements likely to be occluded (optimized) + // This avoids layout thrashing for the vast majority of elements + const occluded = inView ? isOccluded(el, rect, style) : false; + + // Get effective background color (traverses DOM to find non-transparent color) + const effectiveBgColor = getEffectiveBackgroundColor(el); + + rawData.push({ + id: idx, + tag: el.tagName.toLowerCase(), + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + styles: { + display: toSafeString(style.display), + visibility: toSafeString(style.visibility), + opacity: toSafeString(style.opacity), + z_index: toSafeString(style.zIndex || "auto"), + position: toSafeString(style.position), + bg_color: toSafeString(effectiveBgColor || style.backgroundColor), + color: toSafeString(style.color), + cursor: toSafeString(style.cursor), + font_weight: toSafeString(style.fontWeight), + font_size: toSafeString(style.fontSize) + }, + attributes: { + role: toSafeString(el.getAttribute('role')), + type_: toSafeString(el.getAttribute('type')), + aria_label: toSafeString(el.getAttribute('aria-label')), + href: toSafeString(el.href || el.getAttribute('href') || null), + class: toSafeString(getClassName(el)), + // Capture dynamic input state (not just initial attributes) + value: el.value !== undefined ? toSafeString(el.value) : toSafeString(el.getAttribute('value')), + checked: el.checked !== undefined ? String(el.checked) : null + }, + text: toSafeString(textVal), + in_viewport: inView, + is_occluded: occluded + }); + }); + + console.log(`[SentienceAPI] Collected ${rawData.length} elements from main frame`); - // C. Clean up null/undefined fields to save tokens (Your existing cleaner) - const cleanElement = (obj) => { - if (Array.isArray(obj)) { - return obj.map(cleanElement); - } else if (obj !== null && typeof obj === 'object') { - const cleaned = {}; - for (const [key, value] of Object.entries(obj)) { - // Keep boolean false for critical flags if desired, or remove to match Rust defaults - if (value !== null && value !== undefined) { - cleaned[key] = cleanElement(value); + // Step 1.5: Collect iframe snapshots and FLATTEN immediately + // "Flatten Early" architecture: Merge iframe elements into main array before WASM + // This allows WASM to process all elements uniformly (no recursion needed) + let allRawElements = [...rawData]; // Start with main frame elements + let totalIframeElements = 0; + + if (options.collectIframes !== false) { + try { + console.log(`[SentienceAPI] Starting iframe collection...`); + const iframeSnapshots = await collectIframeSnapshots(options); + console.log(`[SentienceAPI] Iframe collection complete. Received ${iframeSnapshots.size} snapshot(s)`); + + if (iframeSnapshots.size > 0) { + // FLATTEN IMMEDIATELY: Don't nest them. Just append them with coordinate translation. + iframeSnapshots.forEach((iframeSnapshot, iframeEl) => { + // Debug: Log structure to verify data is correct + // console.log(`[SentienceAPI] Processing iframe snapshot:`, iframeSnapshot); + + if (iframeSnapshot && iframeSnapshot.raw_elements) { + const rawElementsCount = iframeSnapshot.raw_elements.length; + console.log(`[SentienceAPI] Processing ${rawElementsCount} elements from iframe (src: ${iframeEl.src || 'unknown'})`); + // Get iframe's bounding rect (offset for coordinate translation) + const iframeRect = iframeEl.getBoundingClientRect(); + const offset = { x: iframeRect.x, y: iframeRect.y }; + + // Get iframe context for frame switching (Playwright needs this) + const iframeSrc = iframeEl.src || iframeEl.getAttribute('src') || ''; + let isSameOrigin = false; + try { + // Try to access contentWindow to check if same-origin + isSameOrigin = iframeEl.contentWindow !== null; + } catch (e) { + isSameOrigin = false; + } + + // Adjust coordinates and add iframe context to each element + const adjustedElements = iframeSnapshot.raw_elements.map(el => { + const adjusted = { ...el }; + + // Adjust rect coordinates to parent viewport + if (adjusted.rect) { + adjusted.rect = { + ...adjusted.rect, + x: adjusted.rect.x + offset.x, + y: adjusted.rect.y + offset.y + }; + } + + // Add iframe context so agents can switch frames in Playwright + adjusted.iframe_context = { + src: iframeSrc, + is_same_origin: isSameOrigin + }; + + return adjusted; + }); + + // Append flattened iframe elements to main array + allRawElements.push(...adjustedElements); + totalIframeElements += adjustedElements.length; + } + }); + + // console.log(`[SentienceAPI] Merged ${iframeSnapshots.size} iframe(s). Total elements: ${allRawElements.length} (${rawData.length} main + ${totalIframeElements} iframe)`); } + } catch (error) { + console.warn('[SentienceAPI] Iframe collection failed:', error); } - return cleaned; } - return obj; - }; - const cleanedElements = cleanElement(result); + // Step 2: Send EVERYTHING to WASM (One giant flat list) + // Now WASM prunes iframe elements and main elements in one pass! + // No recursion needed - everything is already flat + console.log(`[SentienceAPI] Sending ${allRawElements.length} total elements to WASM (${rawData.length} main + ${totalIframeElements} iframe)`); + const processed = await processSnapshotInBackground(allRawElements, options); + + if (!processed || !processed.elements) { + throw new Error('WASM processing returned invalid result'); + } + + // Step 3: Capture screenshot if requested + let screenshot = null; + if (options.screenshot) { + screenshot = await captureScreenshot(options.screenshot); + } - return { - status: "success", - url: window.location.href, - elements: cleanedElements, - raw_elements: rawData, // Include raw data for server-side processing (safe to expose - no proprietary value) - screenshot: screenshot - }; + // Step 4: Clean and return + const cleanedElements = cleanElement(processed.elements); + const cleanedRawElements = cleanElement(processed.raw_elements); + + // FIXED: Removed undefined 'totalIframeRawElements' + // FIXED: Logic updated for "Flatten Early" architecture. + // processed.elements ALREADY contains the merged iframe elements, + // so we simply use .length. No addition needed. + + const totalCount = cleanedElements.length; + const totalRaw = cleanedRawElements.length; + const iframeCount = totalIframeElements || 0; + + console.log(`[SentienceAPI] ✓ Complete: ${totalCount} Smart Elements, ${totalRaw} Raw Elements (includes ${iframeCount} from iframes) (WASM took ${processed.duration?.toFixed(1)}ms)`); + + return { + status: "success", + url: window.location.href, + viewport: { + width: window.innerWidth, + height: window.innerHeight + }, + elements: cleanedElements, + raw_elements: cleanedRawElements, + screenshot: screenshot + }; + } catch (error) { + console.error('[SentienceAPI] snapshot() failed:', error); + console.error('[SentienceAPI] Error stack:', error.stack); + return { + status: "error", + error: error.message || 'Unknown error', + stack: error.stack + }; + } }, - // 2. Read Content (New) + + // 2. Read Content (unchanged) read: (options = {}) => { - const format = options.format || 'raw'; // 'raw', 'text', or 'markdown' + const format = options.format || 'raw'; let content; - + if (format === 'raw') { - // Return raw HTML suitable for Turndown or other Node.js libraries content = getRawHTML(document.body); } else if (format === 'markdown') { - // Return lightweight markdown conversion content = convertToMarkdown(document.body); } else { - // Default to text content = convertToText(document.body); } - + return { status: "success", url: window.location.href, @@ -428,11 +1086,388 @@ }; }, - // 3. Action + // 2b. Find Text Rectangle - Get exact pixel coordinates of specific text + findTextRect: (options = {}) => { + const { + text, + containerElement = document.body, + caseSensitive = false, + wholeWord = false, + maxResults = 10 + } = options; + + if (!text || text.trim().length === 0) { + return { + status: "error", + error: "Text parameter is required" + }; + } + + const results = []; + const searchText = caseSensitive ? text : text.toLowerCase(); + + // Helper function to find text in a single text node + function findInTextNode(textNode) { + const nodeText = textNode.nodeValue; + const searchableText = caseSensitive ? nodeText : nodeText.toLowerCase(); + + let startIndex = 0; + while (startIndex < nodeText.length && results.length < maxResults) { + const foundIndex = searchableText.indexOf(searchText, startIndex); + + if (foundIndex === -1) break; + + // Check whole word matching if required + if (wholeWord) { + const before = foundIndex > 0 ? nodeText[foundIndex - 1] : ' '; + const after = foundIndex + text.length < nodeText.length + ? nodeText[foundIndex + text.length] + : ' '; + + // Check if surrounded by word boundaries + if (!/\s/.test(before) || !/\s/.test(after)) { + startIndex = foundIndex + 1; + continue; + } + } + + try { + // Create range for this occurrence + const range = document.createRange(); + range.setStart(textNode, foundIndex); + range.setEnd(textNode, foundIndex + text.length); + + const rect = range.getBoundingClientRect(); + + // Only include visible rectangles + if (rect.width > 0 && rect.height > 0) { + results.push({ + text: nodeText.substring(foundIndex, foundIndex + text.length), + rect: { + x: rect.left + window.scrollX, + y: rect.top + window.scrollY, + width: rect.width, + height: rect.height, + left: rect.left + window.scrollX, + top: rect.top + window.scrollY, + right: rect.right + window.scrollX, + bottom: rect.bottom + window.scrollY + }, + viewport_rect: { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height + }, + context: { + before: nodeText.substring(Math.max(0, foundIndex - 20), foundIndex), + after: nodeText.substring(foundIndex + text.length, Math.min(nodeText.length, foundIndex + text.length + 20)) + }, + in_viewport: ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ) + }); + } + } catch (e) { + console.warn('[SentienceAPI] Failed to get rect for text:', e); + } + + startIndex = foundIndex + 1; + } + } + + // Tree walker to find all text nodes + const walker = document.createTreeWalker( + containerElement, + NodeFilter.SHOW_TEXT, + { + acceptNode: function(node) { + // Skip script, style, and empty text nodes + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + + const tagName = parent.tagName.toLowerCase(); + if (tagName === 'script' || tagName === 'style' || tagName === 'noscript') { + return NodeFilter.FILTER_REJECT; + } + + // Skip whitespace-only nodes + if (!node.nodeValue || node.nodeValue.trim().length === 0) { + return NodeFilter.FILTER_REJECT; + } + + // Check if element is visible + const computedStyle = window.getComputedStyle(parent); + if (computedStyle.display === 'none' || + computedStyle.visibility === 'hidden' || + computedStyle.opacity === '0') { + return NodeFilter.FILTER_REJECT; + } + + return NodeFilter.FILTER_ACCEPT; + } + } + ); + + // Walk through all text nodes + let currentNode; + while ((currentNode = walker.nextNode()) && results.length < maxResults) { + findInTextNode(currentNode); + } + + return { + status: "success", + query: text, + case_sensitive: caseSensitive, + whole_word: wholeWord, + matches: results.length, + results: results, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + scroll_x: window.scrollX, + scroll_y: window.scrollY + } + }; + }, + + // 3. Click Action (unchanged) click: (id) => { const el = window.sentience_registry[id]; - if (el) { el.click(); el.focus(); return true; } + if (el) { + el.click(); + el.focus(); + return true; + } return false; + }, + + // 4. Inspector Mode: Start Recording for Golden Set Collection + startRecording: (options = {}) => { + const { + highlightColor = '#ff0000', + successColor = '#00ff00', + autoDisableTimeout = 30 * 60 * 1000, // 30 minutes default + keyboardShortcut = 'Ctrl+Shift+I' + } = options; + + console.log("🔴 [Sentience] Recording Mode STARTED. Click an element to copy its Ground Truth JSON."); + console.log(` Press ${keyboardShortcut} or call stopRecording() to stop.`); + + // Validate registry is populated + if (!window.sentience_registry || window.sentience_registry.length === 0) { + console.warn("⚠️ Registry empty. Call `await window.sentience.snapshot()` first to populate registry."); + alert("Registry empty. Run `await window.sentience.snapshot()` first!"); + return () => {}; // Return no-op cleanup function + } + + // Create reverse mapping for O(1) lookup (fixes registry lookup bug) + window.sentience_registry_map = new Map(); + window.sentience_registry.forEach((el, idx) => { + if (el) window.sentience_registry_map.set(el, idx); + }); + + // Create highlight box overlay + let highlightBox = document.getElementById('sentience-highlight-box'); + if (!highlightBox) { + highlightBox = document.createElement('div'); + highlightBox.id = 'sentience-highlight-box'; + highlightBox.style.cssText = ` + position: fixed; + pointer-events: none; + z-index: 2147483647; + border: 2px solid ${highlightColor}; + background: rgba(255, 0, 0, 0.1); + display: none; + transition: all 0.1s ease; + box-sizing: border-box; + `; + document.body.appendChild(highlightBox); + } + + // Create visual indicator (red border on page when recording) + let recordingIndicator = document.getElementById('sentience-recording-indicator'); + if (!recordingIndicator) { + recordingIndicator = document.createElement('div'); + recordingIndicator.id = 'sentience-recording-indicator'; + recordingIndicator.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + height: 3px; + background: ${highlightColor}; + z-index: 2147483646; + pointer-events: none; + `; + document.body.appendChild(recordingIndicator); + } + recordingIndicator.style.display = 'block'; + + // Hover handler (visual feedback) + const mouseOverHandler = (e) => { + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + + const rect = el.getBoundingClientRect(); + highlightBox.style.display = 'block'; + highlightBox.style.top = (rect.top + window.scrollY) + 'px'; + highlightBox.style.left = (rect.left + window.scrollX) + 'px'; + highlightBox.style.width = rect.width + 'px'; + highlightBox.style.height = rect.height + 'px'; + }; + + // Click handler (capture ground truth data) + const clickHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + + // Use Map for reliable O(1) lookup + const sentienceId = window.sentience_registry_map.get(el); + if (sentienceId === undefined) { + console.warn("⚠️ Element not found in Sentience Registry. Did you run snapshot() first?"); + alert("Element not in registry. Run `await window.sentience.snapshot()` first!"); + return; + } + + // Extract raw data (ground truth + raw signals, NOT model outputs) + const rawData = extractRawElementData(el); + const selector = getUniqueSelector(el); + const role = el.getAttribute('role') || el.tagName.toLowerCase(); + const text = getText(el); + + // Build golden set JSON (ground truth + raw signals only) + const snippet = { + task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`, + url: window.location.href, + timestamp: new Date().toISOString(), + target_criteria: { + id: sentienceId, + selector: selector, + role: role, + text: text.substring(0, 50) + }, + debug_snapshot: rawData + }; + + // Copy to clipboard + const jsonString = JSON.stringify(snippet, null, 2); + navigator.clipboard.writeText(jsonString).then(() => { + console.log("✅ Copied Ground Truth to clipboard:", snippet); + + // Flash green to indicate success + highlightBox.style.border = `2px solid ${successColor}`; + highlightBox.style.background = 'rgba(0, 255, 0, 0.2)'; + setTimeout(() => { + highlightBox.style.border = `2px solid ${highlightColor}`; + highlightBox.style.background = 'rgba(255, 0, 0, 0.1)'; + }, 500); + }).catch(err => { + console.error("❌ Failed to copy to clipboard:", err); + alert("Failed to copy to clipboard. Check console for JSON."); + }); + }; + + // Auto-disable timeout + let timeoutId = null; + + // Cleanup function to stop recording (defined before use) + const stopRecording = () => { + document.removeEventListener('mouseover', mouseOverHandler, true); + document.removeEventListener('click', clickHandler, true); + document.removeEventListener('keydown', keyboardHandler, true); + + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + + if (highlightBox) { + highlightBox.style.display = 'none'; + } + + if (recordingIndicator) { + recordingIndicator.style.display = 'none'; + } + + // Clean up registry map (optional, but good practice) + if (window.sentience_registry_map) { + window.sentience_registry_map.clear(); + } + + // Remove global reference + if (window.sentience_stopRecording === stopRecording) { + delete window.sentience_stopRecording; + } + + console.log("⚪ [Sentience] Recording Mode STOPPED."); + }; + + // Keyboard shortcut handler (defined after stopRecording) + const keyboardHandler = (e) => { + // Ctrl+Shift+I or Cmd+Shift+I + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'I') { + e.preventDefault(); + stopRecording(); + } + }; + + // Attach event listeners (use capture phase to intercept early) + document.addEventListener('mouseover', mouseOverHandler, true); + document.addEventListener('click', clickHandler, true); + document.addEventListener('keydown', keyboardHandler, true); + + // Set up auto-disable timeout + if (autoDisableTimeout > 0) { + timeoutId = setTimeout(() => { + console.log("⏰ [Sentience] Recording Mode auto-disabled after timeout."); + stopRecording(); + }, autoDisableTimeout); + } + + // Store stop function globally for keyboard shortcut access + window.sentience_stopRecording = stopRecording; + + return stopRecording; + } + }; + + /** + * Show overlay highlighting specific elements with Shadow DOM + * @param {Array} elements - List of elements with bbox, importance, visual_cues + * @param {number} targetElementId - Optional ID of target element (shown in red) + */ + window.sentience.showOverlay = function(elements, targetElementId = null) { + if (!elements || !Array.isArray(elements)) { + console.warn('[Sentience] showOverlay: elements must be an array'); + return; } + + window.postMessage({ + type: 'SENTIENCE_SHOW_OVERLAY', + elements: elements, + targetElementId: targetElementId, + timestamp: Date.now() + }, '*'); + + console.log(`[Sentience] Overlay requested for ${elements.length} elements`); }; -})(); \ No newline at end of file + + /** + * Clear overlay manually + */ + window.sentience.clearOverlay = function() { + window.postMessage({ + type: 'SENTIENCE_CLEAR_OVERLAY' + }, '*'); + console.log('[Sentience] Overlay cleared'); + }; + + console.log('[SentienceAPI] ✓ Ready! (CSP-Resistant - WASM runs in background)'); +})(); diff --git a/src/extension/manifest.json b/src/extension/manifest.json index 9d979cb1..f75c6817 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -1,10 +1,14 @@ { "manifest_version": 3, "name": "Sentience Semantic Visual Grounding Extractor", - "version": "1.0.5", + "version": "2.0.7", "description": "Extract semantic visual grounding data from web pages", "permissions": ["activeTab", "scripting"], "host_permissions": [""], + "background": { + "service_worker": "background.js", + "type": "module" + }, "web_accessible_resources": [ { "resources": ["pkg/*"], @@ -15,16 +19,18 @@ { "matches": [""], "js": ["content.js"], - "run_at": "document_start" + "run_at": "document_start", + "all_frames": true }, { "matches": [""], "js": ["injected_api.js"], "run_at": "document_idle", - "world": "MAIN" + "world": "MAIN", + "all_frames": true } ], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" } -} \ No newline at end of file +} diff --git a/src/extension/pkg/README.md b/src/extension/pkg/README.md index ca9d035c..7f0d49b0 100644 --- a/src/extension/pkg/README.md +++ b/src/extension/pkg/README.md @@ -21,6 +21,7 @@ Perfect for AI agents, automation scripts, visual grounding, and accessibility t 11. [API Reference](#api-reference) 12. [Performance](#performance) 13. [Troubleshooting](#troubleshooting) +14. Transferred to SentienceAPI Org --- @@ -151,13 +152,20 @@ Claude/ ## User API -### The Only Function You Need +### Core Functions + +The extension provides two main functions: + +1. **`window.sentience.snapshot(options?)`** - Extract page geometry and elements +2. **`window.sentience.findTextRect(options)`** - Find exact pixel coordinates of text + +### snapshot() - Geometry Extraction ```javascript window.sentience.snapshot(options?) ``` -**One function, many capabilities:** +**Capabilities:** - Get geometry map - Capture screenshot - Filter by role/size/z-index @@ -213,6 +221,159 @@ await window.sentience.snapshot({ } ``` +### findTextRect() - Text Location Finder + +Find exact pixel coordinates of any text on the page using the DOM Range API. Perfect for highlighting specific words, clicking on text, or text-based navigation **without Vision Models**. + +```javascript +window.sentience.findTextRect(options) +``` + +**Parameters:** +```typescript +{ + text: string, // Required: Text to find + containerElement?: Element, // Optional: Search within (default: document.body) + caseSensitive?: boolean, // Optional: Case-sensitive search (default: false) + wholeWord?: boolean, // Optional: Match whole words only (default: false) + maxResults?: number // Optional: Limit results (default: 10) +} +``` + +**Returns:** +```typescript +{ + status: "success" | "error", + query: string, // The search text + case_sensitive: boolean, + whole_word: boolean, + matches: number, // Total matches found + results: [{ + text: string, // Actual matched text + rect: { // Absolute coordinates (with scroll) + x: number, + y: number, + width: number, + height: number, + left: number, + top: number, + right: number, + bottom: number + }, + viewport_rect: { // Viewport-relative coordinates + x: number, + y: number, + width: number, + height: number + }, + context: { // Surrounding text + before: string, // 20 chars before + after: string // 20 chars after + }, + in_viewport: boolean // Is it currently visible? + }], + viewport: { + width: number, + height: number, + scroll_x: number, + scroll_y: number + }, + error?: string // Error message if status is "error" +} +``` + +**Usage Examples:** + +```javascript +// Example 1: Find "Add to Cart" text +const result = await window.sentience.findTextRect({ + text: "Add to Cart" +}); + +if (result.status === "success") { + console.log(`Found ${result.matches} occurrences`); + result.results.forEach((match, i) => { + console.log(`${i+1}. At (${match.rect.x}, ${match.rect.y})`); + console.log(` Context: "${match.context.before}${match.text}${match.context.after}"`); + }); +} + +// Example 2: Highlight all matches +const result = await window.sentience.findTextRect({ + text: "price", + caseSensitive: false, + maxResults: 20 +}); + +result.results.forEach(match => { + const highlight = document.createElement('div'); + highlight.style.cssText = ` + position: absolute; + left: ${match.rect.x}px; + top: ${match.rect.y}px; + width: ${match.rect.width}px; + height: ${match.rect.height}px; + background: yellow; + opacity: 0.5; + pointer-events: none; + z-index: 9999; + `; + document.body.appendChild(highlight); +}); + +// Example 3: Click on specific text (not button!) +const result = await window.sentience.findTextRect({ + text: "Terms of Service", + wholeWord: true +}); + +if (result.matches > 0) { + const first = result.results[0]; + // Click the center of the text + const centerX = first.viewport_rect.x + first.viewport_rect.width / 2; + const centerY = first.viewport_rect.y + first.viewport_rect.height / 2; + + document.elementFromPoint(centerX, centerY)?.click(); +} + +// Example 4: Find text only in header +const header = document.querySelector('header'); +const result = await window.sentience.findTextRect({ + text: "Login", + containerElement: header +}); + +// Example 5: Scroll to first match +const result = await window.sentience.findTextRect({ + text: "Contact Us" +}); + +if (result.matches > 0) { + const first = result.results[0]; + window.scrollTo({ + top: first.rect.y - 100, // Offset for header + behavior: 'smooth' + }); +} +``` + +**Use Cases:** +- 🎯 **Text-based clicking** - Click on text that's not in a button +- 🖍️ **Text highlighting** - Draw bounding boxes around specific words +- 📍 **Text navigation** - Scroll to specific content +- ♿ **Accessibility** - Find and highlight important text +- 🤖 **AI Agents** - Locate text without vision models +- 🔍 **Search results** - Find and highlight search terms + +**Features:** +- ✅ Pixel-perfect coordinates using DOM Range API +- ✅ Filters invisible/hidden text automatically +- ✅ Returns both absolute and viewport-relative coordinates +- ✅ Provides context for ambiguous matches +- ✅ Handles multiple occurrences +- ✅ Performance-safe with result limits +- ✅ Works with case-insensitive and whole-word matching + --- ## Usage Examples diff --git a/src/extension/pkg/sentience_core.d.ts b/src/extension/pkg/sentience_core.d.ts index 017160d8..e280c268 100644 --- a/src/extension/pkg/sentience_core.d.ts +++ b/src/extension/pkg/sentience_core.d.ts @@ -7,6 +7,14 @@ export function analyze_page_with_options(val: any, options: any): any; export function decide_and_act(_raw_elements: any): void; +/** + * Prune raw elements before sending to API + * This is a "dumb" filter that reduces payload size without leaking proprietary IP + * Filters out: tiny elements, invisible elements, non-interactive wrapper divs + * Amazon: 5000-6000 elements -> ~200-400 elements (~95% reduction) + */ +export function prune_for_api(val: any): any; + export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { @@ -14,6 +22,7 @@ export interface InitOutput { readonly analyze_page: (a: number) => number; readonly analyze_page_with_options: (a: number, b: number) => number; readonly decide_and_act: (a: number) => void; + readonly prune_for_api: (a: number) => number; readonly __wbindgen_export: (a: number, b: number) => number; readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_export3: (a: number) => void; diff --git a/src/extension/pkg/sentience_core.js b/src/extension/pkg/sentience_core.js index bb44be74..b232d138 100644 --- a/src/extension/pkg/sentience_core.js +++ b/src/extension/pkg/sentience_core.js @@ -223,6 +223,19 @@ export function decide_and_act(_raw_elements) { wasm.decide_and_act(addHeapObject(_raw_elements)); } +/** + * Prune raw elements before sending to API + * This is a "dumb" filter that reduces payload size without leaking proprietary IP + * Filters out: tiny elements, invisible elements, non-interactive wrapper divs + * Amazon: 5000-6000 elements -> ~200-400 elements (~95% reduction) + * @param {any} val + * @returns {any} + */ +export function prune_for_api(val) { + const ret = wasm.prune_for_api(addHeapObject(val)); + return takeObject(ret); +} + const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); async function __wbg_load(module, imports) { @@ -338,6 +351,9 @@ function __wbg_get_imports() { const ret = getObject(arg0).done; return ret; }; + imports.wbg.__wbg_error_7bc7d576a6aaf855 = function(arg0) { + console.error(getObject(arg0)); + }; imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return addHeapObject(ret); diff --git a/src/extension/pkg/sentience_core_bg.wasm b/src/extension/pkg/sentience_core_bg.wasm index 10a312c90c6c49bbb03136420025ad179f22abfc..ddb4659c370c3d7ab4cf47552b7fd88d2b2e2218 100644 GIT binary patch delta 31061 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 YoZbwg4UoO6sE^-Q*u7Wxo`psK2XG7_j{pDw diff --git a/src/extension/pkg/sentience_core_bg.wasm.d.ts b/src/extension/pkg/sentience_core_bg.wasm.d.ts index 35441434..dccf049f 100644 --- a/src/extension/pkg/sentience_core_bg.wasm.d.ts +++ b/src/extension/pkg/sentience_core_bg.wasm.d.ts @@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory; export const analyze_page: (a: number) => number; export const analyze_page_with_options: (a: number, b: number) => number; export const decide_and_act: (a: number) => void; +export const prune_for_api: (a: number) => number; export const __wbindgen_export: (a: number, b: number) => number; export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_export3: (a: number) => void; diff --git a/src/extension/release.json b/src/extension/release.json new file mode 100644 index 00000000..ebade77e --- /dev/null +++ b/src/extension/release.json @@ -0,0 +1,115 @@ +{ + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/273122615", + "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/273122615/assets", + "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/273122615/assets{?name,label}", + "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.0.7", + "id": 273122615, + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "node_id": "RE_kwDOQshiJ84QR4U3", + "tag_name": "v2.0.7", + "target_commitish": "main", + "name": "Release v2.0.7", + "draft": false, + "immutable": false, + "prerelease": false, + "created_at": "2025-12-29T03:56:13Z", + "updated_at": "2025-12-29T03:57:09Z", + "published_at": "2025-12-29T03:57:08Z", + "assets": [ + { + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/333966751", + "id": 333966751, + "node_id": "RA_kwDOQshiJ84T5-2f", + "name": "extension-files.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 78091, + "digest": "sha256:e281f8b755b61da4b8015d6172064aa9a337c14133ceceff4ab29199ee53307e", + "download_count": 2, + "created_at": "2025-12-29T03:57:09Z", + "updated_at": "2025-12-29T03:57:09Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.0.7/extension-files.tar.gz" + }, + { + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/333966752", + "id": 333966752, + "node_id": "RA_kwDOQshiJ84T5-2g", + "name": "extension-package.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 80179, + "digest": "sha256:a025edeb8b6d05bfb25c57f913b68507060653ecbdf616000a46df4cb8dec377", + "download_count": 0, + "created_at": "2025-12-29T03:57:09Z", + "updated_at": "2025-12-29T03:57:09Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.0.7/extension-package.zip" + } + ], + "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.0.7", + "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.0.7", + "body": "**Full Changelog**: https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/compare/v2.0.6...v2.0.7" +} diff --git a/src/extension/test-content.js b/src/extension/test-content.js new file mode 100644 index 00000000..7ca4ddc9 --- /dev/null +++ b/src/extension/test-content.js @@ -0,0 +1,4 @@ +// test-content.js - Simple test script +console.log('TEST: Extension content script is loading!'); +window.testExtension = true; +alert('Extension loaded!');