From 013e4d587adf7427744fabb73d17d95c77bf3465 Mon Sep 17 00:00:00 2001 From: rcholic Date: Fri, 23 Jan 2026 06:15:02 +0000 Subject: [PATCH] chore: sync extension files from sentience-chrome v2.8.3 --- sentience/extension/background.js | 310 +-- sentience/extension/content.js | 611 ++--- sentience/extension/injected_api.js | 3891 ++++++++------------------- sentience/extension/manifest.json | 2 +- sentience/extension/release.json | 92 +- 5 files changed, 1484 insertions(+), 3422 deletions(-) diff --git a/sentience/extension/background.js b/sentience/extension/background.js index 33e0b13..b5192d9 100644 --- a/sentience/extension/background.js +++ b/sentience/extension/background.js @@ -1,242 +1,104 @@ -// Sentience Chrome Extension - Background Service Worker -// Auto-generated from modular source -import init, { analyze_page_with_options, analyze_page, prune_for_api } from './pkg/sentience_core.js'; +import init, { analyze_page_with_options, analyze_page, prune_for_api } from "./pkg/sentience_core.js"; -// background.js - Service Worker with WASM (CSP-Immune!) -// This runs in an isolated environment, completely immune to page CSP policies +let wasmReady = !1, wasmInitPromise = null; - -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; + if (!wasmReady) return wasmInitPromise || (wasmInitPromise = (async () => { + try { + globalThis.js_click_element = () => {}, await init(), wasmReady = !0; + } catch (error) { + throw error; + } + })(), wasmInitPromise); +} - wasmInitPromise = (async () => { +async function handleScreenshotCapture(_tabId, options = {}) { 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 = () => { - 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' - ); + const {format: format = "png", quality: quality = 90} = options; + return await chrome.tabs.captureVisibleTab(null, { + format: format, + quality: quality + }); } catch (error) { - console.error('[Sentience Background] WASM initialization failed:', error); - throw error; + throw new Error(`Failed to capture screenshot: ${error.message}`); } - })(); - - 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) => { - // 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 - } - - // 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; - } -}); - -/** - * Handle screenshot capture (existing functionality) - */ -async function handleScreenshotCapture(_tabId, options = {}) { - try { - const { format = 'png', quality = 90 } = options; - - const dataUrl = await chrome.tabs.captureVisibleTab(null, { - format, - 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}`); - } -} - -/** - * 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 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; + const startTime = performance.now(); try { - // Use a timeout wrapper to prevent infinite hangs - const wasmPromise = new Promise((resolve, reject) => { + if (!Array.isArray(rawData)) throw new Error("rawData must be an array"); + if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(), + !wasmReady) throw new Error("WASM module not initialized"); + let analyzedElements, prunedRawData; try { - let result; - if (options.limit || options.filter) { - result = analyze_page_with_options(rawData, options); - } else { - result = analyze_page(rawData); - } - resolve(result); + const wasmPromise = new Promise((resolve, reject) => { + try { + let result; + result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData), + resolve(result); + } catch (e) { + reject(e); + } + }); + analyzedElements = await Promise.race([ wasmPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("WASM processing timeout (>18s)")), 18e3)) ]); } catch (e) { - reject(e); + const errorMsg = e.message || "Unknown WASM error"; + throw new Error(`WASM analyze_page failed: ${errorMsg}`); } - }); - - // 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}`); + try { + prunedRawData = prune_for_api(rawData); + } catch (e) { + prunedRawData = rawData; + } + performance.now(); + return { + elements: analyzedElements, + raw_elements: prunedRawData + }; + } catch (error) { + performance.now(); + throw error; } +} - // Prune elements for API (prevents 413 errors on large sites) - let prunedRawData; +initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { try { - prunedRawData = prune_for_api(rawData); - } catch (e) { - console.warn('[Sentience Background] prune_for_api failed, using original data:', e); - prunedRawData = rawData; + return "captureScreenshot" === request.action ? (handleScreenshotCapture(sender.tab.id, request.options).then(screenshot => { + sendResponse({ + success: !0, + screenshot: screenshot + }); + }).catch(error => { + sendResponse({ + success: !1, + error: error.message || "Screenshot capture failed" + }); + }), !0) : "processSnapshot" === request.action ? (handleSnapshotProcessing(request.rawData, request.options).then(result => { + sendResponse({ + success: !0, + result: result + }); + }).catch(error => { + sendResponse({ + success: !1, + error: error.message || "Snapshot processing failed" + }); + }), !0) : (sendResponse({ + success: !1, + error: "Unknown action" + }), !1); + } catch (error) { + try { + sendResponse({ + success: !1, + error: `Fatal error: ${error.message || "Unknown error"}` + }); + } catch (e) {} + return !1; } - - 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 -}); +}), self.addEventListener("error", event => { + event.preventDefault(); +}), self.addEventListener("unhandledrejection", event => { + event.preventDefault(); +}); \ No newline at end of file diff --git a/sentience/extension/content.js b/sentience/extension/content.js index ee6efa2..b65cfb5 100644 --- a/sentience/extension/content.js +++ b/sentience/extension/content.js @@ -1,456 +1,161 @@ -// Sentience Chrome Extension - Content Script -// Auto-generated from modular source -(function () { - 'use strict'; - - // content.js - ISOLATED WORLD (Bridge between Main World and Background) - console.log('[Sentience Bridge] Loaded.'); - - // 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. 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) 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; - - case 'SENTIENCE_SHOW_GRID_OVERLAY': - handleShowGridOverlay(event.data); - break; - } - }); - - /** - * Handle screenshot requests (existing functionality) - */ - function handleScreenshotRequest(data) { - chrome.runtime.sendMessage({ action: 'captureScreenshot', options: data.options }, (response) => { - window.postMessage( - { - type: 'SENTIENCE_SCREENSHOT_RESULT', - requestId: data.requestId, - screenshot: response?.success ? response.screenshot : null, - error: response?.error, - }, - '*' - ); - }); - } - - /** - * 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, - }, - '*' - ); - } - }, 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, - }, - '*' - ); - 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, - }, - '*' - ); - } 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, - }, - '*' - ); - } +!function() { + "use strict"; + window, window.top; + document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id, window.addEventListener("message", event => { + var data; + if (event.source === window) switch (event.data.type) { + case "SENTIENCE_SCREENSHOT_REQUEST": + data = event.data, chrome.runtime.sendMessage({ + action: "captureScreenshot", + options: data.options + }, response => { + window.postMessage({ + type: "SENTIENCE_SCREENSHOT_RESULT", + requestId: data.requestId, + screenshot: response?.success ? response.screenshot : null, + error: response?.error + }, "*"); + }); + break; + + case "SENTIENCE_SNAPSHOT_REQUEST": + !function(data) { + const startTime = performance.now(); + let responded = !1; + const timeoutId = setTimeout(() => { + if (!responded) { + responded = !0; + const duration = performance.now() - startTime; + window.postMessage({ + type: "SENTIENCE_SNAPSHOT_RESULT", + requestId: data.requestId, + error: "WASM processing timeout - background script may be unresponsive", + duration: duration + }, "*"); + } + }, 2e4); + try { + chrome.runtime.sendMessage({ + action: "processSnapshot", + rawData: data.rawData, + options: data.options + }, response => { + if (responded) return; + responded = !0, clearTimeout(timeoutId); + const duration = performance.now() - startTime; + chrome.runtime.lastError ? window.postMessage({ + type: "SENTIENCE_SNAPSHOT_RESULT", + requestId: data.requestId, + error: `Chrome runtime error: ${chrome.runtime.lastError.message}`, + duration: duration + }, "*") : response?.success ? window.postMessage({ + type: "SENTIENCE_SNAPSHOT_RESULT", + requestId: data.requestId, + elements: response.result.elements, + raw_elements: response.result.raw_elements, + duration: duration + }, "*") : window.postMessage({ + type: "SENTIENCE_SNAPSHOT_RESULT", + requestId: data.requestId, + error: response?.error || "Processing failed", + duration: duration + }, "*"); + }); + } catch (error) { + if (!responded) { + responded = !0, clearTimeout(timeoutId); + const duration = performance.now() - startTime; + window.postMessage({ + type: "SENTIENCE_SNAPSHOT_RESULT", + requestId: data.requestId, + error: `Failed to send message: ${error.message}`, + duration: duration + }, "*"); + } + } + }(event.data); + break; + + case "SENTIENCE_SHOW_OVERLAY": + !function(data) { + const {elements: elements, targetElementId: targetElementId} = data; + if (!elements || !Array.isArray(elements)) return; + removeOverlay(); + const host = document.createElement("div"); + host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", + document.body.appendChild(host); + const shadow = host.attachShadow({ + mode: "closed" + }), 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, isPrimary = element.visual_cues?.is_primary || !1, importance = element.importance || 0; + let color; + color = isTarget ? "#FF0000" : isPrimary ? "#0066FF" : "#00FF00"; + const importanceRatio = maxImportance > 0 ? importance / maxImportance : .5, borderOpacity = isTarget ? 1 : isPrimary ? .9 : Math.max(.4, .5 + .5 * importanceRatio), fillOpacity = .2 * borderOpacity, borderWidth = isTarget ? 2 : isPrimary ? 1.5 : Math.max(.5, Math.round(2 * importanceRatio)), hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div"); + if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `, + importance > 0 || isPrimary) { + const badge = document.createElement("span"); + badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, + box.appendChild(badge); + } + if (isTarget) { + const targetIndicator = document.createElement("span"); + targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", + box.appendChild(targetIndicator); + } + shadow.appendChild(box); + }), overlayTimeout = setTimeout(() => { + removeOverlay(); + }, 5e3); + }(event.data); + break; + + case "SENTIENCE_CLEAR_OVERLAY": + removeOverlay(); + break; + + case "SENTIENCE_SHOW_GRID_OVERLAY": + !function(data) { + const {grids: grids, targetGridId: targetGridId} = data; + if (!grids || !Array.isArray(grids)) return; + removeOverlay(); + const host = document.createElement("div"); + host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", + document.body.appendChild(host); + const shadow = host.attachShadow({ + mode: "closed" + }); + grids.forEach(grid => { + const bbox = grid.bbox; + if (!bbox) return; + const isTarget = grid.grid_id === targetGridId, isDominant = !0 === grid.is_dominant; + let color = "#9B59B6"; + isTarget ? color = "#FF0000" : isDominant && (color = "#FF8C00"); + const borderStyle = isTarget ? "solid" : "dashed", borderWidth = isTarget ? 3 : isDominant ? 2.5 : 2, opacity = isTarget ? 1 : isDominant ? .9 : .8, fillOpacity = .1 * opacity, hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div"); + box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px ${borderStyle} ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${opacity};\n pointer-events: none;\n `; + let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`; + grid.is_dominant && (labelText = `⭐ ${labelText} (dominant)`); + const badge = document.createElement("span"); + if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, + box.appendChild(badge), isTarget) { + const targetIndicator = document.createElement("span"); + targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", + box.appendChild(targetIndicator); + } + shadow.appendChild(box); + }), overlayTimeout = setTimeout(() => { + removeOverlay(); + }, 5e3); + }(event.data); } - ); - } 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, - }, - '*' - ); - } - } - } - - // ============================================================================ - // 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); - } - - /** - * Show grid overlay highlighting detected grids - * @param {Object} data - Message data with grids and targetGridId - */ - function handleShowGridOverlay(data) { - const { grids, targetGridId } = data; - - if (!grids || !Array.isArray(grids)) { - console.warn('[Sentience Bridge] showGridOverlay: grids must be an array'); - return; + const OVERLAY_HOST_ID = "sentience-overlay-host"; + let overlayTimeout = null; + function removeOverlay() { + const existing = document.getElementById(OVERLAY_HOST_ID); + existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout), + overlayTimeout = null); } - - 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' }); - - grids.forEach((grid) => { - const bbox = grid.bbox; - if (!bbox) return; - - const isTarget = grid.grid_id === targetGridId; - const isDominant = grid.is_dominant === true; - - // Grid colors: Red for target, Orange for dominant, Purple for regular - let color = '#9B59B6'; // Purple (default) - if (isTarget) { - color = '#FF0000'; // Red for target - } else if (isDominant) { - color = '#FF8C00'; // Orange for dominant group - } - const borderStyle = isTarget ? 'solid' : 'dashed'; // Dashed for grids - const borderWidth = isTarget ? 3 : isDominant ? 2.5 : 2; - const opacity = isTarget ? 1.0 : isDominant ? 0.9 : 0.8; - const fillOpacity = opacity * 0.1; // 10% opacity for grids (lighter than elements) - - // Convert fill opacity to hex for background-color - const hexOpacity = Math.round(fillOpacity * 255) - .toString(16) - .padStart(2, '0'); - - // Create grid box - 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 ${borderStyle} ${color}; - background-color: ${color}${hexOpacity}; - box-sizing: border-box; - opacity: ${opacity}; - pointer-events: none; - `; - - // Add badge with grid_id and label - let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`; - // Add dominant indicator if this is the dominant group - if (grid.is_dominant) { - labelText = `⭐ ${labelText} (dominant)`; - } - const badge = document.createElement('span'); - badge.textContent = labelText; - 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 indicator if target - 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] Grid overlay shown for ${grids.length} grids`); - - // Auto-remove after 5 seconds - overlayTimeout = setTimeout(() => { - removeOverlay(); - console.log('[Sentience Bridge] Grid 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); - -})(); +}(); \ No newline at end of file diff --git a/sentience/extension/injected_api.js b/sentience/extension/injected_api.js index e6ff2f9..12ad84b 100644 --- a/sentience/extension/injected_api.js +++ b/sentience/extension/injected_api.js @@ -1,2749 +1,1244 @@ -// Sentience Chrome Extension - Injected API -// Auto-generated from modular source -(function () { - 'use strict'; - - // utils.js - Helper Functions (CSP-Resistant) - // All utility functions needed for DOM data collection - - // --- HELPER: Deep Walker with Native Filter --- - function getAllElements(root = document) { - const elements = []; - const filter = { - acceptNode(node) { - // Skip metadata and script/style tags - if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD'].includes(node.tagName)) { - return NodeFilter.FILTER_REJECT; +!function() { + "use strict"; + function getAllElements(root = document) { + const elements = [], filter = { + acceptNode: node => [ "SCRIPT", "STYLE", "NOSCRIPT", "META", "LINK", "HEAD" ].includes(node.tagName) || node.parentNode && "SVG" === node.parentNode.tagName && "SVG" !== node.tagName ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT + }, walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); + for (;walker.nextNode(); ) { + const node = walker.currentNode; + node.isConnected && (elements.push(node), node.shadowRoot && elements.push(...getAllElements(node.shadowRoot))); } - // 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; - if (node.isConnected) { - elements.push(node); - if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot)); - } - } - return elements; - } - - // ============================================================================ - // CAPTCHA DETECTION (detection only, no solving/bypass logic) - // ============================================================================ - - const CAPTCHA_DETECTED_THRESHOLD = 0.7; - const CAPTCHA_MAX_EVIDENCE = 5; - const CAPTCHA_TEXT_MAX_LEN = 2000; - - const CAPTCHA_TEXT_KEYWORDS = [ - 'verify you are human', - 'captcha', - 'human verification', - 'unusual traffic', - 'are you a robot', - 'security check', - 'prove you are human', - 'bot detection', - 'automated access', - ]; - - const CAPTCHA_URL_HINTS = ['captcha', 'challenge', 'verify']; - - const CAPTCHA_IFRAME_HINTS = { - recaptcha: ['recaptcha', 'google.com/recaptcha'], - hcaptcha: ['hcaptcha.com'], - turnstile: ['challenges.cloudflare.com', 'turnstile'], - arkose: ['arkoselabs.com', 'funcaptcha.com', 'client-api.arkoselabs.com'], - awswaf: ['amazonaws.com/captcha', 'awswaf.com'], - }; - - const CAPTCHA_SCRIPT_HINTS = { - recaptcha: ['recaptcha'], - hcaptcha: ['hcaptcha'], - turnstile: ['turnstile', 'challenges.cloudflare.com'], - arkose: ['arkoselabs', 'funcaptcha'], - awswaf: ['captcha.awswaf', 'awswaf-captcha'], - }; - - const CAPTCHA_CONTAINER_SELECTORS = [ - // reCAPTCHA - { selector: '.g-recaptcha', provider: 'recaptcha' }, - { selector: '#g-recaptcha', provider: 'recaptcha' }, - { selector: '[data-sitekey]', provider: 'unknown' }, - { selector: 'iframe[title*="recaptcha" i]', provider: 'recaptcha' }, - // hCaptcha - { selector: '.h-captcha', provider: 'hcaptcha' }, - { selector: '#h-captcha', provider: 'hcaptcha' }, - { selector: 'iframe[title*="hcaptcha" i]', provider: 'hcaptcha' }, - // Cloudflare Turnstile - { selector: '.cf-turnstile', provider: 'turnstile' }, - { selector: '[data-cf-turnstile-sitekey]', provider: 'turnstile' }, - { selector: 'iframe[src*="challenges.cloudflare.com"]', provider: 'turnstile' }, - // Arkose Labs / FunCaptcha - { selector: '#FunCaptcha', provider: 'arkose' }, - { selector: '.funcaptcha', provider: 'arkose' }, - { selector: '[data-arkose-public-key]', provider: 'arkose' }, - { selector: 'iframe[src*="arkoselabs"]', provider: 'arkose' }, - // AWS WAF CAPTCHA - { selector: '#captcha-container', provider: 'awswaf' }, - { selector: '[data-awswaf-captcha]', provider: 'awswaf' }, - // Generic - { selector: 'iframe[title*="captcha" i]', provider: 'unknown' }, - ]; - - function addEvidence(list, value) { - if (!value) return; - if (list.length >= CAPTCHA_MAX_EVIDENCE) return; - list.push(value); - } - - function truncateText(text, maxLen) { - if (!text) return ''; - if (text.length <= maxLen) return text; - return text.slice(0, maxLen); - } - - function collectVisibleTextSnippet() { - try { - const candidates = document.querySelectorAll( - 'h1, h2, h3, h4, p, label, button, form, div, span' - ); - let combined = ''; - let count = 0; - for (const node of candidates) { - if (count >= 30 || combined.length >= CAPTCHA_TEXT_MAX_LEN) break; - if (!node || typeof node.innerText !== 'string') continue; - if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue; - const text = node.innerText.replace(/\s+/g, ' ').trim(); - if (!text) continue; - combined += `${text} `; - count += 1; - } - combined = combined.trim(); - if (combined) { - return truncateText(combined, CAPTCHA_TEXT_MAX_LEN); - } - } catch (e) { - // ignore - } - - try { - let bodyText = document.body?.innerText || ''; - if (!bodyText && document.body?.textContent) { - bodyText = document.body.textContent; - } - return truncateText(bodyText.replace(/\s+/g, ' ').trim(), CAPTCHA_TEXT_MAX_LEN); - } catch (e) { - return ''; - } - } - - function matchHints(value, hints) { - const lower = String(value || '').toLowerCase(); - if (!lower) return false; - return hints.some((hint) => lower.includes(hint)); - } - - function detectCaptcha() { - const evidence = { - text_hits: [], - selector_hits: [], - iframe_src_hits: [], - url_hits: [], - }; - - let hasIframeHit = false; - let hasContainerHit = false; - let hasScriptHit = false; - let hasKeywordHit = false; - let hasUrlHit = false; - - const providerSignals = { - recaptcha: 0, - hcaptcha: 0, - turnstile: 0, - arkose: 0, - awswaf: 0, - }; - - // Iframe hints (strongest signal) - try { - const iframes = document.querySelectorAll('iframe'); - for (const iframe of iframes) { - const src = iframe.getAttribute('src') || ''; - const title = iframe.getAttribute('title') || ''; - if (src) { - for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) { - if (matchHints(src, hints)) { - hasIframeHit = true; - providerSignals[provider] += 1; - addEvidence(evidence.iframe_src_hits, truncateText(src, 120)); + return elements; + } + const CAPTCHA_TEXT_KEYWORDS = [ "verify you are human", "captcha", "human verification", "unusual traffic", "are you a robot", "security check", "prove you are human", "bot detection", "automated access" ], CAPTCHA_URL_HINTS = [ "captcha", "challenge", "verify" ], CAPTCHA_IFRAME_HINTS = { + recaptcha: [ "recaptcha", "google.com/recaptcha" ], + hcaptcha: [ "hcaptcha.com" ], + turnstile: [ "challenges.cloudflare.com", "turnstile" ], + arkose: [ "arkoselabs.com", "funcaptcha.com", "client-api.arkoselabs.com" ], + awswaf: [ "amazonaws.com/captcha", "awswaf.com" ] + }, CAPTCHA_SCRIPT_HINTS = { + recaptcha: [ "recaptcha" ], + hcaptcha: [ "hcaptcha" ], + turnstile: [ "turnstile", "challenges.cloudflare.com" ], + arkose: [ "arkoselabs", "funcaptcha" ], + awswaf: [ "captcha.awswaf", "awswaf-captcha" ] + }, CAPTCHA_CONTAINER_SELECTORS = [ { + selector: ".g-recaptcha", + provider: "recaptcha" + }, { + selector: "#g-recaptcha", + provider: "recaptcha" + }, { + selector: "[data-sitekey]", + provider: "unknown" + }, { + selector: 'iframe[title*="recaptcha" i]', + provider: "recaptcha" + }, { + selector: ".h-captcha", + provider: "hcaptcha" + }, { + selector: "#h-captcha", + provider: "hcaptcha" + }, { + selector: 'iframe[title*="hcaptcha" i]', + provider: "hcaptcha" + }, { + selector: ".cf-turnstile", + provider: "turnstile" + }, { + selector: "[data-cf-turnstile-sitekey]", + provider: "turnstile" + }, { + selector: 'iframe[src*="challenges.cloudflare.com"]', + provider: "turnstile" + }, { + selector: "#FunCaptcha", + provider: "arkose" + }, { + selector: ".funcaptcha", + provider: "arkose" + }, { + selector: "[data-arkose-public-key]", + provider: "arkose" + }, { + selector: 'iframe[src*="arkoselabs"]', + provider: "arkose" + }, { + selector: "#captcha-container", + provider: "awswaf" + }, { + selector: "[data-awswaf-captcha]", + provider: "awswaf" + }, { + selector: 'iframe[title*="captcha" i]', + provider: "unknown" + } ]; + function addEvidence(list, value) { + value && (list.length >= 5 || list.push(value)); + } + function truncateText(text, maxLen) { + return text ? text.length <= maxLen ? text : text.slice(0, maxLen) : ""; + } + function matchHints(value, hints) { + const lower = String(value || "").toLowerCase(); + return !!lower && hints.some(hint => lower.includes(hint)); + } + function detectCaptcha() { + const evidence = { + text_hits: [], + selector_hits: [], + iframe_src_hits: [], + url_hits: [] + }; + let hasIframeHit = !1, hasContainerHit = !1, hasScriptHit = !1, hasKeywordHit = !1, hasUrlHit = !1; + const providerSignals = { + recaptcha: 0, + hcaptcha: 0, + turnstile: 0, + arkose: 0, + awswaf: 0 + }; + try { + const iframes = document.querySelectorAll("iframe"); + for (const iframe of iframes) { + const src = iframe.getAttribute("src") || "", title = iframe.getAttribute("title") || ""; + if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0, + providerSignals[provider] += 1, addEvidence(evidence.iframe_src_hits, truncateText(src, 120))); + if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0, + addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]')), evidence.iframe_src_hits.length >= 5) break; } - } - } - if (title && matchHints(title, ['captcha', 'recaptcha'])) { - hasContainerHit = true; - addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]'); - } - if (evidence.iframe_src_hits.length >= CAPTCHA_MAX_EVIDENCE) break; - } - } catch (e) { - // ignore - } - - // Script hints - try { - const scripts = document.querySelectorAll('script[src]'); - for (const script of scripts) { - const src = script.getAttribute('src') || ''; - if (!src) continue; - for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) { - if (matchHints(src, hints)) { - hasScriptHit = true; - providerSignals[provider] += 1; - addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`); - } - } - if (evidence.selector_hits.length >= CAPTCHA_MAX_EVIDENCE) break; - } - } catch (e) { - // ignore - } - - // Container selectors - for (const { selector, provider } of CAPTCHA_CONTAINER_SELECTORS) { - try { - const hit = document.querySelector(selector); - if (hit) { - hasContainerHit = true; - addEvidence(evidence.selector_hits, selector); - if (provider !== 'unknown') { - providerSignals[provider] += 1; - } - } - } catch (e) { - // ignore invalid selectors - } - } - - // Text keyword hints - const textSnippet = collectVisibleTextSnippet(); - if (textSnippet) { - const lowerText = textSnippet.toLowerCase(); - for (const keyword of CAPTCHA_TEXT_KEYWORDS) { - if (lowerText.includes(keyword)) { - hasKeywordHit = true; - addEvidence(evidence.text_hits, keyword); + } catch (e) {} + try { + const scripts = document.querySelectorAll("script[src]"); + for (const script of scripts) { + const src = script.getAttribute("src") || ""; + if (src) { + for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) matchHints(src, hints) && (hasScriptHit = !0, + providerSignals[provider] += 1, addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`)); + if (evidence.selector_hits.length >= 5) break; + } + } + } catch (e) {} + for (const {selector: selector, provider: provider} of CAPTCHA_CONTAINER_SELECTORS) try { + document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector), + "unknown" !== provider && (providerSignals[provider] += 1)); + } catch (e) {} + const textSnippet = function() { + try { + const candidates = document.querySelectorAll("h1, h2, h3, h4, p, label, button, form, div, span"); + let combined = "", count = 0; + for (const node of candidates) { + if (count >= 30 || combined.length >= 2e3) break; + if (!node || "string" != typeof node.innerText) continue; + if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue; + const text = node.innerText.replace(/\s+/g, " ").trim(); + text && (combined += `${text} `, count += 1); + } + if (combined = combined.trim(), combined) return truncateText(combined, 2e3); + } catch (e) {} + try { + let bodyText = document.body?.innerText || ""; + return !bodyText && document.body?.textContent && (bodyText = document.body.textContent), + truncateText(bodyText.replace(/\s+/g, " ").trim(), 2e3); + } catch (e) { + return ""; + } + }(); + if (textSnippet) { + const lowerText = textSnippet.toLowerCase(); + for (const keyword of CAPTCHA_TEXT_KEYWORDS) lowerText.includes(keyword) && (hasKeywordHit = !0, + addEvidence(evidence.text_hits, keyword)); } - } + try { + const lowerUrl = (window.location?.href || "").toLowerCase(); + for (const hint of CAPTCHA_URL_HINTS) lowerUrl.includes(hint) && (hasUrlHit = !0, + addEvidence(evidence.url_hits, hint)); + } catch (e) {} + let confidence = 0; + hasIframeHit && (confidence += .7), hasContainerHit && (confidence += .5), hasScriptHit && (confidence += .5), + hasKeywordHit && (confidence += .3), hasUrlHit && (confidence += .2), confidence = Math.min(1, confidence), + hasIframeHit && (confidence = Math.max(confidence, .8)), !hasKeywordHit || hasIframeHit || hasContainerHit || hasScriptHit || hasUrlHit || (confidence = Math.min(confidence, .4)); + const detected = confidence >= .7; + let providerHint = null; + return providerSignals.recaptcha > 0 ? providerHint = "recaptcha" : providerSignals.hcaptcha > 0 ? providerHint = "hcaptcha" : providerSignals.turnstile > 0 ? providerHint = "turnstile" : providerSignals.arkose > 0 ? providerHint = "arkose" : providerSignals.awswaf > 0 ? providerHint = "awswaf" : detected && (providerHint = "unknown"), + { + detected: detected, + provider_hint: providerHint, + confidence: confidence, + evidence: evidence + }; } - - // URL hints - try { - const url = window.location?.href || ''; - const lowerUrl = url.toLowerCase(); - for (const hint of CAPTCHA_URL_HINTS) { - if (lowerUrl.includes(hint)) { - hasUrlHit = true; - addEvidence(evidence.url_hits, hint); + const DEFAULT_INFERENCE_CONFIG = { + allowedTags: [ "label", "span", "div" ], + allowedRoles: [], + allowedClassPatterns: [], + maxParentDepth: 2, + maxSiblingDistance: 1, + requireSameContainer: !0, + containerTags: [ "form", "fieldset", "div" ], + methods: { + explicitLabel: !0, + ariaLabelledby: !0, + parentTraversal: !0, + siblingProximity: !0 } - } - } catch (e) { - // ignore - } - - // Confidence scoring - let confidence = 0.0; - if (hasIframeHit) confidence += 0.7; - if (hasContainerHit) confidence += 0.5; - if (hasScriptHit) confidence += 0.5; - if (hasKeywordHit) confidence += 0.3; - if (hasUrlHit) confidence += 0.2; - confidence = Math.min(1.0, confidence); - - if (hasIframeHit) { - confidence = Math.max(confidence, 0.8); - } - - if (hasKeywordHit && !hasIframeHit && !hasContainerHit && !hasScriptHit && !hasUrlHit) { - confidence = Math.min(confidence, 0.4); - } - - const detected = confidence >= CAPTCHA_DETECTED_THRESHOLD; - - let providerHint = null; - if (providerSignals.recaptcha > 0) { - providerHint = 'recaptcha'; - } else if (providerSignals.hcaptcha > 0) { - providerHint = 'hcaptcha'; - } else if (providerSignals.turnstile > 0) { - providerHint = 'turnstile'; - } else if (providerSignals.arkose > 0) { - providerHint = 'arkose'; - } else if (providerSignals.awswaf > 0) { - providerHint = 'awswaf'; - } else if (detected) { - providerHint = 'unknown'; - } - - return { - detected, - provider_hint: providerHint, - confidence, - evidence, - }; - } - - // ============================================================================ - // LABEL INFERENCE SYSTEM - // ============================================================================ - - // Default inference configuration (conservative - Stage 1 equivalent) - const DEFAULT_INFERENCE_CONFIG = { - // Allowed tag names that can be inference sources - allowedTags: ['label', 'span', 'div'], - - // Allowed ARIA roles that can be inference sources - allowedRoles: [], - - // Class name patterns (substring match, case-insensitive) - allowedClassPatterns: [], - - // DOM tree traversal limits - maxParentDepth: 2, // Max 2 levels up DOM tree - maxSiblingDistance: 1, // Only immediate previous/next sibling - - // Container requirements (no distance-based checks) - requireSameContainer: true, // Must share common parent - containerTags: ['form', 'fieldset', 'div'], - - // Enable/disable specific inference methods - methods: { - explicitLabel: true, // el.labels API - ariaLabelledby: true, // aria-labelledby attribute - parentTraversal: true, // Check parent/grandparent - siblingProximity: true, // Check preceding sibling (same container only) - }, - }; - - // Merge user config with defaults - function mergeInferenceConfig(userConfig = {}) { - return { - ...DEFAULT_INFERENCE_CONFIG, - ...userConfig, - methods: { - ...DEFAULT_INFERENCE_CONFIG.methods, - ...(userConfig.methods || {}), - }, }; - } - - // Check if element matches inference source criteria - function isInferenceSource(el, config) { - if (!el || !el.tagName) return false; - - const tag = el.tagName.toLowerCase(); - const role = el.getAttribute ? el.getAttribute('role') : ''; - const className = ((el.className || '') + '').toLowerCase(); - - // Check tag name - if (config.allowedTags.includes(tag)) { - return true; - } - - // Check role - if (config.allowedRoles.length > 0 && role && config.allowedRoles.includes(role)) { - return true; - } - - // Check class patterns - if (config.allowedClassPatterns.length > 0) { - for (const pattern of config.allowedClassPatterns) { - if (className.includes(pattern.toLowerCase())) { - return true; - } - } - } - - return false; - } - - // Helper: Find common parent element - function findCommonParent(el1, el2) { - if (!el1 || !el2) return null; - - // Get document reference safely for stopping conditions - // eslint-disable-next-line no-undef - const doc = - (typeof global !== 'undefined' && global.document) || - (typeof window !== 'undefined' && window.document) || - (typeof document !== 'undefined' && document) || - null; - - const parents1 = []; - let current = el1; - // Collect all parents (including el1 itself) - while (current) { - parents1.push(current); - // Stop if no parent - if (!current.parentElement) { - break; - } - // Stop at body or documentElement if they exist - if (doc && (current === doc.body || current === doc.documentElement)) { - break; - } - current = current.parentElement; - } - - // Check if el2 or any of its parents are in parents1 - current = el2; - while (current) { - // Use indexOf for more reliable comparison (handles object identity) - if (parents1.indexOf(current) !== -1) { - return current; - } - // Stop if no parent - if (!current.parentElement) { - break; - } - // Stop at body or documentElement if they exist - if (doc && (current === doc.body || current === doc.documentElement)) { - break; - } - current = current.parentElement; - } - - return null; - } - - // Helper: Check if element is a valid container - function isValidContainer(el, validTags) { - if (!el || !el.tagName) return false; - const tag = el.tagName.toLowerCase(); - // Handle both string and object className - let className = ''; - try { - className = (el.className || '') + ''; - } catch (e) { - className = ''; - } - return ( - validTags.includes(tag) || - className.toLowerCase().includes('form') || - className.toLowerCase().includes('field') - ); - } - - // Helper: Check container requirements (no distance-based checks) - function isInSameValidContainer(element, candidate, limits) { - if (!element || !candidate) return false; - - // Check same container requirement - if (limits.requireSameContainer) { - const commonParent = findCommonParent(element, candidate); - if (!commonParent) { - return false; - } - // Check if common parent is a valid container - if (!isValidContainer(commonParent, limits.containerTags)) { - return false; - } - } - - return true; - } - - // Main inference function - function getInferredLabel(el, options = {}) { - if (!el) return null; - - const { - enableInference = true, - inferenceConfig = {}, // User-provided config, merged with defaults - } = options; - - if (!enableInference) return null; - - // OPTIMIZATION: If element already has text or aria-label, skip inference entirely - // Check this BEFORE checking labels, so we don't infer if element already has text - // Note: For INPUT elements, we check value/placeholder, not innerText - // For IMG elements, we check alt, not innerText - // For other elements, innerText is considered explicit text - const ariaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; - const hasAriaLabel = ariaLabel && ariaLabel.trim(); - const hasInputValue = el.tagName === 'INPUT' && (el.value || el.placeholder); - const hasImgAlt = el.tagName === 'IMG' && el.alt; - // For non-input/img elements, check innerText - but only if it's not empty - // Access innerText safely - it might be a getter or property - let innerTextValue = ''; - try { - innerTextValue = el.innerText || ''; - } catch (e) { - // If innerText access fails, treat as empty - innerTextValue = ''; - } - const hasInnerText = - el.tagName !== 'INPUT' && el.tagName !== 'IMG' && innerTextValue && innerTextValue.trim(); - - if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) { - return null; - } - - // Merge config with defaults - const config = mergeInferenceConfig(inferenceConfig); - - // Method 1: Explicit label association (el.labels API) - if (config.methods.explicitLabel && el.labels && el.labels.length > 0) { - const label = el.labels[0]; - if (isInferenceSource(label, config)) { - const text = (label.innerText || '').trim(); - if (text) { - return { - text, - source: 'explicit_label', - }; - } - } - } - - // Method 2: aria-labelledby (supports space-separated list of IDs) - // NOTE: aria-labelledby is an EXPLICIT reference, so it should work with ANY element - // regardless of inference source criteria. The config only controls whether this method runs. - if (config.methods.ariaLabelledby && el.hasAttribute && el.hasAttribute('aria-labelledby')) { - const labelIdsAttr = el.getAttribute('aria-labelledby'); - if (labelIdsAttr) { - // Split by whitespace to support multiple IDs (space-separated list) - const labelIds = labelIdsAttr.split(/\s+/).filter((id) => id.trim()); - const labelTexts = []; - - // Helper function to get document.getElementById from available contexts - const getDocument = () => { - // eslint-disable-next-line no-undef - if (typeof global !== 'undefined' && global.document) { - // eslint-disable-next-line no-undef - return global.document; - } - if (typeof window !== 'undefined' && window.document) { - return window.document; - } - if (typeof document !== 'undefined') { - return document; - } - return null; - }; - - const doc = getDocument(); - if (!doc || !doc.getElementById) ; else { - // Process each ID in the space-separated list - for (const labelId of labelIds) { - if (!labelId.trim()) continue; - - let labelEl = null; - try { - labelEl = doc.getElementById(labelId); - } catch (e) { - // If getElementById fails, skip this ID - continue; - } - - // aria-labelledby is an explicit reference - use ANY element, not just those matching inference criteria - // This follows ARIA spec: aria-labelledby can reference any element in the document - if (labelEl) { - // Extract text from the referenced element - let text = ''; - try { - // Try innerText first (preferred for visible text) - text = (labelEl.innerText || '').trim(); - // Fallback to textContent if innerText is empty - if (!text && labelEl.textContent) { - text = labelEl.textContent.trim(); + function isInferenceSource(el, config) { + if (!el || !el.tagName) return !1; + const tag = el.tagName.toLowerCase(), role = el.getAttribute ? el.getAttribute("role") : "", className = ((el.className || "") + "").toLowerCase(); + if (config.allowedTags.includes(tag)) return !0; + if (config.allowedRoles.length > 0 && role && config.allowedRoles.includes(role)) return !0; + if (config.allowedClassPatterns.length > 0) for (const pattern of config.allowedClassPatterns) if (className.includes(pattern.toLowerCase())) return !0; + return !1; + } + function isInSameValidContainer(element, candidate, limits) { + if (!element || !candidate) return !1; + if (limits.requireSameContainer) { + const commonParent = function(el1, el2) { + if (!el1 || !el2) return null; + const doc = "undefined" != typeof global && global.document || "undefined" != typeof window && window.document || "undefined" != typeof document && document || null, parents1 = []; + let current = el1; + for (;current && (parents1.push(current), current.parentElement) && (!doc || current !== doc.body && current !== doc.documentElement); ) current = current.parentElement; + for (current = el2; current; ) { + if (-1 !== parents1.indexOf(current)) return current; + if (!current.parentElement) break; + if (doc && (current === doc.body || current === doc.documentElement)) break; + current = current.parentElement; } - // Fallback to aria-label if available - if (!text && labelEl.getAttribute) { - const ariaLabel = labelEl.getAttribute('aria-label'); - if (ariaLabel) { - text = ariaLabel.trim(); - } + return null; + }(element, candidate); + if (!commonParent) return !1; + if (!function(el, validTags) { + if (!el || !el.tagName) return !1; + const tag = el.tagName.toLowerCase(); + let className = ""; + try { + className = (el.className || "") + ""; + } catch (e) { + className = ""; } - } catch (e) { - // If text extraction fails, skip this element - continue; - } - - if (text) { - labelTexts.push(text); - } - } - } + return validTags.includes(tag) || className.toLowerCase().includes("form") || className.toLowerCase().includes("field"); + }(commonParent, limits.containerTags)) return !1; } - - // Combine multiple label texts (space-separated) - if (labelTexts.length > 0) { - return { - text: labelTexts.join(' '), - source: 'aria_labelledby', - }; - } - } - } - - // Method 3: Parent/grandparent traversal - if (config.methods.parentTraversal) { - let parent = el.parentElement; - let depth = 0; - while (parent && depth < config.maxParentDepth) { - if (isInferenceSource(parent, config)) { - const text = (parent.innerText || '').trim(); - if (text) { - return { - text, - source: 'parent_label', - }; - } + return !0; + } + function getInferredLabel(el, options = {}) { + if (!el) return null; + const {enableInference: enableInference = !0, inferenceConfig: inferenceConfig = {}} = options; + if (!enableInference) return null; + const ariaLabel = el.getAttribute ? el.getAttribute("aria-label") : null, hasAriaLabel = ariaLabel && ariaLabel.trim(), hasInputValue = "INPUT" === el.tagName && (el.value || el.placeholder), hasImgAlt = "IMG" === el.tagName && el.alt; + let innerTextValue = ""; + try { + innerTextValue = el.innerText || ""; + } catch (e) { + innerTextValue = ""; } - parent = parent.parentElement; - depth++; - } - } - - // Method 4: Preceding sibling (no distance-based checks, only DOM structure) - if (config.methods.siblingProximity) { - const prev = el.previousElementSibling; - if (prev && isInferenceSource(prev, config)) { - // Only check if they're in the same valid container (no pixel distance) - if ( - isInSameValidContainer(el, prev, { - requireSameContainer: config.requireSameContainer, - containerTags: config.containerTags, - }) - ) { - const text = (prev.innerText || '').trim(); - if (text) { + const hasInnerText = "INPUT" !== el.tagName && "IMG" !== el.tagName && innerTextValue && innerTextValue.trim(); + if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) return null; + const config = function(userConfig = {}) { return { - text, - source: 'sibling_label', + ...DEFAULT_INFERENCE_CONFIG, + ...userConfig, + methods: { + ...DEFAULT_INFERENCE_CONFIG.methods, + ...userConfig.methods || {} + } }; - } + }(inferenceConfig); + if (config.methods.explicitLabel && el.labels && el.labels.length > 0) { + const label = el.labels[0]; + if (isInferenceSource(label, config)) { + const text = (label.innerText || "").trim(); + if (text) return { + text: text, + source: "explicit_label" + }; + } } - } - } - - return null; - } - - // --- HELPER: Nearby Static Text (cheap, best-effort) --- - // Returns a short, single-line snippet near the element (sibling/parent). - function getNearbyText(el, options = {}) { - if (!el) return null; - - const maxLen = typeof options.maxLen === 'number' ? options.maxLen : 80; - const ownText = normalizeNearbyText(el.innerText || ''); - - const candidates = []; - - const collect = (node) => { - if (!node) return; - let text = ''; - try { - text = normalizeNearbyText(node.innerText || node.textContent || ''); - } catch (e) { - text = ''; - } - if (!text || text === ownText) return; - candidates.push(text); - }; - - // Prefer immediate siblings - collect(el.previousElementSibling); - collect(el.nextElementSibling); - - // Fallback: short parent text (avoid large blocks) - if (candidates.length === 0 && el.parentElement) { - let parentText = ''; - try { - parentText = normalizeNearbyText(el.parentElement.innerText || ''); - } catch (e) { - parentText = ''; - } - if (parentText && parentText !== ownText && parentText.length <= 120) { - candidates.push(parentText); - } - } - - if (candidates.length === 0) return null; - - let text = candidates[0]; - if (text.length > maxLen) { - text = text.slice(0, maxLen).trim(); - } - return text || null; - } - - function normalizeNearbyText(text) { - if (!text) return ''; - return text.replace(/\s+/g, ' ').trim(); - } - - // Helper: Check if element is interactable (should have role inferred) - function isInteractableElement(el) { - if (!el || !el.tagName) return false; - - const tag = el.tagName.toLowerCase(); - const role = el.getAttribute ? el.getAttribute('role') : null; - const hasTabIndex = el.hasAttribute ? el.hasAttribute('tabindex') : false; - const hasHref = el.tagName === 'A' && (el.hasAttribute ? el.hasAttribute('href') : false); - - // Native interactive elements - const interactiveTags = [ - 'button', - 'input', - 'textarea', - 'select', - 'option', - 'details', - 'summary', - 'a', - ]; - if (interactiveTags.includes(tag)) { - // For , only if it has href - if (tag === 'a' && !hasHref) return false; - return true; - } - - // Elements with explicit interactive roles - const interactiveRoles = [ - 'button', - 'link', - 'tab', - 'menuitem', - 'checkbox', - 'radio', - 'switch', - 'slider', - 'combobox', - 'textbox', - 'searchbox', - 'spinbutton', - ]; - if (role && interactiveRoles.includes(role.toLowerCase())) { - return true; - } - - // Focusable elements (tabindex makes them interactive) - if (hasTabIndex) { - return true; - } - - // Elements with event handlers (custom interactive elements) - if (el.onclick || el.onkeydown || el.onkeypress || el.onkeyup) { - return true; - } - - // Check for inline event handlers via attributes - if (el.getAttribute) { - const hasInlineHandler = - el.getAttribute('onclick') || - el.getAttribute('onkeydown') || - el.getAttribute('onkeypress') || - el.getAttribute('onkeyup'); - if (hasInlineHandler) { - return true; - } - } - - return false; - } - - // Helper: Infer ARIA role for interactable elements - function getInferredRole(el, options = {}) { - const { - enableInference = true, - // inferenceConfig reserved for future extensibility - } = options; - - if (!enableInference) return null; - - // Only infer roles for interactable elements - if (!isInteractableElement(el)) { - return null; - } - - // CRITICAL: Only infer if element has NO aria-label AND NO explicit role - const hasAriaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; - const hasExplicitRole = el.getAttribute ? el.getAttribute('role') : null; - - if (hasAriaLabel || hasExplicitRole) { - return null; // Skip inference if element already has aria-label or role - } - - // If element is native semantic HTML, it already has a role - const tag = el.tagName.toLowerCase(); - const semanticTags = ['button', 'a', 'input', 'textarea', 'select', 'option']; - if (semanticTags.includes(tag)) { - return null; // Native HTML already has role - } - - // Infer role based on element behavior or context - // Check for click handlers first (most common) - if (el.onclick || (el.getAttribute && el.getAttribute('onclick'))) { - return 'button'; - } - - // Check for keyboard handlers - if ( - el.onkeydown || - el.onkeypress || - el.onkeyup || - (el.getAttribute && - (el.getAttribute('onkeydown') || el.getAttribute('onkeypress') || el.getAttribute('onkeyup'))) - ) { - return 'button'; // Default to button for keyboard-interactive elements - } - - // Focusable div/span likely needs a role - if (el.hasAttribute && el.hasAttribute('tabindex') && (tag === 'div' || tag === 'span')) { - return 'button'; // Default assumption for focusable elements - } - - return null; - } - - // --- HELPER: Smart Text Extractor --- - function getText(el) { - if (el.getAttribute('aria-label')) return el.getAttribute('aria-label'); - if (el.tagName === 'INPUT') { - // Privacy: never return password values - const t = (el.getAttribute && el.getAttribute('type')) || el.type || ''; - if (String(t).toLowerCase() === 'password') { - return el.placeholder || ''; - } - return el.value || el.placeholder || ''; - } - if (el.tagName === 'IMG') return el.alt || ''; - return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100); - } - - // Best-effort accessible name extraction for controls (used for v1 state-aware assertions) - function getAccessibleName(el) { - if (!el || !el.getAttribute) return ''; - - // 1) aria-label - const ariaLabel = el.getAttribute('aria-label'); - if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim().substring(0, 200); - - // 2) aria-labelledby (space-separated IDs) - const labelledBy = el.getAttribute('aria-labelledby'); - if (labelledBy && labelledBy.trim()) { - const ids = labelledBy.split(/\s+/).filter((id) => id.trim()); - const texts = []; - for (const id of ids) { - try { - const ref = document.getElementById(id); - if (!ref) continue; - const txt = (ref.innerText || ref.textContent || ref.getAttribute?.('aria-label') || '') - .toString() - .trim(); - if (txt) texts.push(txt); - } catch (e) { - // ignore + if (config.methods.ariaLabelledby && el.hasAttribute && el.hasAttribute("aria-labelledby")) { + const labelIdsAttr = el.getAttribute("aria-labelledby"); + if (labelIdsAttr) { + const labelIds = labelIdsAttr.split(/\s+/).filter(id => id.trim()), labelTexts = [], doc = (() => "undefined" != typeof global && global.document ? global.document : "undefined" != typeof window && window.document ? window.document : "undefined" != typeof document ? document : null)(); + if (doc && doc.getElementById) for (const labelId of labelIds) { + if (!labelId.trim()) continue; + let labelEl = null; + try { + labelEl = doc.getElementById(labelId); + } catch (e) { + continue; + } + if (labelEl) { + let text = ""; + try { + if (text = (labelEl.innerText || "").trim(), !text && labelEl.textContent && (text = labelEl.textContent.trim()), + !text && labelEl.getAttribute) { + const ariaLabel = labelEl.getAttribute("aria-label"); + ariaLabel && (text = ariaLabel.trim()); + } + } catch (e) { + continue; + } + text && labelTexts.push(text); + } + } else ; + if (labelTexts.length > 0) return { + text: labelTexts.join(" "), + source: "aria_labelledby" + }; + } } - } - if (texts.length > 0) return texts.join(' ').substring(0, 200); - } - - // 3) has the text - // Case 2: Spans that wrap links (parent spans like HN's "titleline") - child is the actionable element - // This significantly reduces element count on link-heavy pages (HN, Reddit, search results) - const tagName = el.tagName.toLowerCase(); - if (tagName === 'span') { - // Case 1: Span is inside a link (any ancestor ) - if (el.closest('a')) { - return; // Skip - parent link has the content - } - // Case 2: Span contains a link as ANY descendant (wrapper span) - // HN structure: Title... - // Also handles: ... - const childLink = el.querySelector('a[href]'); // Find ANY descendant link with href - if (childLink && childLink.href) { - return; // Skip - descendant link is the actionable element - } - // Debug: Log spans with "titleline" class that weren't filtered - if (options.debug && el.className && el.className.includes('titleline')) { - console.log('[SentienceAPI] DEBUG: titleline span NOT filtered', { - className: el.className, - text: el.textContent?.slice(0, 50), - childLink: childLink, - hasChildHref: childLink?.href, - }); - } } - - window.sentience_registry[idx] = el; - - // Input type is needed for safe value redaction (passwords) and state-aware assertions - const inputType = - tagName === 'input' - ? toSafeString((el.getAttribute && el.getAttribute('type')) || el.type || null) - : null; - const isPasswordInput = inputType && inputType.toLowerCase() === 'password'; - - // Use getSemanticText for inference support (falls back to getText if no inference) - const semanticText = getSemanticText(el, { - enableInference: options.enableInference !== false, // Default: true - inferenceConfig: options.inferenceConfig, // Pass configurable inference settings + invisibleSelectors.forEach(selector => { + try { + clone.querySelectorAll(selector).forEach(el => { + el.parentNode && el.parentNode.removeChild(el); + }); + } catch (e) {} }); - const textVal = semanticText.text || getText(el); // Fallback to getText for backward compat - - // Infer role for interactable elements (only if no aria-label and no explicit role) - const inferredRole = getInferredRole(el, { - enableInference: options.enableInference !== false, - inferenceConfig: options.inferenceConfig, + clone.querySelectorAll("a[href]").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) {} }); - 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); - - // Safe value extraction (PII-aware) - let safeValue = null; - let valueRedacted = null; - try { - if (el.value !== undefined || (el.getAttribute && el.getAttribute('value') !== null)) { - if (isPasswordInput) { - safeValue = null; - valueRedacted = 'true'; - } else { - const rawValue = - el.value !== undefined ? String(el.value) : String(el.getAttribute('value')); - safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue; - valueRedacted = 'false'; - } - } - } catch (e) { - // ignore + return clone.querySelectorAll("img[src]").forEach(img => { + const src = img.getAttribute("src"); + if (src && !src.startsWith("http://") && !src.startsWith("https://") && !src.startsWith("data:")) try { + img.setAttribute("src", new URL(src, document.baseURI).href); + } catch (e) {} + }), clone.innerHTML; + } + function cleanElement(obj) { + if (Array.isArray(obj)) return obj.map(cleanElement); + if (null !== obj && "object" == typeof obj) { + const cleaned = {}; + for (const [key, value] of Object.entries(obj)) if (null != value) if ("object" == typeof value) { + const deepClean = cleanElement(value); + Object.keys(deepClean).length > 0 && (cleaned[key] = deepClean); + } else cleaned[key] = value; + return cleaned; } - - // Best-effort accessible name (label-like, not the typed value) - const accessibleName = toSafeString(getAccessibleName(el) || null); - - const nearbyText = isInteractableElement(el) ? getNearbyText(el, { maxLen: 80 }) : null; - - rawData.push({ - id: idx, - tag: tagName, - 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')), - input_type: inputType, - aria_label: - semanticText?.source === 'explicit_aria_label' - ? semanticText.text - : toSafeString(el.getAttribute('aria-label')), // Keep original for backward compat - name: accessibleName, - inferred_label: - semanticText?.source && - !['explicit_aria_label', 'input_value', 'img_alt', 'inner_text'].includes( - semanticText.source - ) - ? toSafeString(semanticText.text) - : null, - label_source: semanticText?.source || null, // Track source for gateway - inferred_role: inferredRole ? toSafeString(inferredRole) : null, // Inferred role for interactable elements - nearby_text: toSafeString(nearbyText), - // Get href: check element first, then traverse up to find parent link - // This ensures nested spans inside links inherit the href - href: toSafeString( - el.href || el.getAttribute('href') || (el.closest && el.closest('a')?.href) || null - ), - class: toSafeString(getClassName(el)), - // Capture dynamic input state (not just initial attributes) - value: safeValue !== null ? toSafeString(safeValue) : null, - value_redacted: valueRedacted, - checked: el.checked !== undefined ? String(el.checked) : null, - disabled: el.disabled !== undefined ? String(el.disabled) : null, - aria_checked: toSafeString(el.getAttribute('aria-checked')), - aria_disabled: toSafeString(el.getAttribute('aria-disabled')), - aria_expanded: toSafeString(el.getAttribute('aria-expanded')), - }, - text: toSafeString(textVal), - in_viewport: inView, - is_occluded: occluded, - // Phase 1: Pass scroll position for doc_y computation in WASM - scroll_y: window.scrollY, - }); - }); - - console.log(`[SentienceAPI] Collected ${rawData.length} elements from main frame`); - - // 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) - const allRawElements = [...rawData]; // Start with main frame elements - let totalIframeElements = 0; - - if (options.collectIframes !== false) { + return obj; + } + async function snapshot(options = {}) { 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; + !1 !== options.waitForStability && await async function(options = {}) { + const {minNodeCount: minNodeCount = 500, quietPeriod: quietPeriod = 200, maxWait: maxWait = 5e3} = options, startTime = Date.now(); try { - // Try to access contentWindow to check if same-origin - isSameOrigin = iframeEl.contentWindow !== null; - } catch (e) { - isSameOrigin = false; + window.__sentience_lastMutationTs = performance.now(); + } catch (e) {} + return new Promise(resolve => { + if (document.querySelectorAll("*").length >= minNodeCount) { + let lastChange = Date.now(); + const observer = new MutationObserver(() => { + lastChange = Date.now(); + try { + window.__sentience_lastMutationTs = performance.now(); + } catch (e) {} + }); + observer.observe(document.body, { + childList: !0, + subtree: !0, + attributes: !1 + }); + const checkStable = () => { + const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; + timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(), + resolve()) : setTimeout(checkStable, 50); + }; + checkStable(); + } else { + const observer = new MutationObserver(() => { + const currentCount = document.querySelectorAll("*").length, totalWait = Date.now() - startTime; + try { + window.__sentience_lastMutationTs = performance.now(); + } catch (e) {} + if (currentCount >= minNodeCount) { + observer.disconnect(); + let lastChange = Date.now(); + const quietObserver = new MutationObserver(() => { + lastChange = Date.now(); + try { + window.__sentience_lastMutationTs = performance.now(); + } catch (e) {} + }); + quietObserver.observe(document.body, { + childList: !0, + subtree: !0, + attributes: !1 + }); + const checkQuiet = () => { + const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; + timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(), + resolve()) : setTimeout(checkQuiet, 50); + }; + checkQuiet(); + } else totalWait >= maxWait && (observer.disconnect(), resolve()); + }); + observer.observe(document.body, { + childList: !0, + subtree: !0, + attributes: !1 + }), setTimeout(() => { + observer.disconnect(), resolve(); + }, maxWait); + } + }); + }(options.waitForStability || {}); + const rawData = []; + window.sentience_registry = []; + getAllElements().forEach((el, idx) => { + if (!el.getBoundingClientRect) return; + const rect = el.getBoundingClientRect(); + if (rect.width < 5 || rect.height < 5) return; + const tagName = el.tagName.toLowerCase(); + if ("span" === tagName) { + if (el.closest("a")) return; + const childLink = el.querySelector("a[href]"); + if (childLink && childLink.href) return; + options.debug && el.className && el.className.includes("titleline"); } - - // 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, + window.sentience_registry[idx] = el; + const inputType = "input" === tagName ? toSafeString(el.getAttribute && el.getAttribute("type") || el.type || null) : null, isPasswordInput = inputType && "password" === inputType.toLowerCase(), semanticText = function(el, options = {}) { + if (!el) return { + text: "", + source: null + }; + const explicitAriaLabel = el.getAttribute ? el.getAttribute("aria-label") : null; + if (explicitAriaLabel && explicitAriaLabel.trim()) return { + text: explicitAriaLabel.trim(), + source: "explicit_aria_label" + }; + if ("INPUT" === el.tagName) { + const t = el.getAttribute && el.getAttribute("type") || el.type || "", isPassword = "password" === String(t).toLowerCase(), value = (isPassword ? el.placeholder || "" : el.value || el.placeholder || "").trim(); + if (value) return { + text: value, + source: isPassword ? "input_placeholder" : "input_value" + }; + } + if ("IMG" === el.tagName) { + const alt = (el.alt || "").trim(); + if (alt) return { + text: alt, + source: "img_alt" + }; + } + const innerText = (el.innerText || "").trim(); + if (innerText) return { + text: innerText.substring(0, 100), + source: "inner_text" + }; + const inferred = getInferredLabel(el, { + enableInference: !1 !== options.enableInference, + inferenceConfig: options.inferenceConfig + }); + return inferred || { + text: "", + source: null + }; + }(el, { + enableInference: !1 !== options.enableInference, + inferenceConfig: options.inferenceConfig + }), textVal = semanticText.text || getText(el), inferredRole = function(el, options = {}) { + const {enableInference: enableInference = !0} = options; + if (!enableInference) return null; + if (!isInteractableElement(el)) return null; + const hasAriaLabel = el.getAttribute ? el.getAttribute("aria-label") : null, hasExplicitRole = el.getAttribute ? el.getAttribute("role") : null; + if (hasAriaLabel || hasExplicitRole) return null; + const tag = el.tagName.toLowerCase(); + return [ "button", "a", "input", "textarea", "select", "option" ].includes(tag) ? null : el.onclick || el.getAttribute && el.getAttribute("onclick") || el.onkeydown || el.onkeypress || el.onkeyup || el.getAttribute && (el.getAttribute("onkeydown") || el.getAttribute("onkeypress") || el.getAttribute("onkeyup")) || el.hasAttribute && el.hasAttribute("tabindex") && ("div" === tag || "span" === tag) ? "button" : null; + }(el, { + enableInference: !1 !== options.enableInference, + inferenceConfig: options.inferenceConfig + }), inView = function(rect) { + return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; + }(rect), style = window.getComputedStyle(el), occluded = !!inView && function(el, rect, style) { + const zIndex = parseInt(style.zIndex, 10); + if ("static" === style.position && (isNaN(zIndex) || zIndex <= 10)) return !1; + const cx = rect.x + rect.width / 2, cy = rect.y + rect.height / 2; + if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return !1; + const topEl = document.elementFromPoint(cx, cy); + return !!topEl && !(el === topEl || el.contains(topEl) || topEl.contains(el)); + }(el, rect, style), effectiveBgColor = function(el) { + if (!el) return null; + if ("SVG" === el.tagName) { + const svgColor = getSVGColor(el); + if (svgColor) return svgColor; + } + let current = el, depth = 0; + for (;current && depth < 10; ) { + const style = window.getComputedStyle(current); + if ("SVG" === current.tagName) { + const svgColor = getSVGColor(current); + if (svgColor) return svgColor; + } + const bgColor = style.backgroundColor; + if (bgColor && "transparent" !== bgColor && "rgba(0, 0, 0, 0)" !== bgColor) { + const rgbaMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (!rgbaMatch) return bgColor.startsWith("rgb("), bgColor; + if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; + } + current = current.parentElement, depth++; + } + return null; + }(el); + let safeValue = null, valueRedacted = null; + try { + if (void 0 !== el.value || el.getAttribute && null !== el.getAttribute("value")) if (isPasswordInput) safeValue = null, + valueRedacted = "true"; else { + const rawValue = void 0 !== el.value ? String(el.value) : String(el.getAttribute("value")); + safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue, valueRedacted = "false"; + } + } catch (e) {} + const accessibleName = toSafeString(function(el) { + if (!el || !el.getAttribute) return ""; + const ariaLabel = el.getAttribute("aria-label"); + if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim().substring(0, 200); + const labelledBy = el.getAttribute("aria-labelledby"); + if (labelledBy && labelledBy.trim()) { + const ids = labelledBy.split(/\s+/).filter(id => id.trim()), texts = []; + for (const id of ids) try { + const ref = document.getElementById(id); + if (!ref) continue; + const txt = (ref.innerText || ref.textContent || ref.getAttribute?.("aria-label") || "").toString().trim(); + txt && texts.push(txt); + } catch (e) {} + if (texts.length > 0) return texts.join(" ").substring(0, 200); + } + try { + if (el.labels && el.labels.length > 0) { + const t = (el.labels[0].innerText || el.labels[0].textContent || "").toString().trim(); + if (t) return t.substring(0, 200); + } + } catch (e) {} + try { + const parentLabel = el.closest && el.closest("label"); + if (parentLabel) { + const t = (parentLabel.innerText || parentLabel.textContent || "").toString().trim(); + if (t) return t.substring(0, 200); + } + } catch (e) {} + const tag = (el.tagName || "").toUpperCase(); + if ("INPUT" === tag || "TEXTAREA" === tag) { + const ph = (el.getAttribute("placeholder") || "").toString().trim(); + if (ph) return ph.substring(0, 200); + } + const title = el.getAttribute("title"); + return title && title.trim() ? title.trim().substring(0, 200) : ""; + }(el) || null), nearbyText = isInteractableElement(el) ? function(el, options = {}) { + if (!el) return null; + const maxLen = "number" == typeof options.maxLen ? options.maxLen : 80, ownText = normalizeNearbyText(el.innerText || ""), candidates = [], collect = node => { + if (!node) return; + let text = ""; + try { + text = normalizeNearbyText(node.innerText || node.textContent || ""); + } catch (e) { + text = ""; + } + text && text !== ownText && candidates.push(text); }; - } - - // Add iframe context so agents can switch frames in Playwright - adjusted.iframe_context = { - src: iframeSrc, - is_same_origin: isSameOrigin, - }; - - return adjusted; + if (collect(el.previousElementSibling), collect(el.nextElementSibling), 0 === candidates.length && el.parentElement) { + let parentText = ""; + try { + parentText = normalizeNearbyText(el.parentElement.innerText || ""); + } catch (e) { + parentText = ""; + } + parentText && parentText !== ownText && parentText.length <= 120 && candidates.push(parentText); + } + if (0 === candidates.length) return null; + let text = candidates[0]; + return text.length > maxLen && (text = text.slice(0, maxLen).trim()), text || null; + }(el, { + maxLen: 80 + }) : null; + rawData.push({ + id: idx, + tag: tagName, + 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")), + input_type: inputType, + aria_label: "explicit_aria_label" === semanticText?.source ? semanticText.text : toSafeString(el.getAttribute("aria-label")), + name: accessibleName, + inferred_label: semanticText?.source && ![ "explicit_aria_label", "input_value", "img_alt", "inner_text" ].includes(semanticText.source) ? toSafeString(semanticText.text) : null, + label_source: semanticText?.source || null, + inferred_role: inferredRole ? toSafeString(inferredRole) : null, + nearby_text: toSafeString(nearbyText), + href: toSafeString(el.href || el.getAttribute("href") || el.closest && el.closest("a")?.href || null), + class: toSafeString(getClassName(el)), + value: null !== safeValue ? toSafeString(safeValue) : null, + value_redacted: valueRedacted, + checked: void 0 !== el.checked ? String(el.checked) : null, + disabled: void 0 !== el.disabled ? String(el.disabled) : null, + aria_checked: toSafeString(el.getAttribute("aria-checked")), + aria_disabled: toSafeString(el.getAttribute("aria-disabled")), + aria_expanded: toSafeString(el.getAttribute("aria-expanded")) + }, + text: toSafeString(textVal), + in_viewport: inView, + is_occluded: occluded, + scroll_y: window.scrollY + }); + }); + const allRawElements = [ ...rawData ]; + let totalIframeElements = 0; + if (!1 !== options.collectIframes) try { + const iframeSnapshots = await async function(options = {}) { + const iframeData = new Map, iframes = Array.from(document.querySelectorAll("iframe")); + if (0 === iframes.length) return iframeData; + const iframePromises = iframes.map((iframe, idx) => { + const src = iframe.src || ""; + return src.includes("doubleclick") || src.includes("googleadservices") || src.includes("ads system") ? Promise.resolve(null) : new Promise(resolve => { + const requestId = `iframe-${idx}-${Date.now()}`, timeout = setTimeout(() => { + resolve(null); + }, 5e3), listener = event => { + "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data, "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data?.requestId === requestId && (clearTimeout(timeout), + window.removeEventListener("message", listener), event.data.error ? resolve(null) : (event.data.snapshot, + resolve({ + iframe: iframe, + data: event.data.snapshot, + error: null + }))); + }; + window.addEventListener("message", listener); + try { + iframe.contentWindow ? iframe.contentWindow.postMessage({ + type: "SENTIENCE_IFRAME_SNAPSHOT_REQUEST", + requestId: requestId, + options: { + ...options, + collectIframes: !0 + } + }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener), + resolve(null)); + } catch (error) { + clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null); + } + }); + }); + return (await Promise.all(iframePromises)).forEach((result, idx) => { + result && result.data && !result.error ? iframeData.set(iframes[idx], result.data) : result && result.error; + }), iframeData; + }(options); + iframeSnapshots.size > 0 && iframeSnapshots.forEach((iframeSnapshot, iframeEl) => { + if (iframeSnapshot && iframeSnapshot.raw_elements) { + iframeSnapshot.raw_elements.length; + const iframeRect = iframeEl.getBoundingClientRect(), offset = { + x: iframeRect.x, + y: iframeRect.y + }, iframeSrc = iframeEl.src || iframeEl.getAttribute("src") || ""; + let isSameOrigin = !1; + try { + isSameOrigin = null !== iframeEl.contentWindow; + } catch (e) { + isSameOrigin = !1; + } + const adjustedElements = iframeSnapshot.raw_elements.map(el => { + const adjusted = { + ...el + }; + return adjusted.rect && (adjusted.rect = { + ...adjusted.rect, + x: adjusted.rect.x + offset.x, + y: adjusted.rect.y + offset.y + }), adjusted.iframe_context = { + src: iframeSrc, + is_same_origin: isSameOrigin + }, adjusted; + }); + allRawElements.push(...adjustedElements), totalIframeElements += adjustedElements.length; + } }); - - // Append flattened iframe elements to main array - allRawElements.push(...adjustedElements); - totalIframeElements += adjustedElements.length; - } + } catch (error) {} + const fallbackElementsFromRaw = raw => (raw || []).map(r => { + const rect = r && r.rect || { + x: 0, + y: 0, + width: 0, + height: 0 + }, attrs = r && r.attributes || {}, role = attrs.role || r && (r.inferred_role || r.inferredRole) || (r && "a" === r.tag ? "link" : "generic"), href = attrs.href || r && r.href || null, isClickable = "link" === role || "button" === role || "textbox" === role || "checkbox" === role || "radio" === role || "combobox" === role || !!href; + return { + id: Number(r && r.id || 0), + role: String(role || "generic"), + text: r && (r.text || r.semantic_text || r.semanticText) || null, + importance: 1, + bbox: { + x: Number(rect.x || 0), + y: Number(rect.y || 0), + width: Number(rect.width || 0), + height: Number(rect.height || 0) + }, + visual_cues: { + is_primary: !1, + is_clickable: !!isClickable + }, + in_viewport: !0, + is_occluded: !(!r || !r.occluded && !r.is_occluded), + z_index: 0, + name: attrs.aria_label || attrs.ariaLabel || null, + value: r && r.value || null, + input_type: attrs.type_ || attrs.type || null, + checked: "boolean" == typeof (r && r.checked) ? r.checked : null, + disabled: "boolean" == typeof (r && r.disabled) ? r.disabled : null, + expanded: "boolean" == typeof (r && r.expanded) ? r.expanded : null + }; }); - - // console.log(`[SentienceAPI] Merged ${iframeSnapshots.size} iframe(s). Total elements: ${allRawElements.length} (${rawData.length} main + ${totalIframeElements} iframe)`); - } + let processed = null; + try { + processed = await function(rawData, options) { + return new Promise((resolve, reject) => { + const requestId = Math.random().toString(36).substring(7); + let resolved = !1; + const timeout = setTimeout(() => { + resolved || (resolved = !0, window.removeEventListener("message", listener), reject(new Error("WASM processing timeout - extension may be unresponsive. Try reloading the extension."))); + }, 25e3), listener = e => { + if ("SENTIENCE_SNAPSHOT_RESULT" === e.data.type && e.data.requestId === requestId) { + if (resolved) return; + resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), + e.data.error ? reject(new Error(e.data.error)) : 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: requestId, + rawData: rawData, + options: options + }, "*"); + } catch (error) { + resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), + reject(new Error(`Failed to send snapshot request: ${error.message}`))); + } + }); + }(allRawElements, options); + } catch (error) { + processed = { + elements: fallbackElementsFromRaw(allRawElements), + raw_elements: allRawElements, + duration: null + }; + } + processed && processed.elements || (processed = { + elements: fallbackElementsFromRaw(allRawElements), + raw_elements: allRawElements, + duration: null + }); + let screenshot = null; + options.screenshot && (screenshot = await function(options) { + return new Promise(resolve => { + const requestId = Math.random().toString(36).substring(7), listener = e => { + "SENTIENCE_SCREENSHOT_RESULT" === e.data.type && e.data.requestId === requestId && (window.removeEventListener("message", listener), + resolve(e.data.screenshot)); + }; + window.addEventListener("message", listener), window.postMessage({ + type: "SENTIENCE_SCREENSHOT_REQUEST", + requestId: requestId, + options: options + }, "*"), setTimeout(() => { + window.removeEventListener("message", listener), resolve(null); + }, 1e4); + }); + }(options.screenshot)); + const cleanedElements = cleanElement(processed.elements), cleanedRawElements = cleanElement(processed.raw_elements); + cleanedElements.length, cleanedRawElements.length; + let diagnostics; + try { + const lastMutationTs = window.__sentience_lastMutationTs, now = performance.now(), quietMs = "number" == typeof lastMutationTs && Number.isFinite(lastMutationTs) ? Math.max(0, now - lastMutationTs) : null, nodeCount = document.querySelectorAll("*").length; + let requiresVision = !1, requiresVisionReason = null; + const canvasCount = document.getElementsByTagName("canvas").length; + canvasCount > 0 && (requiresVision = !0, requiresVisionReason = `canvas:${canvasCount}`), + diagnostics = { + metrics: { + ready_state: document.readyState || null, + quiet_ms: quietMs, + node_count: nodeCount + }, + captcha: detectCaptcha(), + requires_vision: requiresVision, + requires_vision_reason: requiresVisionReason + }; + } catch (e) {} + return { + status: "success", + url: window.location.href, + viewport: { + width: window.innerWidth, + height: window.innerHeight + }, + elements: cleanedElements, + raw_elements: cleanedRawElements, + screenshot: screenshot, + diagnostics: diagnostics + }; } catch (error) { - console.warn('[SentienceAPI] Iframe collection failed:', error); + return { + status: "error", + error: error.message || "Unknown error", + stack: error.stack + }; } - } - - // 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 fallbackElementsFromRaw = (raw) => - (raw || []).map((r) => { - const rect = (r && r.rect) || { x: 0, y: 0, width: 0, height: 0 }; - const attrs = (r && r.attributes) || {}; - const role = - attrs.role || - (r && (r.inferred_role || r.inferredRole)) || - (r && r.tag === 'a' ? 'link' : 'generic'); - const href = attrs.href || (r && r.href) || null; - const isClickable = - role === 'link' || - role === 'button' || - role === 'textbox' || - role === 'checkbox' || - role === 'radio' || - role === 'combobox' || - !!href; - - return { - id: Number((r && r.id) || 0), - role: String(role || 'generic'), - text: (r && (r.text || r.semantic_text || r.semanticText)) || null, - importance: 1, - bbox: { - x: Number(rect.x || 0), - y: Number(rect.y || 0), - width: Number(rect.width || 0), - height: Number(rect.height || 0), - }, - visual_cues: { - is_primary: false, - is_clickable: !!isClickable, - }, - in_viewport: true, - is_occluded: !!(r && (r.occluded || r.is_occluded)), - z_index: 0, - name: attrs.aria_label || attrs.ariaLabel || null, - value: (r && r.value) || null, - input_type: attrs.type_ || attrs.type || null, - checked: typeof (r && r.checked) === 'boolean' ? r.checked : null, - disabled: typeof (r && r.disabled) === 'boolean' ? r.disabled : null, - expanded: typeof (r && r.expanded) === 'boolean' ? r.expanded : null, - }; - }); - - let processed = null; - try { - processed = await processSnapshotInBackground(allRawElements, options); - } catch (error) { - console.warn( - '[SentienceAPI] WASM processing failed; falling back to raw mapping:', - error - ); - processed = { - elements: fallbackElementsFromRaw(allRawElements), - raw_elements: allRawElements, - duration: null, + } + function read(options = {}) { + const format = options.format || "raw"; + let content; + return content = "raw" === format ? getRawHTML(document.body) : "markdown" === format ? function(root) { + const rawHTML = getRawHTML(root), tempDiv = document.createElement("div"); + tempDiv.innerHTML = rawHTML; + let markdown = "", insideLink = !1; + return function walk(node) { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.replace(/[\r\n]+/g, " ").replace(/\s+/g, " "); + return void (text.trim() && (markdown += text)); + } + if (node.nodeType !== Node.ELEMENT_NODE) return; + const tag = node.tagName.toLowerCase(); + if ("h1" === tag && (markdown += "\n# "), "h2" === tag && (markdown += "\n## "), + "h3" === tag && (markdown += "\n### "), "li" === tag && (markdown += "\n- "), insideLink || "p" !== tag && "div" !== tag && "br" !== tag || (markdown += "\n"), + "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), + "a" === tag && (markdown += "[", insideLink = !0), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), + "a" === tag) { + const href = node.getAttribute("href"); + markdown += href ? `](${href})` : "]", insideLink = !1; + } + "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), + insideLink || "h1" !== tag && "h2" !== tag && "h3" !== tag && "p" !== tag && "div" !== tag || (markdown += "\n"); + }(tempDiv), markdown.replace(/\n{3,}/g, "\n\n").trim(); + }(document.body) : function(root) { + let text = ""; + return function walk(node) { + if (node.nodeType !== Node.TEXT_NODE) { + if (node.nodeType === Node.ELEMENT_NODE) { + const tag = node.tagName.toLowerCase(); + if ([ "nav", "footer", "header", "script", "style", "noscript", "iframe", "svg" ].includes(tag)) return; + const style = window.getComputedStyle(node); + if ("none" === style.display || "hidden" === style.visibility) return; + const isBlock = "block" === style.display || "flex" === style.display || "P" === node.tagName || "DIV" === node.tagName; + isBlock && (text += " "), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), + isBlock && (text += "\n"); + } + } else text += node.textContent; + }(root || document.body), text.replace(/\n{3,}/g, "\n\n").trim(); + }(document.body), { + status: "success", + url: window.location.href, + format: format, + content: content, + length: content.length }; - } - - if (!processed || !processed.elements) { - processed = { - elements: fallbackElementsFromRaw(allRawElements), - raw_elements: allRawElements, - duration: null, + } + function findTextRect(options = {}) { + const {text: text, containerElement: containerElement = document.body, caseSensitive: caseSensitive = !1, wholeWord: wholeWord = !1, maxResults: maxResults = 10} = options; + if (!text || 0 === text.trim().length) return { + status: "error", + error: "Text parameter is required" }; - } - - // Step 3: Capture screenshot if requested - let screenshot = null; - if (options.screenshot) { - screenshot = await captureScreenshot(options.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)` - ); - - // Snapshot diagnostics (Phase 2): report stability metrics from the page context. - // Confidence/exhaustion is computed in the Gateway/SDKs; the extension supplies raw metrics. - let diagnostics = undefined; - try { - const lastMutationTs = window.__sentience_lastMutationTs; - const now = performance.now(); - const quietMs = - typeof lastMutationTs === 'number' && Number.isFinite(lastMutationTs) - ? Math.max(0, now - lastMutationTs) - : null; - const nodeCount = document.querySelectorAll('*').length; - - // P1-01: best-effort signal that structure may be insufficient (vision executor recommended). - // Keep heuristics conservative: we only set requires_vision when we see clear structural blockers. - let requiresVision = false; - let requiresVisionReason = null; - const canvasCount = document.getElementsByTagName('canvas').length; - if (canvasCount > 0) { - requiresVision = true; - requiresVisionReason = `canvas:${canvasCount}`; + const results = [], searchText = caseSensitive ? text : text.toLowerCase(); + function findInTextNode(textNode) { + const nodeText = textNode.nodeValue, searchableText = caseSensitive ? nodeText : nodeText.toLowerCase(); + let startIndex = 0; + for (;startIndex < nodeText.length && results.length < maxResults; ) { + const foundIndex = searchableText.indexOf(searchText, startIndex); + if (-1 === foundIndex) break; + if (wholeWord) { + const before = foundIndex > 0 ? nodeText[foundIndex - 1] : " ", after = foundIndex + text.length < nodeText.length ? nodeText[foundIndex + text.length] : " "; + if (!/\s/.test(before) || !/\s/.test(after)) { + startIndex = foundIndex + 1; + continue; + } + } + try { + const range = document.createRange(); + range.setStart(textNode, foundIndex), range.setEnd(textNode, foundIndex + text.length); + const rect = range.getBoundingClientRect(); + 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) {} + startIndex = foundIndex + 1; + } } - - diagnostics = { - metrics: { - ready_state: document.readyState || null, - quiet_ms: quietMs, - node_count: nodeCount, - }, - captcha: detectCaptcha(), - requires_vision: requiresVision, - requires_vision_reason: requiresVisionReason, + const walker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + const tagName = parent.tagName.toLowerCase(); + if ("script" === tagName || "style" === tagName || "noscript" === tagName) return NodeFilter.FILTER_REJECT; + if (!node.nodeValue || 0 === node.nodeValue.trim().length) return NodeFilter.FILTER_REJECT; + const computedStyle = window.getComputedStyle(parent); + return "none" === computedStyle.display || "hidden" === computedStyle.visibility || "0" === computedStyle.opacity ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; + } + }); + let currentNode; + for (;(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 + } }; - } catch (e) { - // ignore - } - - return { - status: 'success', - url: window.location.href, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - }, - elements: cleanedElements, - raw_elements: cleanedRawElements, - screenshot, - diagnostics, - }; - } 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, - }; - } - } - - // read.js - Content Reading Methods - - // 2. Read Content (unchanged) - function read(options = {}) { - const format = options.format || 'raw'; - let content; - - if (format === 'raw') { - content = getRawHTML(document.body); - } else if (format === 'markdown') { - content = convertToMarkdown(document.body); - } else { - content = convertToText(document.body); - } - - return { - status: 'success', - url: window.location.href, - format, - content, - length: content.length, - }; - } - - // 2b. Find Text Rectangle - Get exact pixel coordinates of specific text - function 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(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, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - scroll_x: window.scrollX, - scroll_y: window.scrollY, - }, - }; - } - - // click.js - Click Action Method - - // 3. Click Action (unchanged) - function click(id) { - const el = window.sentience_registry[id]; - if (el) { - el.click(); - el.focus(); - return true; - } - return false; - } - - // registry.js - Inspector Mode / Golden Set Collection - - // 4. Inspector Mode: Start Recording for Golden Set Collection - function 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); + function click(id) { + const el = window.sentience_registry[id]; + return !!el && (el.click(), el.focus(), !0); } - - // 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, - 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.'); + function startRecording(options = {}) { + const {highlightColor: highlightColor = "#ff0000", successColor: successColor = "#00ff00", autoDisableTimeout: autoDisableTimeout = 18e5, keyboardShortcut: keyboardShortcut = "Ctrl+Shift+I"} = options; + if (!window.sentience_registry || 0 === window.sentience_registry.length) return alert("Registry empty. Run `await window.sentience.snapshot()` first!"), + () => {}; + window.sentience_registry_map = new Map, window.sentience_registry.forEach((el, idx) => { + el && window.sentience_registry_map.set(el, idx); }); - }; - - // 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; - } - - // overlay.js - Visual Overlay Methods - - /** - * 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) - */ - function showOverlay(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, - targetElementId, - timestamp: Date.now(), - }, - '*' - ); - - console.log(`[Sentience] Overlay requested for ${elements.length} elements`); - } - - /** - * Show grid overlay highlighting detected grids - * @param {Array} grids - Array of GridInfo objects from SDK's get_grid_bounds() - * @param {number|null} targetGridId - Optional grid ID to highlight in red - */ - function showGrid(grids, targetGridId = null) { - if (!grids || !Array.isArray(grids)) { - console.warn('[Sentience] showGrid: grids must be an array'); - return; - } - - window.postMessage( - { - type: 'SENTIENCE_SHOW_GRID_OVERLAY', - grids, - targetGridId, - timestamp: Date.now(), - }, - '*' - ); - - console.log(`[Sentience] Grid overlay requested for ${grids.length} grids`); - } - - /** - * Clear overlay manually - */ - function clearOverlay() { - window.postMessage( - { - type: 'SENTIENCE_CLEAR_OVERLAY', - }, - '*' - ); - console.log('[Sentience] Overlay cleared'); - } - - // index.js - Main Entry Point for Injected API - // This script ONLY collects raw DOM data and sends it to background for processing - - - (async () => { - // console.log('[SentienceAPI] Initializing (CSP-Resistant Mode)...'); - - // Wait for Extension ID from content.js - const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; - let extId = getExtensionId(); - - if (!extId) { - await new Promise((resolve) => { - const check = setInterval(() => { - extId = getExtensionId(); - if (extId) { - clearInterval(check); - resolve(); - } - }, 50); - setTimeout(() => resolve(), 5000); // Max 5s wait - }); - } - - 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 = []; - - // --- GLOBAL API --- - window.sentience = { - snapshot, - read, - findTextRect, - click, - startRecording, - showOverlay, - showGrid, - clearOverlay, - }; - - // Setup iframe handler when script loads (only once) - if (!window.sentience_iframe_handler_setup) { - setupIframeSnapshotHandler(); - window.sentience_iframe_handler_setup = true; - } - - console.log('[SentienceAPI] ✓ Ready! (CSP-Resistant - WASM runs in background)'); - })(); - -})(); + let highlightBox = document.getElementById("sentience-highlight-box"); + highlightBox || (highlightBox = document.createElement("div"), highlightBox.id = "sentience-highlight-box", + highlightBox.style.cssText = `\n position: fixed;\n pointer-events: none;\n z-index: 2147483647;\n border: 2px solid ${highlightColor};\n background: rgba(255, 0, 0, 0.1);\n display: none;\n transition: all 0.1s ease;\n box-sizing: border-box;\n `, + document.body.appendChild(highlightBox)); + let recordingIndicator = document.getElementById("sentience-recording-indicator"); + recordingIndicator || (recordingIndicator = document.createElement("div"), recordingIndicator.id = "sentience-recording-indicator", + recordingIndicator.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: ${highlightColor};\n z-index: 2147483646;\n pointer-events: none;\n `, + document.body.appendChild(recordingIndicator)), recordingIndicator.style.display = "block"; + 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"; + }, clickHandler = e => { + e.preventDefault(), e.stopPropagation(); + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + const sentienceId = window.sentience_registry_map.get(el); + if (void 0 === sentienceId) return void alert("Element not in registry. Run `await window.sentience.snapshot()` first!"); + const rawData = function(el) { + const style = window.getComputedStyle(el), 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 + } + }; + }(el), selector = function(el) { + if (!el || !el.tagName) return ""; + if (el.id) return `#${el.id}`; + for (const attr of el.attributes) if (attr.name.startsWith("data-") || "aria-label" === attr.name) { + const value = attr.value ? attr.value.replace(/"/g, '\\"') : ""; + return `${el.tagName.toLowerCase()}[${attr.name}="${value}"]`; + } + const path = []; + let current = el; + for (;current && current !== document.body && current !== document.documentElement; ) { + let selector = current.tagName.toLowerCase(); + if (current.id) { + selector = `#${current.id}`, path.unshift(selector); + break; + } + if (current.className && "string" == typeof current.className) { + const classes = current.className.trim().split(/\s+/).filter(c => c); + classes.length > 0 && (selector += `.${classes[0]}`); + } + if (current.parentElement) { + const sameTagSiblings = Array.from(current.parentElement.children).filter(s => s.tagName === current.tagName), index = sameTagSiblings.indexOf(current); + (index > 0 || sameTagSiblings.length > 1) && (selector += `:nth-of-type(${index + 1})`); + } + path.unshift(selector), current = current.parentElement; + } + return path.join(" > ") || el.tagName.toLowerCase(); + }(el), role = el.getAttribute("role") || el.tagName.toLowerCase(), text = getText(el), 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 + }, jsonString = JSON.stringify(snippet, null, 2); + navigator.clipboard.writeText(jsonString).then(() => { + 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 => { + alert("Failed to copy to clipboard. Check console for JSON."); + }); + }; + let timeoutId = null; + const stopRecording = () => { + document.removeEventListener("mouseover", mouseOverHandler, !0), document.removeEventListener("click", clickHandler, !0), + document.removeEventListener("keydown", keyboardHandler, !0), timeoutId && (clearTimeout(timeoutId), + timeoutId = null), highlightBox && (highlightBox.style.display = "none"), recordingIndicator && (recordingIndicator.style.display = "none"), + window.sentience_registry_map && window.sentience_registry_map.clear(), window.sentience_stopRecording === stopRecording && delete window.sentience_stopRecording; + }, keyboardHandler = e => { + (e.ctrlKey || e.metaKey) && e.shiftKey && "I" === e.key && (e.preventDefault(), + stopRecording()); + }; + return document.addEventListener("mouseover", mouseOverHandler, !0), document.addEventListener("click", clickHandler, !0), + document.addEventListener("keydown", keyboardHandler, !0), autoDisableTimeout > 0 && (timeoutId = setTimeout(() => { + stopRecording(); + }, autoDisableTimeout)), window.sentience_stopRecording = stopRecording, stopRecording; + } + function showOverlay(elements, targetElementId = null) { + elements && Array.isArray(elements) && window.postMessage({ + type: "SENTIENCE_SHOW_OVERLAY", + elements: elements, + targetElementId: targetElementId, + timestamp: Date.now() + }, "*"); + } + function showGrid(grids, targetGridId = null) { + grids && Array.isArray(grids) && window.postMessage({ + type: "SENTIENCE_SHOW_GRID_OVERLAY", + grids: grids, + targetGridId: targetGridId, + timestamp: Date.now() + }, "*"); + } + function clearOverlay() { + window.postMessage({ + type: "SENTIENCE_CLEAR_OVERLAY" + }, "*"); + } + (async () => { + const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; + let extId = getExtensionId(); + extId || await new Promise(resolve => { + const check = setInterval(() => { + extId = getExtensionId(), extId && (clearInterval(check), resolve()); + }, 50); + setTimeout(() => resolve(), 5e3); + }), extId && (window.sentience_registry = [], window.sentience = { + snapshot: snapshot, + read: read, + findTextRect: findTextRect, + click: click, + startRecording: startRecording, + showOverlay: showOverlay, + showGrid: showGrid, + clearOverlay: clearOverlay + }, window.sentience_iframe_handler_setup || (window.addEventListener("message", async event => { + if ("SENTIENCE_IFRAME_SNAPSHOT_REQUEST" === event.data?.type) { + const {requestId: requestId, options: options} = event.data; + try { + const snapshotOptions = { + ...options, + collectIframes: !0, + waitForStability: (options.waitForStability, !1) + }, snapshot = await window.sentience.snapshot(snapshotOptions); + event.source && event.source.postMessage && event.source.postMessage({ + type: "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE", + requestId: requestId, + snapshot: snapshot, + error: null + }, "*"); + } catch (error) { + event.source && event.source.postMessage && event.source.postMessage({ + type: "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE", + requestId: requestId, + snapshot: null, + error: error.message + }, "*"); + } + } + }), window.sentience_iframe_handler_setup = !0)); + })(); +}(); \ No newline at end of file diff --git a/sentience/extension/manifest.json b/sentience/extension/manifest.json index c1c6e6f..55838ca 100644 --- a/sentience/extension/manifest.json +++ b/sentience/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Sentience Semantic Visual Grounding Extractor", - "version": "2.8.0", + "version": "2.8.3", "description": "Extract semantic visual grounding data from web pages", "permissions": ["activeTab", "scripting"], "host_permissions": [""], diff --git a/sentience/extension/release.json b/sentience/extension/release.json index 89b5c5a..9bac170 100644 --- a/sentience/extension/release.json +++ b/sentience/extension/release.json @@ -1,45 +1,45 @@ { - "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279228428", - "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279228428/assets", - "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279228428/assets{?name,label}", - "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.8.0", - "id": 279228428, + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279250819", + "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279250819/assets", + "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279250819/assets{?name,label}", + "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.8.3", + "id": 279250819, "author": { - "login": "rcholic", - "id": 135060, - "node_id": "MDQ6VXNlcjEzNTA2MA==", - "avatar_url": "https://avatars.githubusercontent.com/u/135060?v=4", + "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/rcholic", - "html_url": "https://github.com/rcholic", - "followers_url": "https://api.github.com/users/rcholic/followers", - "following_url": "https://api.github.com/users/rcholic/following{/other_user}", - "gists_url": "https://api.github.com/users/rcholic/gists{/gist_id}", - "starred_url": "https://api.github.com/users/rcholic/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/rcholic/subscriptions", - "organizations_url": "https://api.github.com/users/rcholic/orgs", - "repos_url": "https://api.github.com/users/rcholic/repos", - "events_url": "https://api.github.com/users/rcholic/events{/privacy}", - "received_events_url": "https://api.github.com/users/rcholic/received_events", - "type": "User", + "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_kwDOQshiJ84QpLAM", - "tag_name": "v2.8.0", + "node_id": "RE_kwDOQshiJ84QpQeD", + "tag_name": "v2.8.3", "target_commitish": "main", - "name": "Release v2.8.0", + "name": "Release v2.8.3", "draft": false, "immutable": false, "prerelease": false, - "created_at": "2026-01-23T03:51:24Z", - "updated_at": "2026-01-23T03:52:39Z", - "published_at": "2026-01-23T03:52:16Z", + "created_at": "2026-01-23T06:12:02Z", + "updated_at": "2026-01-23T06:14:22Z", + "published_at": "2026-01-23T06:13:11Z", "assets": [ { - "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/344613562", - "id": 344613562, - "node_id": "RA_kwDOQshiJ84UimK6", + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/344654926", + "id": 344654926, + "node_id": "RA_kwDOQshiJ84UiwRO", "name": "extension-files.tar.gz", "label": "", "uploader": { @@ -65,17 +65,17 @@ }, "content_type": "application/gzip", "state": "uploaded", - "size": 78756, - "digest": "sha256:5835596decbd70f2c8d0d06a6cbd45222a40b4cb1e150612415bae28878a7ecb", + "size": 79317, + "digest": "sha256:0d8979ec6c3cde0fb679501f89e5fdb0db8d547661b1da7d6299f28262ad1fc4", "download_count": 0, - "created_at": "2026-01-23T03:52:39Z", - "updated_at": "2026-01-23T03:52:39Z", - "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.8.0/extension-files.tar.gz" + "created_at": "2026-01-23T06:13:12Z", + "updated_at": "2026-01-23T06:13:12Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.8.3/extension-files.tar.gz" }, { - "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/344613561", - "id": 344613561, - "node_id": "RA_kwDOQshiJ84UimK5", + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/344654925", + "id": 344654925, + "node_id": "RA_kwDOQshiJ84UiwRN", "name": "extension-package.zip", "label": "", "uploader": { @@ -101,15 +101,15 @@ }, "content_type": "application/zip", "state": "uploaded", - "size": 80234, - "digest": "sha256:dcd4d9a8e555d6f48751ddf140fae1d468a170e6160a156e69d03ab2490a1392", + "size": 80784, + "digest": "sha256:cc80691d6efc868a9d4009fe1b8bdb94f33017666a7d2db429a1f4aaee978616", "download_count": 0, - "created_at": "2026-01-23T03:52:39Z", - "updated_at": "2026-01-23T03:52:39Z", - "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.8.0/extension-package.zip" + "created_at": "2026-01-23T06:13:12Z", + "updated_at": "2026-01-23T06:13:12Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.8.3/extension-package.zip" } ], - "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.8.0", - "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.8.0", - "body": "" + "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.8.3", + "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.8.3", + "body": "**Full Changelog**: https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/compare/v2.8.2...v2.8.3" }