From e77d8574813d809aa51304d626fd8c42bd9ef299 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 9 Jan 2026 17:25:12 -0800 Subject: [PATCH 1/6] Ordinal fields --- sentience/extension/background.js | 6 +-- sentience/extension/content.js | 12 ++--- sentience/extension/injected_api.js | 58 +++++++++++------------ sentience/extension/pkg/sentience_core.js | 14 +++--- sentience/models.py | 7 +++ tests/test_video_recording.py | 3 +- 6 files changed, 54 insertions(+), 46 deletions(-) diff --git a/sentience/extension/background.js b/sentience/extension/background.js index b5192d9..02c0408 100644 --- a/sentience/extension/background.js +++ b/sentience/extension/background.js @@ -28,14 +28,14 @@ async function handleSnapshotProcessing(rawData, options = {}) { const startTime = performance.now(); try { if (!Array.isArray(rawData)) throw new Error("rawData must be an array"); - if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(), + if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(), !wasmReady) throw new Error("WASM module not initialized"); let analyzedElements, prunedRawData; try { const wasmPromise = new Promise((resolve, reject) => { try { let result; - result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData), + result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData), resolve(result); } catch (e) { reject(e); @@ -101,4 +101,4 @@ initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, send 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 e94cde1..9d5b3bf 100644 --- a/sentience/extension/content.js +++ b/sentience/extension/content.js @@ -82,7 +82,7 @@ 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 ", + 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" @@ -94,15 +94,15 @@ 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 `, + 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 `, + 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 ", + 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); @@ -120,7 +120,7 @@ let overlayTimeout = null; function removeOverlay() { const existing = document.getElementById(OVERLAY_HOST_ID); - existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout), + existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout), overlayTimeout = null); } -}(); \ No newline at end of file +}(); diff --git a/sentience/extension/injected_api.js b/sentience/extension/injected_api.js index c62bcab..69c7d36 100644 --- a/sentience/extension/injected_api.js +++ b/sentience/extension/injected_api.js @@ -112,7 +112,7 @@ if (labelEl) { let text = ""; try { - if (text = (labelEl.innerText || "").trim(), !text && labelEl.textContent && (text = labelEl.textContent.trim()), + 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()); @@ -281,7 +281,7 @@ }); const checkStable = () => { const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; - timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(), + timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(), resolve()) : setTimeout(checkStable, 50); }; checkStable(); @@ -301,7 +301,7 @@ }); const checkQuiet = () => { const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; - timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(), + timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(), resolve()) : setTimeout(checkQuiet, 50); }; checkQuiet(); @@ -461,8 +461,8 @@ 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, + "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, @@ -478,7 +478,7 @@ ...options, collectIframes: !0 } - }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener), + }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null)); } catch (error) { clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null); @@ -528,7 +528,7 @@ }, 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), + 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, @@ -545,7 +545,7 @@ options: options }, "*"); } catch (error) { - resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), + resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), reject(new Error(`Failed to send snapshot request: ${error.message}`))); } }); @@ -555,7 +555,7 @@ 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), + "SENTIENCE_SCREENSHOT_RESULT" === e.data.type && e.data.requestId === requestId && (window.removeEventListener("message", listener), resolve(e.data.screenshot)); }; window.addEventListener("message", listener), window.postMessage({ @@ -602,15 +602,15 @@ } 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), + 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 += "_"), + "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) { @@ -623,7 +623,7 @@ 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 += " "), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), isBlock && (text += "\n"); } } else text += node.textContent; @@ -722,25 +722,25 @@ } 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!"), + 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); }); 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 `, + 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 `, + 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.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(); @@ -817,7 +817,7 @@ 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)", + 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); @@ -827,15 +827,15 @@ }; 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"), + 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(), + (e.ctrlKey || e.metaKey) && e.shiftKey && "I" === e.key && (e.preventDefault(), stopRecording()); }; - return document.addEventListener("mouseover", mouseOverHandler, !0), document.addEventListener("click", clickHandler, !0), + 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; @@ -895,4 +895,4 @@ } }), window.sentience_iframe_handler_setup = !0)); })(); -}(); \ No newline at end of file +}(); diff --git a/sentience/extension/pkg/sentience_core.js b/sentience/extension/pkg/sentience_core.js index ecba479..2696a64 100644 --- a/sentience/extension/pkg/sentience_core.js +++ b/sentience/extension/pkg/sentience_core.js @@ -47,7 +47,7 @@ function getArrayU8FromWasm0(ptr, len) { let cachedDataViewMemory0 = null; function getDataViewMemory0() { - return (null === cachedDataViewMemory0 || !0 === cachedDataViewMemory0.buffer.detached || void 0 === cachedDataViewMemory0.buffer.detached && cachedDataViewMemory0.buffer !== wasm.memory.buffer) && (cachedDataViewMemory0 = new DataView(wasm.memory.buffer)), + return (null === cachedDataViewMemory0 || !0 === cachedDataViewMemory0.buffer.detached || void 0 === cachedDataViewMemory0.buffer.detached && cachedDataViewMemory0.buffer !== wasm.memory.buffer) && (cachedDataViewMemory0 = new DataView(wasm.memory.buffer)), cachedDataViewMemory0; } @@ -58,7 +58,7 @@ function getStringFromWasm0(ptr, len) { let cachedUint8ArrayMemory0 = null; function getUint8ArrayMemory0() { - return null !== cachedUint8ArrayMemory0 && 0 !== cachedUint8ArrayMemory0.byteLength || (cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer)), + return null !== cachedUint8ArrayMemory0 && 0 !== cachedUint8ArrayMemory0.byteLength || (cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer)), cachedUint8ArrayMemory0; } @@ -87,7 +87,7 @@ function isLikeNone(x) { function passStringToWasm0(arg, malloc, realloc) { if (void 0 === realloc) { const buf = cachedTextEncoder.encode(arg), ptr = malloc(buf.length, 1) >>> 0; - return getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf), WASM_VECTOR_LEN = buf.length, + return getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf), WASM_VECTOR_LEN = buf.length, ptr; } let len = arg.length, ptr = malloc(len, 1) >>> 0; @@ -188,7 +188,7 @@ function __wbg_get_imports() { return Number(getObject(arg0)); }, imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) { const v = getObject(arg1), ret = "bigint" == typeof v ? v : void 0; - getDataViewMemory0().setBigInt64(arg0 + 8, isLikeNone(ret) ? BigInt(0) : ret, !0), + getDataViewMemory0().setBigInt64(arg0 + 8, isLikeNone(ret) ? BigInt(0) : ret, !0), getDataViewMemory0().setInt32(arg0 + 0, !isLikeNone(ret), !0); }, imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { const v = getObject(arg0), ret = "boolean" == typeof v ? v : void 0; @@ -296,7 +296,7 @@ function __wbg_get_imports() { } function __wbg_finalize_init(instance, module) { - return wasm = instance.exports, __wbg_init.__wbindgen_wasm_module = module, cachedDataViewMemory0 = null, + return wasm = instance.exports, __wbg_init.__wbindgen_wasm_module = module, cachedDataViewMemory0 = null, cachedUint8ArrayMemory0 = null, wasm; } @@ -310,7 +310,7 @@ function initSync(module) { async function __wbg_init(module_or_path) { if (void 0 !== wasm) return wasm; - void 0 !== module_or_path && Object.getPrototypeOf(module_or_path) === Object.prototype && ({module_or_path: module_or_path} = module_or_path), + void 0 !== module_or_path && Object.getPrototypeOf(module_or_path) === Object.prototype && ({module_or_path: module_or_path} = module_or_path), void 0 === module_or_path && (module_or_path = new URL("sentience_core_bg.wasm", import.meta.url)); const imports = __wbg_get_imports(); ("string" == typeof module_or_path || "function" == typeof Request && module_or_path instanceof Request || "function" == typeof URL && module_or_path instanceof URL) && (module_or_path = fetch(module_or_path)); @@ -320,4 +320,4 @@ async function __wbg_init(module_or_path) { export { initSync }; -export default __wbg_init; \ No newline at end of file +export default __wbg_init; diff --git a/sentience/models.py b/sentience/models.py index 985a264..45b6a5b 100644 --- a/sentience/models.py +++ b/sentience/models.py @@ -54,6 +54,13 @@ class Element(BaseModel): # Diff status for frontend Diff Overlay feature diff_status: Literal["ADDED", "REMOVED", "MODIFIED", "MOVED"] | None = None + # Phase 1: Ordinal support fields for position-based selection + center_x: float | None = None # X coordinate of element center (viewport coords) + center_y: float | None = None # Y coordinate of element center (viewport coords) + doc_y: float | None = None # Y coordinate in document (center_y + scroll_y) + group_key: str | None = None # Geometric bucket key for ordinal grouping + group_index: int | None = None # Position within group (0-indexed, sorted by doc_y) + class Snapshot(BaseModel): """Snapshot response from extension""" diff --git a/tests/test_video_recording.py b/tests/test_video_recording.py index 0b49553..9a069da 100644 --- a/tests/test_video_recording.py +++ b/tests/test_video_recording.py @@ -22,9 +22,10 @@ def test_video_recording_basic(): try: browser.page.goto("https://example.com") browser.page.wait_for_load_state("domcontentloaded") - + # Small delay to ensure page is fully loaded and video recording is stable import time + time.sleep(0.5) video_path = browser.close() From a4cc85964b6a4bd0d3ec594f44c48420a60e60d2 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 9 Jan 2026 18:14:42 -0800 Subject: [PATCH 2/6] ordinal support --- sentience/__init__.py | 8 + sentience/models.py | 2 + sentience/ordinal.py | 280 ++++++++++++++++++++++++++ tests/test_ordinal.py | 442 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 732 insertions(+) create mode 100644 sentience/ordinal.py create mode 100644 tests/test_ordinal.py diff --git a/sentience/__init__.py b/sentience/__init__.py index 91ebe36..d3663df 100644 --- a/sentience/__init__.py +++ b/sentience/__init__.py @@ -77,6 +77,9 @@ ViewportRect, WaitResult, ) + +# Ordinal support (Phase 3) +from .ordinal import OrdinalIntent, boost_ordinal_elements, detect_ordinal_intent, select_by_ordinal from .overlay import clear_overlay, show_overlay from .query import find, query from .read import read @@ -242,4 +245,9 @@ "all_of", "any_of", "custom", + # Ordinal support (Phase 3) + "OrdinalIntent", + "detect_ordinal_intent", + "select_by_ordinal", + "boost_ordinal_elements", ] diff --git a/sentience/models.py b/sentience/models.py index 45b6a5b..fbce127 100644 --- a/sentience/models.py +++ b/sentience/models.py @@ -74,6 +74,8 @@ class Snapshot(BaseModel): screenshot_format: Literal["png", "jpeg"] | None = None error: str | None = None requires_license: bool | None = None + # Phase 2: Dominant group key for ordinal selection + dominant_group_key: str | None = None # The most common group_key (main content group) def save(self, filepath: str) -> None: """Save snapshot as JSON file""" diff --git a/sentience/ordinal.py b/sentience/ordinal.py new file mode 100644 index 0000000..2fd7089 --- /dev/null +++ b/sentience/ordinal.py @@ -0,0 +1,280 @@ +""" +Phase 3: Ordinal Intent Detection for Semantic Search + +This module provides functions to detect ordinal intent in natural language goals +and select elements based on their position within groups. + +Ordinal operators supported: +- Position-based: "first", "second", "third", "1st", "2nd", "3rd", etc. +- Relative: "top", "bottom", "last", "next", "previous" +- Numeric: "#1", "#2", "number 1", "item 3" + +Example usage: + from sentience.ordinal import detect_ordinal_intent, select_by_ordinal + + intent = detect_ordinal_intent("click the first search result") + # OrdinalIntent(kind='nth', n=1, detected=True) + + element = select_by_ordinal(elements, dominant_group_key, intent) +""" + +from dataclasses import dataclass +from typing import Literal +import re + +from sentience.models import Element + + +@dataclass +class OrdinalIntent: + """Detected ordinal intent from a goal string.""" + + detected: bool + kind: Literal["first", "last", "nth", "top_k", "next", "previous"] | None = None + n: int | None = None # For "nth" kind: 1-indexed position (1=first, 2=second) + k: int | None = None # For "top_k" kind: number of items + + +# Ordinal word to number mapping +ORDINAL_WORDS = { + "first": 1, + "second": 2, + "third": 3, + "fourth": 4, + "fifth": 5, + "sixth": 6, + "seventh": 7, + "eighth": 8, + "ninth": 9, + "tenth": 10, + "1st": 1, + "2nd": 2, + "3rd": 3, + "4th": 4, + "5th": 5, + "6th": 6, + "7th": 7, + "8th": 8, + "9th": 9, + "10th": 10, +} + +# Patterns for detecting ordinal intent +ORDINAL_PATTERNS = [ + # "first", "second", etc. + ( + r"\b(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\b", + "ordinal_word", + ), + # "1st", "2nd", "3rd", etc. + (r"\b(\d+)(st|nd|rd|th)\b", "ordinal_suffix"), + # "#1", "#2", etc. + (r"#(\d+)\b", "hash_number"), + # "number 1", "item 3", "result 5" + (r"\b(?:number|item|result|option|choice)\s*(\d+)\b", "labeled_number"), + # "top" (implies first/best) + (r"\btop\b(?!\s*\d)", "top"), + # "top 3", "top 5" + (r"\btop\s+(\d+)\b", "top_k"), + # "last", "final", "bottom" + (r"\b(last|final|bottom)\b", "last"), + # "next", "following" + (r"\b(next|following)\b", "next"), + # "previous", "preceding", "prior" + (r"\b(previous|preceding|prior)\b", "previous"), +] + + +def detect_ordinal_intent(goal: str) -> OrdinalIntent: + """ + Detect ordinal intent from a goal string. + + Args: + goal: Natural language goal (e.g., "click the first search result") + + Returns: + OrdinalIntent with detected=True if ordinal intent found, False otherwise. + + Examples: + >>> detect_ordinal_intent("click the first item") + OrdinalIntent(detected=True, kind='nth', n=1) + + >>> detect_ordinal_intent("select the 3rd option") + OrdinalIntent(detected=True, kind='nth', n=3) + + >>> detect_ordinal_intent("show top 5 results") + OrdinalIntent(detected=True, kind='top_k', k=5) + + >>> detect_ordinal_intent("click the last button") + OrdinalIntent(detected=True, kind='last') + + >>> detect_ordinal_intent("find the submit button") + OrdinalIntent(detected=False) + """ + goal_lower = goal.lower() + + for pattern, pattern_type in ORDINAL_PATTERNS: + match = re.search(pattern, goal_lower, re.IGNORECASE) + if match: + if pattern_type == "ordinal_word": + word = match.group(1).lower() + n = ORDINAL_WORDS.get(word) + if n: + return OrdinalIntent(detected=True, kind="nth", n=n) + + elif pattern_type == "ordinal_suffix": + n = int(match.group(1)) + return OrdinalIntent(detected=True, kind="nth", n=n) + + elif pattern_type == "hash_number": + n = int(match.group(1)) + return OrdinalIntent(detected=True, kind="nth", n=n) + + elif pattern_type == "labeled_number": + n = int(match.group(1)) + return OrdinalIntent(detected=True, kind="nth", n=n) + + elif pattern_type == "top": + # "top" without a number means "first/best" + return OrdinalIntent(detected=True, kind="first") + + elif pattern_type == "top_k": + k = int(match.group(1)) + return OrdinalIntent(detected=True, kind="top_k", k=k) + + elif pattern_type == "last": + return OrdinalIntent(detected=True, kind="last") + + elif pattern_type == "next": + return OrdinalIntent(detected=True, kind="next") + + elif pattern_type == "previous": + return OrdinalIntent(detected=True, kind="previous") + + return OrdinalIntent(detected=False) + + +def select_by_ordinal( + elements: list[Element], + dominant_group_key: str | None, + intent: OrdinalIntent, + current_element_id: int | None = None, +) -> Element | list[Element] | None: + """ + Select element(s) from a list based on ordinal intent. + + Uses the dominant_group_key to filter to the "main content" group, + then selects by group_index based on the ordinal intent. + + Args: + elements: List of elements with group_key and group_index populated + dominant_group_key: The most common group key (main content group) + intent: Detected ordinal intent + current_element_id: Current element ID (for next/previous navigation) + + Returns: + Single Element for nth/first/last, list of Elements for top_k, + or None if no matching element found. + + Examples: + >>> intent = OrdinalIntent(detected=True, kind='nth', n=1) + >>> element = select_by_ordinal(elements, "x5-w2-h1", intent) + # Returns element with group_key="x5-w2-h1" and group_index=0 + """ + if not intent.detected: + return None + + # Filter to dominant group if available + if dominant_group_key: + group_elements = [e for e in elements if e.group_key == dominant_group_key] + else: + # Fallback: use all elements with group_index + group_elements = [e for e in elements if e.group_index is not None] + + if not group_elements: + return None + + # Sort by group_index to ensure correct ordering + group_elements.sort(key=lambda e: e.group_index if e.group_index is not None else 0) + + if intent.kind == "first" or (intent.kind == "nth" and intent.n == 1): + # First element (group_index=0) + return group_elements[0] if group_elements else None + + elif intent.kind == "nth" and intent.n is not None: + # Nth element (1-indexed, so n=2 means group_index=1) + target_index = intent.n - 1 + if 0 <= target_index < len(group_elements): + return group_elements[target_index] + return None + + elif intent.kind == "last": + # Last element + return group_elements[-1] if group_elements else None + + elif intent.kind == "top_k" and intent.k is not None: + # Top K elements + return group_elements[: intent.k] + + elif intent.kind == "next" and current_element_id is not None: + # Next element after current + for i, elem in enumerate(group_elements): + if elem.id == current_element_id and i + 1 < len(group_elements): + return group_elements[i + 1] + return None + + elif intent.kind == "previous" and current_element_id is not None: + # Previous element before current + for i, elem in enumerate(group_elements): + if elem.id == current_element_id and i > 0: + return group_elements[i - 1] + return None + + return None + + +def boost_ordinal_elements( + elements: list[Element], + dominant_group_key: str | None, + intent: OrdinalIntent, + boost_factor: int = 10000, +) -> list[Element]: + """ + Boost the importance of elements matching ordinal intent. + + This is useful for integrating ordinal selection with existing + importance-based ranking. Elements matching the ordinal intent + get a significant importance boost. + + Args: + elements: List of elements (not modified) + dominant_group_key: The most common group key + intent: Detected ordinal intent + boost_factor: Amount to add to importance (default: 10000) + + Returns: + A new list with copies of elements, with boosted importance for matches. + """ + if not intent.detected or not dominant_group_key: + return [e.model_copy() for e in elements] + + target = select_by_ordinal(elements, dominant_group_key, intent) + + if target is None: + return [e.model_copy() for e in elements] + + # Handle single element or list + if isinstance(target, list): + target_ids = {e.id for e in target} + else: + target_ids = {target.id} + + # Create copies and boost matching elements + result = [] + for elem in elements: + copy = elem.model_copy() + if copy.id in target_ids: + copy.importance = (copy.importance or 0) + boost_factor + result.append(copy) + + return result diff --git a/tests/test_ordinal.py b/tests/test_ordinal.py new file mode 100644 index 0000000..5474851 --- /dev/null +++ b/tests/test_ordinal.py @@ -0,0 +1,442 @@ +""" +Unit tests for ordinal intent detection and selection. + +Tests the detect_ordinal_intent, select_by_ordinal, and boost_ordinal_elements functions. +""" + +import pytest + +from sentience.models import BBox, Element, VisualCues +from sentience.ordinal import ( + OrdinalIntent, + boost_ordinal_elements, + detect_ordinal_intent, + select_by_ordinal, +) + + +class TestDetectOrdinalIntent: + """Tests for detect_ordinal_intent function.""" + + # Ordinal words + def test_first(self): + result = detect_ordinal_intent("Click the first result") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 1 + + def test_second(self): + result = detect_ordinal_intent("Select the second item") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 2 + + def test_third(self): + result = detect_ordinal_intent("Click the third option") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 3 + + def test_fourth(self): + result = detect_ordinal_intent("Choose the fourth link") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 4 + + def test_fifth(self): + result = detect_ordinal_intent("Click the fifth button") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 5 + + def test_tenth(self): + result = detect_ordinal_intent("Select the tenth item") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 10 + + # Ordinal suffixes + def test_1st(self): + result = detect_ordinal_intent("Click the 1st result") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 1 + + def test_2nd(self): + result = detect_ordinal_intent("Select the 2nd item") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 2 + + def test_3rd(self): + result = detect_ordinal_intent("Click the 3rd option") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 3 + + def test_4th(self): + result = detect_ordinal_intent("Choose the 4th link") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 4 + + def test_21st(self): + result = detect_ordinal_intent("Select the 21st item") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 21 + + def test_22nd(self): + result = detect_ordinal_intent("Click the 22nd result") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 22 + + def test_33rd(self): + result = detect_ordinal_intent("Choose the 33rd option") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 33 + + def test_100th(self): + result = detect_ordinal_intent("Select the 100th item") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 100 + + # Hash numbers + def test_hash_1(self): + result = detect_ordinal_intent("Click item #1") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 1 + + def test_hash_3(self): + result = detect_ordinal_intent("Select result #3") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 3 + + def test_hash_10(self): + result = detect_ordinal_intent("Choose option #10") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 10 + + # Labeled numbers + def test_item_number(self): + result = detect_ordinal_intent("Click item 5") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 5 + + def test_result_number(self): + result = detect_ordinal_intent("Select result 3") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 3 + + def test_option_number(self): + result = detect_ordinal_intent("Choose option 2") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 2 + + def test_number_word(self): + result = detect_ordinal_intent("Click number 4") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 4 + + def test_choice_number(self): + result = detect_ordinal_intent("Select choice 1") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 1 + + # Top/first keywords + def test_top(self): + result = detect_ordinal_intent("Click the top result") + assert result.detected is True + assert result.kind == "first" + + def test_top_case_insensitive(self): + result = detect_ordinal_intent("Click the TOP result") + assert result.detected is True + assert result.kind == "first" + + # Top K + def test_top_3(self): + result = detect_ordinal_intent("Select the top 3 items") + assert result.detected is True + assert result.kind == "top_k" + assert result.k == 3 + + def test_top_5(self): + result = detect_ordinal_intent("View top 5 results") + assert result.detected is True + assert result.kind == "top_k" + assert result.k == 5 + + def test_top_10(self): + result = detect_ordinal_intent("Show top 10 products") + assert result.detected is True + assert result.kind == "top_k" + assert result.k == 10 + + # Last keywords + def test_last(self): + result = detect_ordinal_intent("Click the last item") + assert result.detected is True + assert result.kind == "last" + + def test_final(self): + result = detect_ordinal_intent("Select the final option") + assert result.detected is True + assert result.kind == "last" + + def test_bottom(self): + result = detect_ordinal_intent("Click the bottom result") + assert result.detected is True + assert result.kind == "last" + + # Next keywords + def test_next(self): + result = detect_ordinal_intent("Click the next button") + assert result.detected is True + assert result.kind == "next" + + def test_following(self): + result = detect_ordinal_intent("Go to the following item") + assert result.detected is True + assert result.kind == "next" + + # Previous keywords + def test_previous(self): + result = detect_ordinal_intent("Click the previous button") + assert result.detected is True + assert result.kind == "previous" + + def test_preceding(self): + result = detect_ordinal_intent("Go to the preceding item") + assert result.detected is True + assert result.kind == "previous" + + def test_prior(self): + result = detect_ordinal_intent("Select the prior option") + assert result.detected is True + assert result.kind == "previous" + + # No ordinal intent + def test_no_ordinal_click_button(self): + result = detect_ordinal_intent("Click the submit button") + assert result.detected is False + assert result.kind is None + + def test_no_ordinal_search(self): + result = detect_ordinal_intent("Search for laptops") + assert result.detected is False + + def test_no_ordinal_type(self): + result = detect_ordinal_intent("Type hello in the input") + assert result.detected is False + + def test_empty_string(self): + result = detect_ordinal_intent("") + assert result.detected is False + + # Case insensitivity + def test_case_insensitive_first(self): + result = detect_ordinal_intent("Click the FIRST result") + assert result.detected is True + assert result.kind == "nth" + assert result.n == 1 + + def test_case_insensitive_last(self): + result = detect_ordinal_intent("Select the LAST item") + assert result.detected is True + assert result.kind == "last" + + +def _make_element( + id: int, + text: str, + group_key: str | None = None, + group_index: int | None = None, + importance: int = 100, +) -> Element: + """Helper to create test elements.""" + return Element( + id=id, + role="button", + text=text, + importance=importance, + bbox=BBox(x=0, y=id * 50, width=100, height=40), + visual_cues=VisualCues(is_primary=False, background_color_name=None, is_clickable=True), + in_viewport=True, + is_occluded=False, + z_index=0, + group_key=group_key, + group_index=group_index, + ) + + +class TestSelectByOrdinal: + """Tests for select_by_ordinal function.""" + + @pytest.fixture + def elements(self) -> list[Element]: + """Create a list of test elements in the dominant group.""" + return [ + _make_element(1, "Item A", "x100-w200-h40", 0), + _make_element(2, "Item B", "x100-w200-h40", 1), + _make_element(3, "Item C", "x100-w200-h40", 2), + _make_element(4, "Item D", "x100-w200-h40", 3), + _make_element(5, "Item E", "x100-w200-h40", 4), + _make_element(6, "Other", "x500-w100-h30", 0), # Different group + ] + + def test_select_first(self, elements): + intent = OrdinalIntent(detected=True, kind="first") + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert result is not None + assert result.id == 1 + assert result.text == "Item A" + + def test_select_nth_2(self, elements): + intent = OrdinalIntent(detected=True, kind="nth", n=2) + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert result is not None + assert result.id == 2 + assert result.text == "Item B" + + def test_select_nth_5(self, elements): + intent = OrdinalIntent(detected=True, kind="nth", n=5) + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert result is not None + assert result.id == 5 + assert result.text == "Item E" + + def test_select_last(self, elements): + intent = OrdinalIntent(detected=True, kind="last") + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert result is not None + assert result.id == 5 + assert result.text == "Item E" + + def test_select_top_k(self, elements): + intent = OrdinalIntent(detected=True, kind="top_k", k=3) + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert isinstance(result, list) + assert len(result) == 3 + assert [e.id for e in result] == [1, 2, 3] + + def test_select_out_of_bounds(self, elements): + intent = OrdinalIntent(detected=True, kind="nth", n=100) + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert result is None + + def test_select_no_dominant_group(self, elements): + intent = OrdinalIntent(detected=True, kind="first") + result = select_by_ordinal(elements, None, intent) + # Should fall back to all elements sorted by group_index + assert result is not None + + def test_select_not_detected(self, elements): + intent = OrdinalIntent(detected=False) + result = select_by_ordinal(elements, "x100-w200-h40", intent) + assert result is None + + +class TestBoostOrdinalElements: + """Tests for boost_ordinal_elements function.""" + + @pytest.fixture + def elements(self) -> list[Element]: + """Create a list of test elements.""" + return [ + _make_element(1, "Item A", "x100-w200-h40", 0, importance=100), + _make_element(2, "Item B", "x100-w200-h40", 1, importance=90), + _make_element(3, "Item C", "x100-w200-h40", 2, importance=80), + _make_element(4, "Item D", "x100-w200-h40", 3, importance=70), + _make_element(5, "Other", "x500-w100-h30", 0, importance=200), + ] + + def test_boost_first(self, elements): + intent = OrdinalIntent(detected=True, kind="first") + result = boost_ordinal_elements(elements, "x100-w200-h40", intent, boost_factor=10000) + + # First element should be boosted + boosted = [e for e in result if e.id == 1][0] + assert boosted.importance == 100 + 10000 + + # Other elements unchanged + other = [e for e in result if e.id == 2][0] + assert other.importance == 90 + + def test_boost_nth(self, elements): + intent = OrdinalIntent(detected=True, kind="nth", n=3) + result = boost_ordinal_elements(elements, "x100-w200-h40", intent, boost_factor=5000) + + # Third element should be boosted + boosted = [e for e in result if e.id == 3][0] + assert boosted.importance == 80 + 5000 + + def test_boost_last(self, elements): + intent = OrdinalIntent(detected=True, kind="last") + result = boost_ordinal_elements(elements, "x100-w200-h40", intent, boost_factor=10000) + + # Last element in dominant group should be boosted (id=4, group_index=3) + boosted = [e for e in result if e.id == 4][0] + assert boosted.importance == 70 + 10000 + + def test_boost_top_k(self, elements): + intent = OrdinalIntent(detected=True, kind="top_k", k=2) + result = boost_ordinal_elements(elements, "x100-w200-h40", intent, boost_factor=10000) + + # First two elements should be boosted + first = [e for e in result if e.id == 1][0] + second = [e for e in result if e.id == 2][0] + third = [e for e in result if e.id == 3][0] + + assert first.importance == 100 + 10000 + assert second.importance == 90 + 10000 + assert third.importance == 80 # Not boosted + + def test_boost_not_detected(self, elements): + intent = OrdinalIntent(detected=False) + result = boost_ordinal_elements(elements, "x100-w200-h40", intent) + + # No elements should be boosted + for orig, boosted in zip(elements, result): + assert orig.importance == boosted.importance + + def test_boost_returns_copy(self, elements): + intent = OrdinalIntent(detected=True, kind="first") + result = boost_ordinal_elements(elements, "x100-w200-h40", intent) + + # Original elements should not be modified + assert elements[0].importance == 100 + + +class TestOrdinalIntent: + """Tests for OrdinalIntent dataclass.""" + + def test_default_values(self): + intent = OrdinalIntent(detected=False) + assert intent.detected is False + assert intent.kind is None + assert intent.n is None + assert intent.k is None + + def test_with_nth(self): + intent = OrdinalIntent(detected=True, kind="nth", n=5) + assert intent.detected is True + assert intent.kind == "nth" + assert intent.n == 5 + + def test_with_top_k(self): + intent = OrdinalIntent(detected=True, kind="top_k", k=3) + assert intent.detected is True + assert intent.kind == "top_k" + assert intent.k == 3 From 41c6f777b8f99ca1575d78924acb0c8a4f7fb473 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 9 Jan 2026 19:16:40 -0800 Subject: [PATCH 3/6] fix none for dominant group key --- sentience/__init__.py | 7 +- sentience/extension/background.js | 308 +- sentience/extension/content.js | 449 ++- sentience/extension/injected_api.js | 2956 ++++++++++++----- sentience/extension/pkg/sentience_core.js | 584 ++-- .../extension/pkg/sentience_core_bg.wasm | Bin 103409 -> 107941 bytes sentience/ordinal.py | 2 + sentience/snapshot.py | 2 + 8 files changed, 3054 insertions(+), 1254 deletions(-) diff --git a/sentience/__init__.py b/sentience/__init__.py index d3663df..af3ace5 100644 --- a/sentience/__init__.py +++ b/sentience/__init__.py @@ -79,7 +79,12 @@ ) # Ordinal support (Phase 3) -from .ordinal import OrdinalIntent, boost_ordinal_elements, detect_ordinal_intent, select_by_ordinal +from .ordinal import ( + OrdinalIntent, + boost_ordinal_elements, + detect_ordinal_intent, + select_by_ordinal, +) from .overlay import clear_overlay, show_overlay from .query import find, query from .read import read diff --git a/sentience/extension/background.js b/sentience/extension/background.js index 02c0408..1f64f84 100644 --- a/sentience/extension/background.js +++ b/sentience/extension/background.js @@ -1,104 +1,242 @@ -import init, { analyze_page_with_options, analyze_page, prune_for_api } from "./pkg/sentience_core.js"; +// 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'; -let wasmReady = !1, wasmInitPromise = null; +// background.js - Service Worker with WASM (CSP-Immune!) +// This runs in an isolated environment, completely immune to page CSP policies + +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 wasmInitPromise || (wasmInitPromise = (async () => { - try { - globalThis.js_click_element = () => {}, await init(), wasmReady = !0; - } catch (error) { - throw error; - } - })(), wasmInitPromise); -} + if (wasmReady) return; + if (wasmInitPromise) return wasmInitPromise; -async function handleScreenshotCapture(_tabId, options = {}) { + wasmInitPromise = (async () => { try { - const {format: format = "png", quality: quality = 90} = options; - return await chrome.tabs.captureVisibleTab(null, { - format: format, - quality: quality - }); + 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' + ); } catch (error) { - throw new Error(`Failed to capture screenshot: ${error.message}`); + console.error('[Sentience Background] WASM initialization failed:', error); + throw error; + } + })(); + + return wasmInitPromise; +} + +// Initialize WASM on service worker startup +initWASM().catch((err) => { + console.error('[Sentience Background] Failed to initialize WASM:', err); +}); + +/** + * Message handler for all extension communication + * Includes global error handling to prevent extension crashes + */ +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + // 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 startTime = performance.now(); + const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs + const startTime = performance.now(); + + try { + // Safety check: limit element count to prevent hangs + if (!Array.isArray(rawData)) { + throw new Error('rawData must be an array'); + } + + if (rawData.length > MAX_ELEMENTS) { + console.warn( + `[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.` + ); + rawData = rawData.slice(0, MAX_ELEMENTS); + } + + // Ensure WASM is initialized + await initWASM(); + if (!wasmReady) { + throw new Error('WASM module not initialized'); + } + + console.log( + `[Sentience Background] Processing ${rawData.length} elements with options:`, + options + ); + + // Run WASM processing using the imported functions directly + // Wrap in try-catch with timeout protection + let analyzedElements; try { - 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 { - 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) { - const errorMsg = e.message || "Unknown WASM error"; - throw new Error(`WASM analyze_page failed: ${errorMsg}`); - } + // Use a timeout wrapper to prevent infinite hangs + const wasmPromise = new Promise((resolve, reject) => { try { - prunedRawData = prune_for_api(rawData); + let result; + if (options.limit || options.filter) { + result = analyze_page_with_options(rawData, options); + } else { + result = analyze_page(rawData); + } + resolve(result); } catch (e) { - prunedRawData = rawData; + reject(e); } - performance.now(); - return { - elements: analyzedElements, - raw_elements: prunedRawData - }; - } catch (error) { - performance.now(); - throw error; + }); + + // 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}`); } -} -initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + // Prune elements for API (prevents 413 errors on large sites) + let prunedRawData; try { - 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; + prunedRawData = prune_for_api(rawData); + } catch (e) { + console.warn('[Sentience Background] prune_for_api failed, using original data:', e); + prunedRawData = rawData; } -}), self.addEventListener("error", event => { - event.preventDefault(); -}), self.addEventListener("unhandledrejection", event => { - event.preventDefault(); + + const duration = performance.now() - startTime; + console.log( + `[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)` + ); + + return { + elements: analyzedElements, + raw_elements: prunedRawData, + }; + } catch (error) { + const duration = performance.now() - startTime; + console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error); + throw error; + } +} + +console.log('[Sentience Background] Service worker ready'); + +// Global error handlers to prevent extension crashes +self.addEventListener('error', (event) => { + console.error('[Sentience Background] Global error caught:', event.error); + event.preventDefault(); // Prevent extension crash +}); + +self.addEventListener('unhandledrejection', (event) => { + console.error('[Sentience Background] Unhandled promise rejection:', event.reason); + event.preventDefault(); // Prevent extension crash }); diff --git a/sentience/extension/content.js b/sentience/extension/content.js index 9d5b3bf..931ef5a 100644 --- a/sentience/extension/content.js +++ b/sentience/extension/content.js @@ -1,126 +1,329 @@ -!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(); +// 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; + } + }); + + /** + * 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, + }, + '*' + ); + } } + ); + } 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); }); - 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); + + console.log(`[Sentience Bridge] Overlay shown for ${elements.length} elements`); + + // Auto-remove after 5 seconds + overlayTimeout = setTimeout(() => { + removeOverlay(); + console.log('[Sentience Bridge] Overlay auto-cleared after 5 seconds'); + }, 5000); + } + + /** + * Clear overlay manually + */ + function handleClearOverlay() { + removeOverlay(); + console.log('[Sentience Bridge] Overlay cleared manually'); + } + + /** + * Remove overlay from DOM + */ + function removeOverlay() { + const existing = document.getElementById(OVERLAY_HOST_ID); + if (existing) { + existing.remove(); + } + + if (overlayTimeout) { + clearTimeout(overlayTimeout); + overlayTimeout = null; } -}(); + } + + // console.log('[Sentience Bridge] Ready - Extension ID:', chrome.runtime.id); + +})(); diff --git a/sentience/extension/injected_api.js b/sentience/extension/injected_api.js index 69c7d36..f334e0c 100644 --- a/sentience/extension/injected_api.js +++ b/sentience/extension/injected_api.js @@ -1,898 +1,2142 @@ -!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))); +// 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; } - return elements; - } - 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 + // Skip deep SVG children + if (node.parentNode && node.parentNode.tagName === 'SVG' && node.tagName !== 'SVG') { + return NodeFilter.FILTER_REJECT; } + return NodeFilter.FILTER_ACCEPT; + }, }; - 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; + + 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; + } + + // ============================================================================ + // 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(); } - 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 = ""; + // Fallback to aria-label if available + if (!text && labelEl.getAttribute) { + const ariaLabel = labelEl.getAttribute('aria-label'); + if (ariaLabel) { + text = ariaLabel.trim(); + } } - return validTags.includes(tag) || className.toLowerCase().includes("form") || className.toLowerCase().includes("field"); - }(commonParent, limits.containerTags)) return !1; + } catch (e) { + // If text extraction fails, skip this element + continue; + } + + if (text) { + labelTexts.push(text); + } + } + } } - 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 = ""; + + // Combine multiple label texts (space-separated) + if (labelTexts.length > 0) { + return { + text: labelTexts.join(' '), + source: 'aria_labelledby', + }; } - const hasInnerText = "INPUT" !== el.tagName && "IMG" !== el.tagName && innerTextValue && innerTextValue.trim(); - if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) return null; - const config = function(userConfig = {}) { + } + } + + // 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 { - ...DEFAULT_INFERENCE_CONFIG, - ...userConfig, - methods: { - ...DEFAULT_INFERENCE_CONFIG.methods, - ...userConfig.methods || {} - } + text, + source: 'parent_label', }; - }(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" - }; - } + } } - 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" - }; - } + 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) { + return { + text, + source: 'sibling_label', + }; + } } - if (config.methods.parentTraversal) { - let parent = el.parentElement, depth = 0; - for (;parent && depth < config.maxParentDepth; ) { - if (isInferenceSource(parent, config)) { - const text = (parent.innerText || "").trim(); - if (text) return { - text: text, - source: "parent_label" - }; - } - parent = parent.parentElement, depth++; - } + } + } + + return null; + } + + // 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') return el.value || el.placeholder || ''; + if (el.tagName === 'IMG') return el.alt || ''; + return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100); + } + + // Enhanced semantic text extractor with inference support + function getSemanticText(el, options = {}) { + if (!el) { + return { + text: '', + source: null, + }; + } + + // First check explicit aria-label (highest priority) + const explicitAriaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; + if (explicitAriaLabel && explicitAriaLabel.trim()) { + return { + text: explicitAriaLabel.trim(), + source: 'explicit_aria_label', + }; + } + + // Check for existing text (visible text, input value, etc.) + // This matches the existing getText() logic + if (el.tagName === 'INPUT') { + const value = (el.value || el.placeholder || '').trim(); + if (value) { + return { + text: value, + source: 'input_value', + }; + } + } + + if (el.tagName === 'IMG') { + 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), // Match existing getText() limit + source: 'inner_text', + }; + } + + // Only try inference if we have NO explicit text/label + // Pass inferenceConfig from options to getInferredLabel + const inferred = getInferredLabel(el, { + enableInference: options.enableInference !== false, + inferenceConfig: options.inferenceConfig, // Pass config through + }); + if (inferred) { + return inferred; + } + + // Fallback: return empty with no source + return { + text: '', + source: null, + }; + } + + // --- HELPER: Safe Class Name Extractor (Handles SVGAnimatedString) --- + function getClassName(el) { + if (!el || !el.className) return ''; + + // Handle string (HTML elements) + if (typeof el.className === 'string') return el.className; + + // Handle SVGAnimatedString (SVG elements) + if (typeof el.className === 'object') { + if ('baseVal' in el.className && typeof el.className.baseVal === 'string') { + return el.className.baseVal; + } + if ('animVal' in el.className && typeof el.className.animVal === 'string') { + return el.className.animVal; + } + // Fallback: convert to string + try { + return String(el.className); + } catch (e) { + return ''; + } + } + + return ''; + } + + // --- HELPER: Paranoid String Converter (Handles SVGAnimatedString) --- + function toSafeString(value) { + if (value === null || value === undefined) return null; + + // 1. If it's already a primitive string, return it + if (typeof value === 'string') return value; + + // 2. Handle SVG objects (SVGAnimatedString, SVGAnimatedNumber, etc.) + if (typeof value === 'object') { + // Try extracting baseVal (standard SVG property) + if ('baseVal' in value && typeof value.baseVal === 'string') { + return value.baseVal; + } + // Try animVal as fallback + if ('animVal' in value && typeof value.animVal === 'string') { + return value.animVal; + } + // Fallback: Force to string (prevents WASM crash even if data is less useful) + // This prevents the "Invalid Type" crash, even if the data is "[object SVGAnimatedString]" + try { + return String(value); + } catch (e) { + return null; + } + } + + // 3. Last resort cast for primitives + try { + return String(value); + } catch (e) { + return null; + } + } + + // --- HELPER: Get SVG Fill/Stroke Color --- + // For SVG elements, get the fill or stroke color (SVGs use fill/stroke, not backgroundColor) + function getSVGColor(el) { + if (!el || el.tagName !== 'SVG') return null; + + const style = window.getComputedStyle(el); + + // Try fill first (most common for SVG icons) + const fill = style.fill; + if (fill && fill !== 'none' && fill !== 'transparent' && fill !== 'rgba(0, 0, 0, 0)') { + // Convert fill to rgb() format if needed + const rgbaMatch = fill.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (rgbaMatch) { + const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0; + if (alpha >= 0.9) { + return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; } - if (config.methods.siblingProximity) { - const prev = el.previousElementSibling; - if (prev && isInferenceSource(prev, config) && isInSameValidContainer(el, prev, { - requireSameContainer: config.requireSameContainer, - containerTags: config.containerTags - })) { - const text = (prev.innerText || "").trim(); - if (text) return { - text: text, - source: "sibling_label" - }; - } + } else if (fill.startsWith('rgb(')) { + return fill; + } + } + + // Fallback to stroke if fill is not available + const stroke = style.stroke; + if (stroke && stroke !== 'none' && stroke !== 'transparent' && stroke !== 'rgba(0, 0, 0, 0)') { + const rgbaMatch = stroke.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (rgbaMatch) { + const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0; + if (alpha >= 0.9) { + return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; } - return null; + } else if (stroke.startsWith('rgb(')) { + return stroke; + } } - function getText(el) { - return el.getAttribute("aria-label") ? el.getAttribute("aria-label") : "INPUT" === el.tagName ? el.value || el.placeholder || "" : "IMG" === el.tagName ? el.alt || "" : (el.innerText || "").replace(/\s+/g, " ").trim().substring(0, 100); + + return null; + } + + // --- HELPER: Get Effective Background Color --- + // Traverses up the DOM tree to find the nearest non-transparent background color + // For SVGs, also checks fill/stroke properties + // This handles rgba(0,0,0,0) and transparent values that browsers commonly return + function getEffectiveBackgroundColor(el) { + if (!el) return null; + + // For SVG elements, use fill/stroke instead of backgroundColor + if (el.tagName === 'SVG') { + const svgColor = getSVGColor(el); + if (svgColor) return svgColor; } - function getClassName(el) { - if (!el || !el.className) return ""; - if ("string" == typeof el.className) return el.className; - if ("object" == typeof el.className) { - if ("baseVal" in el.className && "string" == typeof el.className.baseVal) return el.className.baseVal; - if ("animVal" in el.className && "string" == typeof el.className.animVal) return el.className.animVal; - try { - return String(el.className); - } catch (e) { - return ""; - } + + let current = el; + const maxDepth = 10; // Prevent infinite loops + let depth = 0; + + while (current && depth < maxDepth) { + const style = window.getComputedStyle(current); + + // For SVG elements in the tree, also check fill/stroke + if (current.tagName === 'SVG') { + const svgColor = getSVGColor(current); + if (svgColor) return svgColor; + } + + const bgColor = style.backgroundColor; + + if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') { + // Check if it's rgba with alpha < 1 (semi-transparent) + const rgbaMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); + if (rgbaMatch) { + const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0; + // If alpha is high enough (>= 0.9), consider it opaque enough + if (alpha >= 0.9) { + // Convert to rgb() format for Gateway compatibility + return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; + } + // If semi-transparent, continue up the tree + } else if (bgColor.startsWith('rgb(')) { + // Already in rgb() format, use it + return bgColor; + } else { + // Named color or other format, return as-is + return bgColor; } - return ""; - } - function toSafeString(value) { - if (null == value) return null; - if ("string" == typeof value) return value; - if ("object" == typeof value) { - if ("baseVal" in value && "string" == typeof value.baseVal) return value.baseVal; - if ("animVal" in value && "string" == typeof value.animVal) return value.animVal; - try { - return String(value); - } catch (e) { - return null; - } + } + + // Move up the DOM tree + current = current.parentElement; + depth++; + } + + // Fallback: return null if nothing found + return null; + } + + // --- HELPER: Viewport Check --- + function isInViewport(rect) { + return ( + rect.top < window.innerHeight && + rect.bottom > 0 && + rect.left < window.innerWidth && + rect.right > 0 + ); + } + + // --- HELPER: Occlusion Check (Optimized to avoid layout thrashing) --- + // Only checks occlusion for elements likely to be occluded (high z-index, positioned) + // This avoids forced reflow for most elements, dramatically improving performance + function isOccluded(el, rect, style) { + // Fast path: Skip occlusion check for most elements + // Only check for elements that are likely to be occluded (overlays, modals, tooltips) + const zIndex = parseInt(style.zIndex, 10); + const position = style.position; + + // Skip occlusion check for normal flow elements (vast majority) + // Only check for positioned elements or high z-index (likely overlays) + if (position === 'static' && (isNaN(zIndex) || zIndex <= 10)) { + return false; // Assume not occluded for performance + } + + // For positioned/high z-index elements, do the expensive check + const cx = rect.x + rect.width / 2; + const cy = rect.y + rect.height / 2; + + if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return false; + + const topEl = document.elementFromPoint(cx, cy); + if (!topEl) return false; + + return !(el === topEl || el.contains(topEl) || topEl.contains(el)); + } + + // --- HELPER: Screenshot Bridge --- + function captureScreenshot(options) { + return new Promise((resolve) => { + const requestId = Math.random().toString(36).substring(7); + const listener = (e) => { + if (e.data.type === 'SENTIENCE_SCREENSHOT_RESULT' && e.data.requestId === requestId) { + window.removeEventListener('message', listener); + resolve(e.data.screenshot); + } + }; + window.addEventListener('message', listener); + window.postMessage({ type: 'SENTIENCE_SCREENSHOT_REQUEST', requestId, options }, '*'); + setTimeout(() => { + window.removeEventListener('message', listener); + resolve(null); + }, 10000); // 10s timeout + }); + } + + // --- HELPER: Snapshot Processing Bridge (NEW!) --- + function processSnapshotInBackground(rawData, options) { + return new Promise((resolve, reject) => { + const requestId = Math.random().toString(36).substring(7); + const TIMEOUT_MS = 25000; // 25 seconds (longer than content.js timeout) + let resolved = false; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + window.removeEventListener('message', listener); + reject( + new Error( + 'WASM processing timeout - extension may be unresponsive. Try reloading the extension.' + ) + ); } + }, TIMEOUT_MS); + + const listener = (e) => { + if (e.data.type === 'SENTIENCE_SNAPSHOT_RESULT' && e.data.requestId === requestId) { + if (resolved) return; // Already handled + resolved = true; + clearTimeout(timeout); + window.removeEventListener('message', listener); + + if (e.data.error) { + reject(new Error(e.data.error)); + } else { + resolve({ + elements: e.data.elements, + raw_elements: e.data.raw_elements, + duration: e.data.duration, + }); + } + } + }; + + window.addEventListener('message', listener); + + try { + window.postMessage( + { + type: 'SENTIENCE_SNAPSHOT_REQUEST', + requestId, + rawData, + options, + }, + '*' + ); + } catch (error) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + window.removeEventListener('message', listener); + reject(new Error(`Failed to send snapshot request: ${error.message}`)); + } + } + }); + } + + // --- HELPER: Raw HTML Extractor (unchanged) --- + function getRawHTML(root) { + const sourceRoot = root || document.body; + const clone = sourceRoot.cloneNode(true); + + const unwantedTags = ['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg']; + unwantedTags.forEach((tag) => { + const elements = clone.querySelectorAll(tag); + elements.forEach((el) => { + if (el.parentNode) el.parentNode.removeChild(el); + }); + }); + + // Remove invisible elements + const invisibleSelectors = []; + const walker = document.createTreeWalker(sourceRoot, NodeFilter.SHOW_ELEMENT, null, false); + let node; + while ((node = walker.nextNode())) { + const tag = node.tagName.toLowerCase(); + if (tag === 'head' || tag === 'title') continue; + + const style = window.getComputedStyle(node); + if ( + style.display === 'none' || + style.visibility === 'hidden' || + (node.offsetWidth === 0 && node.offsetHeight === 0) + ) { + let selector = tag; + if (node.id) { + selector = `#${node.id}`; + } else if (node.className && typeof node.className === 'string') { + const classes = node.className + .trim() + .split(/\s+/) + .filter((c) => c); + if (classes.length > 0) { + selector = `${tag}.${classes.join('.')}`; + } + } + invisibleSelectors.push(selector); + } + } + + invisibleSelectors.forEach((selector) => { + try { + const elements = clone.querySelectorAll(selector); + elements.forEach((el) => { + if (el.parentNode) el.parentNode.removeChild(el); + }); + } catch (e) { + // Invalid selector, skip + } + }); + + // Resolve relative URLs + const links = clone.querySelectorAll('a[href]'); + links.forEach((link) => { + const href = link.getAttribute('href'); + if ( + href && + !href.startsWith('http://') && + !href.startsWith('https://') && + !href.startsWith('#') + ) { try { - return String(value); + link.setAttribute('href', new URL(href, document.baseURI).href); } catch (e) { - return null; + // Ignore invalid URLs } + } + }); + + const images = clone.querySelectorAll('img[src]'); + images.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) { + // Ignore invalid URLs + } + } + }); + + return clone.innerHTML; + } + + // --- HELPER: Markdown Converter (unchanged) --- + function convertToMarkdown(root) { + const rawHTML = getRawHTML(root); + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = rawHTML; + + let markdown = ''; + let insideLink = false; + + function walk(node) { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' '); + if (text.trim()) markdown += text; + return; + } + + if (node.nodeType !== Node.ELEMENT_NODE) return; + + const tag = node.tagName.toLowerCase(); + + // Prefix + if (tag === 'h1') markdown += '\n# '; + if (tag === 'h2') markdown += '\n## '; + if (tag === 'h3') markdown += '\n### '; + if (tag === 'li') markdown += '\n- '; + if (!insideLink && (tag === 'p' || tag === 'div' || tag === 'br')) markdown += '\n'; + if (tag === 'strong' || tag === 'b') markdown += '**'; + if (tag === 'em' || tag === 'i') markdown += '_'; + if (tag === 'a') { + markdown += '['; + insideLink = true; + } + + // Children + if (node.shadowRoot) { + Array.from(node.shadowRoot.childNodes).forEach(walk); + } else { + node.childNodes.forEach(walk); + } + + // Suffix + if (tag === 'a') { + const href = node.getAttribute('href'); + if (href) markdown += `](${href})`; + else markdown += ']'; + insideLink = false; + } + if (tag === 'strong' || tag === 'b') markdown += '**'; + if (tag === 'em' || tag === 'i') markdown += '_'; + if ( + !insideLink && + (tag === 'h1' || tag === 'h2' || tag === 'h3' || tag === 'p' || tag === 'div') + ) + markdown += '\n'; } - function getSVGColor(el) { - if (!el || "SVG" !== el.tagName) return null; - const style = window.getComputedStyle(el), fill = style.fill; - if (fill && "none" !== fill && "transparent" !== fill && "rgba(0, 0, 0, 0)" !== fill) { - const rgbaMatch = fill.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); - if (rgbaMatch) { - if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; - } else if (fill.startsWith("rgb(")) return fill; + + walk(tempDiv); + return markdown.replace(/\n{3,}/g, '\n\n').trim(); + } + + // --- HELPER: Text Extractor (unchanged) --- + function convertToText(root) { + let text = ''; + function walk(node) { + if (node.nodeType === Node.TEXT_NODE) { + text += node.textContent; + return; + } + 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 (style.display === 'none' || style.visibility === 'hidden') return; + + const isBlock = + style.display === 'block' || + style.display === 'flex' || + node.tagName === 'P' || + node.tagName === 'DIV'; + if (isBlock) text += ' '; + + if (node.shadowRoot) { + Array.from(node.shadowRoot.childNodes).forEach(walk); + } else { + node.childNodes.forEach(walk); } - const stroke = style.stroke; - if (stroke && "none" !== stroke && "transparent" !== stroke && "rgba(0, 0, 0, 0)" !== stroke) { - const rgbaMatch = stroke.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); - if (rgbaMatch) { - if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; - } else if (stroke.startsWith("rgb(")) return stroke; + + if (isBlock) text += '\n'; + } + } + walk(root || document.body); + return text.replace(/\n{3,}/g, '\n\n').trim(); + } + + // --- HELPER: Clean null/undefined fields --- + function cleanElement(obj) { + if (Array.isArray(obj)) { + return obj.map(cleanElement); + } + if (obj !== null && typeof obj === 'object') { + const cleaned = {}; + for (const [key, value] of Object.entries(obj)) { + if (value !== null && value !== undefined) { + if (typeof value === 'object') { + const deepClean = cleanElement(value); + if (Object.keys(deepClean).length > 0) { + cleaned[key] = deepClean; + } + } else { + cleaned[key] = value; + } } - return null; + } + return cleaned; + } + return obj; + } + + // --- HELPER: Extract Raw Element Data (for Golden Set) --- + function extractRawElementData(el) { + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + + return { + tag: el.tagName, + rect: { + x: Math.round(rect.x), + y: Math.round(rect.y), + width: Math.round(rect.width), + height: Math.round(rect.height), + }, + styles: { + cursor: style.cursor || null, + backgroundColor: style.backgroundColor || null, + color: style.color || null, + fontWeight: style.fontWeight || null, + fontSize: style.fontSize || null, + display: style.display || null, + position: style.position || null, + zIndex: style.zIndex || null, + opacity: style.opacity || null, + visibility: style.visibility || null, + }, + attributes: { + role: el.getAttribute('role') || null, + type: el.getAttribute('type') || null, + ariaLabel: el.getAttribute('aria-label') || null, + id: el.id || null, + className: el.className || null, + }, + }; + } + + // --- HELPER: Generate Unique CSS Selector (for Golden Set) --- + function getUniqueSelector(el) { + if (!el || !el.tagName) return ''; + + // If element has a unique ID, use it + if (el.id) { + return `#${el.id}`; } - function getRawHTML(root) { - const sourceRoot = root || document.body, clone = sourceRoot.cloneNode(!0); - [ "nav", "footer", "header", "script", "style", "noscript", "iframe", "svg" ].forEach(tag => { - clone.querySelectorAll(tag).forEach(el => { - el.parentNode && el.parentNode.removeChild(el); + + // Try data attributes or aria-label for uniqueness + for (const attr of el.attributes) { + if (attr.name.startsWith('data-') || attr.name === 'aria-label') { + const value = attr.value ? attr.value.replace(/"/g, '\\"') : ''; + return `${el.tagName.toLowerCase()}[${attr.name}="${value}"]`; + } + } + + // Build path with classes and nth-child for uniqueness + const path = []; + let current = el; + + while (current && current !== document.body && current !== document.documentElement) { + let selector = current.tagName.toLowerCase(); + + // If current element has ID, use it and stop + if (current.id) { + selector = `#${current.id}`; + path.unshift(selector); + break; + } + + // Add class if available + if (current.className && typeof current.className === 'string') { + const classes = current.className + .trim() + .split(/\s+/) + .filter((c) => c); + if (classes.length > 0) { + // Use first class for simplicity + selector += `.${classes[0]}`; + } + } + + // Add nth-of-type if needed for uniqueness + if (current.parentElement) { + const siblings = Array.from(current.parentElement.children); + const sameTagSiblings = siblings.filter((s) => s.tagName === current.tagName); + const index = sameTagSiblings.indexOf(current); + if (index > 0 || sameTagSiblings.length > 1) { + selector += `:nth-of-type(${index + 1})`; + } + } + + path.unshift(selector); + current = current.parentElement; + } + + return path.join(' > ') || el.tagName.toLowerCase(); + } + + // --- HELPER: Wait for DOM Stability (SPA Hydration) --- + // Waits for the DOM to stabilize before taking a snapshot + // Useful for React/Vue apps that render empty skeletons before hydration + async function waitForStability(options = {}) { + const { + minNodeCount = 500, + quietPeriod = 200, // milliseconds + maxWait = 5000, // maximum wait time + } = options; + + const startTime = Date.now(); + + return new Promise((resolve) => { + // Check if DOM already has enough nodes + const nodeCount = document.querySelectorAll('*').length; + if (nodeCount >= minNodeCount) { + // DOM seems ready, but wait for quiet period to ensure stability + let lastChange = Date.now(); + const observer = new MutationObserver(() => { + lastChange = Date.now(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + }); + + const checkStable = () => { + const timeSinceLastChange = Date.now() - lastChange; + const totalWait = Date.now() - startTime; + + if (timeSinceLastChange >= quietPeriod) { + observer.disconnect(); + resolve(); + } else if (totalWait >= maxWait) { + observer.disconnect(); + console.warn('[SentienceAPI] DOM stability timeout - proceeding anyway'); + resolve(); + } else { + setTimeout(checkStable, 50); + } + }; + + checkStable(); + } else { + // DOM doesn't have enough nodes yet, wait for them + const observer = new MutationObserver(() => { + const currentCount = document.querySelectorAll('*').length; + const totalWait = Date.now() - startTime; + + if (currentCount >= minNodeCount) { + observer.disconnect(); + // Now wait for quiet period + let lastChange = Date.now(); + const quietObserver = new MutationObserver(() => { + lastChange = Date.now(); + }); + + quietObserver.observe(document.body, { + childList: true, + subtree: true, + attributes: false, }); + + const checkQuiet = () => { + const timeSinceLastChange = Date.now() - lastChange; + const totalWait = Date.now() - startTime; + + if (timeSinceLastChange >= quietPeriod) { + quietObserver.disconnect(); + resolve(); + } else if (totalWait >= maxWait) { + quietObserver.disconnect(); + console.warn('[SentienceAPI] DOM stability timeout - proceeding anyway'); + resolve(); + } else { + setTimeout(checkQuiet, 50); + } + }; + + checkQuiet(); + } else if (totalWait >= maxWait) { + observer.disconnect(); + console.warn('[SentienceAPI] DOM node count timeout - proceeding anyway'); + resolve(); + } }); - const invisibleSelectors = [], walker = document.createTreeWalker(sourceRoot, NodeFilter.SHOW_ELEMENT, null, !1); - let node; - for (;node = walker.nextNode(); ) { - const tag = node.tagName.toLowerCase(); - if ("head" === tag || "title" === tag) continue; - const style = window.getComputedStyle(node); - if ("none" === style.display || "hidden" === style.visibility || 0 === node.offsetWidth && 0 === node.offsetHeight) { - let selector = tag; - if (node.id) selector = `#${node.id}`; else if (node.className && "string" == typeof node.className) { - const classes = node.className.trim().split(/\s+/).filter(c => c); - classes.length > 0 && (selector = `${tag}.${classes.join(".")}`); - } - invisibleSelectors.push(selector); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + }); + + // Timeout fallback + setTimeout(() => { + observer.disconnect(); + console.warn('[SentienceAPI] DOM stability max wait reached - proceeding'); + resolve(); + }, maxWait); + } + }); + } + + // --- HELPER: Collect Iframe Snapshots (Frame Stitching) --- + // Recursively collects snapshot data from all child iframes + // This enables detection of elements inside iframes (e.g., Stripe forms) + // + // NOTE: Cross-origin iframes cannot be accessed due to browser security (Same-Origin Policy). + // Only same-origin iframes will return snapshot data. Cross-origin iframes will be skipped + // with a warning. For cross-origin iframes, users must manually switch frames using + // Playwright's page.frame() API. + async function collectIframeSnapshots(options = {}) { + const iframeData = new Map(); // Map of iframe element -> snapshot data + + // Find all iframe elements in current document + const iframes = Array.from(document.querySelectorAll('iframe')); + + if (iframes.length === 0) { + return iframeData; + } + + console.log(`[SentienceAPI] Found ${iframes.length} iframe(s), requesting snapshots...`); + // Request snapshot from each iframe + const iframePromises = iframes.map((iframe, idx) => { + // OPTIMIZATION: Skip common ad domains to save time + const src = iframe.src || ''; + if ( + src.includes('doubleclick') || + src.includes('googleadservices') || + src.includes('ads system') + ) { + console.log(`[SentienceAPI] Skipping ad iframe: ${src.substring(0, 30)}...`); + return Promise.resolve(null); + } + + return new Promise((resolve) => { + const requestId = `iframe-${idx}-${Date.now()}`; + + // 1. EXTENDED TIMEOUT (Handle slow children) + const timeout = setTimeout(() => { + console.warn(`[SentienceAPI] ⚠️ Iframe ${idx} snapshot TIMEOUT (id: ${requestId})`); + resolve(null); + }, 5000); // Increased to 5s to handle slow processing + + // 2. ROBUST LISTENER with debugging + const listener = (event) => { + // Debug: Log all SENTIENCE_IFRAME_SNAPSHOT_RESPONSE messages to see what's happening + if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE') { + // Only log if it's not our request (for debugging) + if (event.data?.requestId !== requestId) ; + } + + // Check if this is the response we're waiting for + if ( + event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE' && + event.data?.requestId === requestId + ) { + clearTimeout(timeout); + window.removeEventListener('message', listener); + + if (event.data.error) { + console.warn(`[SentienceAPI] Iframe ${idx} returned error:`, event.data.error); + resolve(null); + } else { + const elementCount = event.data.snapshot?.raw_elements?.length || 0; + console.log( + `[SentienceAPI] ✓ Received ${elementCount} elements from Iframe ${idx} (id: ${requestId})` + ); + resolve({ + iframe, + data: event.data.snapshot, + error: null, + }); } + } + }; + + window.addEventListener('message', listener); + + // 3. SEND REQUEST with error handling + try { + if (iframe.contentWindow) { + // console.log(`[SentienceAPI] Sending request to Iframe ${idx} (id: ${requestId})`); + iframe.contentWindow.postMessage( + { + type: 'SENTIENCE_IFRAME_SNAPSHOT_REQUEST', + requestId, + options: { + ...options, + collectIframes: true, // Enable recursion for nested iframes + }, + }, + '*' + ); // Use '*' for cross-origin, but browser will enforce same-origin policy + } else { + console.warn( + `[SentienceAPI] Iframe ${idx} contentWindow is inaccessible (Cross-Origin?)` + ); + clearTimeout(timeout); + window.removeEventListener('message', listener); + resolve(null); + } + } catch (error) { + console.error(`[SentienceAPI] Failed to postMessage to Iframe ${idx}:`, error); + clearTimeout(timeout); + window.removeEventListener('message', listener); + resolve(null); } - invisibleSelectors.forEach(selector => { - try { - clone.querySelectorAll(selector).forEach(el => { - el.parentNode && el.parentNode.removeChild(el); - }); - } catch (e) {} + }); + }); + + // Wait for all iframe responses + const results = await Promise.all(iframePromises); + + // Store iframe data + results.forEach((result, idx) => { + if (result && result.data && !result.error) { + iframeData.set(iframes[idx], result.data); + console.log(`[SentienceAPI] ✓ Collected snapshot from iframe ${idx}`); + } else if (result && result.error) { + console.warn(`[SentienceAPI] Iframe ${idx} snapshot error:`, result.error); + } else if (!result) { + console.warn(`[SentienceAPI] Iframe ${idx} returned no data (timeout or error)`); + } + }); + + return iframeData; + } + + // --- HELPER: Handle Iframe Snapshot Request (for child frames) --- + // When a parent frame requests snapshot, this handler responds with local snapshot + // NOTE: Recursion is safe because querySelectorAll('iframe') only finds direct children. + // Iframe A can ask Iframe B, but won't go back up to parent (no circular dependency risk). + function setupIframeSnapshotHandler() { + window.addEventListener('message', async (event) => { + // Security: only respond to snapshot requests from parent frames + if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_REQUEST') { + const { requestId, options } = event.data; + + try { + // Generate snapshot for this iframe's content + // Allow recursive collection - querySelectorAll('iframe') only finds direct children, + // so Iframe A will ask Iframe B, but won't go back up to parent (safe recursion) + // waitForStability: false makes performance better - i.e. don't wait for children frames + const snapshotOptions = { + ...options, + collectIframes: true, + waitForStability: options.waitForStability === false ? false : false, + }; + const snapshot = await window.sentience.snapshot(snapshotOptions); + + // Send response back to parent + if (event.source && event.source.postMessage) { + event.source.postMessage( + { + type: 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE', + requestId, + snapshot, + error: null, + }, + '*' + ); + } + } catch (error) { + // Send error response + if (event.source && event.source.postMessage) { + event.source.postMessage( + { + type: 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE', + requestId, + snapshot: null, + error: error.message, + }, + '*' + ); + } + } + } + }); + } + + // snapshot.js - Snapshot Method (Main DOM Collection Logic) + + // 1. Geometry snapshot (NEW ARCHITECTURE - No WASM in Main World!) + async function snapshot(options = {}) { + try { + // Step 0: Wait for DOM stability if requested (for SPA hydration) + if (options.waitForStability !== false) { + await waitForStability(options.waitForStability || {}); + } + + // Step 1: Collect raw DOM data (Main World - CSP can't block this!) + const rawData = []; + window.sentience_registry = []; + + const nodes = getAllElements(); + + nodes.forEach((el, idx) => { + if (!el.getBoundingClientRect) return; + const rect = el.getBoundingClientRect(); + if (rect.width < 5 || rect.height < 5) return; + + window.sentience_registry[idx] = el; + + // 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 }); - 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 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, }); - 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; - } - return obj; - } - async function snapshot(options = {}) { + const inView = isInViewport(rect); + + // Get computed style once (needed for both occlusion check and data collection) + const style = window.getComputedStyle(el); + + // Only check occlusion for elements likely to be occluded (optimized) + // This avoids layout thrashing for the vast majority of elements + const occluded = inView ? isOccluded(el, rect, style) : false; + + // Get effective background color (traverses DOM to find non-transparent color) + const effectiveBgColor = getEffectiveBackgroundColor(el); + + rawData.push({ + id: idx, + tag: el.tagName.toLowerCase(), + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + styles: { + display: toSafeString(style.display), + visibility: toSafeString(style.visibility), + opacity: toSafeString(style.opacity), + z_index: toSafeString(style.zIndex || 'auto'), + position: toSafeString(style.position), + bg_color: toSafeString(effectiveBgColor || style.backgroundColor), + color: toSafeString(style.color), + cursor: toSafeString(style.cursor), + font_weight: toSafeString(style.fontWeight), + font_size: toSafeString(style.fontSize), + }, + attributes: { + role: toSafeString(el.getAttribute('role')), + type_: toSafeString(el.getAttribute('type')), + aria_label: + semanticText?.source === 'explicit_aria_label' + ? semanticText.text + : toSafeString(el.getAttribute('aria-label')), // Keep original for backward compat + 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 + href: toSafeString(el.href || el.getAttribute('href') || null), + class: toSafeString(getClassName(el)), + // Capture dynamic input state (not just initial attributes) + value: + el.value !== undefined + ? toSafeString(el.value) + : toSafeString(el.getAttribute('value')), + checked: el.checked !== undefined ? String(el.checked) : null, + }, + text: toSafeString(textVal), + in_viewport: inView, + is_occluded: occluded, + // 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) { try { - !1 !== options.waitForStability && await async function(options = {}) { - const {minNodeCount: minNodeCount = 500, quietPeriod: quietPeriod = 200, maxWait: maxWait = 5e3} = options, startTime = Date.now(); - return new Promise(resolve => { - if (document.querySelectorAll("*").length >= minNodeCount) { - let lastChange = Date.now(); - const observer = new MutationObserver(() => { - lastChange = Date.now(); - }); - 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; - if (currentCount >= minNodeCount) { - observer.disconnect(); - let lastChange = Date.now(); - const quietObserver = new MutationObserver(() => { - lastChange = Date.now(); - }); - 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; - window.sentience_registry[idx] = el; - const 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 value = (el.value || el.placeholder || "").trim(); - if (value) return { - text: value, - source: "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 + console.log(`[SentienceAPI] Starting iframe collection...`); + const iframeSnapshots = await collectIframeSnapshots(options); + console.log( + `[SentienceAPI] Iframe collection complete. Received ${iframeSnapshots.size} snapshot(s)` + ); + + if (iframeSnapshots.size > 0) { + // FLATTEN IMMEDIATELY: Don't nest them. Just append them with coordinate translation. + iframeSnapshots.forEach((iframeSnapshot, iframeEl) => { + // Debug: Log structure to verify data is correct + // console.log(`[SentienceAPI] Processing iframe snapshot:`, iframeSnapshot); + + if (iframeSnapshot && iframeSnapshot.raw_elements) { + const rawElementsCount = iframeSnapshot.raw_elements.length; + console.log( + `[SentienceAPI] Processing ${rawElementsCount} elements from iframe (src: ${iframeEl.src || 'unknown'})` + ); + // Get iframe's bounding rect (offset for coordinate translation) + const iframeRect = iframeEl.getBoundingClientRect(); + const offset = { x: iframeRect.x, y: iframeRect.y }; + + // Get iframe context for frame switching (Playwright needs this) + const iframeSrc = iframeEl.src || iframeEl.getAttribute('src') || ''; + let isSameOrigin = false; + try { + // Try to access contentWindow to check if same-origin + isSameOrigin = iframeEl.contentWindow !== null; + } catch (e) { + isSameOrigin = false; + } + + // Adjust coordinates and add iframe context to each element + const adjustedElements = iframeSnapshot.raw_elements.map((el) => { + const adjusted = { ...el }; + + // Adjust rect coordinates to parent viewport + if (adjusted.rect) { + adjusted.rect = { + ...adjusted.rect, + x: adjusted.rect.x + offset.x, + y: adjusted.rect.y + offset.y, }; - }(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 (!function(el) { - if (!el || !el.tagName) return !1; - const tag = el.tagName.toLowerCase(), role = el.getAttribute ? el.getAttribute("role") : null, hasTabIndex = !!el.hasAttribute && el.hasAttribute("tabindex"), hasHref = "A" === el.tagName && !!el.hasAttribute && el.hasAttribute("href"); - return [ "button", "input", "textarea", "select", "option", "details", "summary", "a" ].includes(tag) ? !("a" === tag && !hasHref) : !(!role || ![ "button", "link", "tab", "menuitem", "checkbox", "radio", "switch", "slider", "combobox", "textbox", "searchbox", "spinbutton" ].includes(role.toLowerCase())) || (!!hasTabIndex || (!!(el.onclick || el.onkeydown || el.onkeypress || el.onkeyup) || !(!el.getAttribute || !(el.getAttribute("onclick") || el.getAttribute("onkeydown") || el.getAttribute("onkeypress") || el.getAttribute("onkeyup"))))); - }(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); - rawData.push({ - id: idx, - tag: el.tagName.toLowerCase(), - rect: { - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height - }, - styles: { - display: toSafeString(style.display), - visibility: toSafeString(style.visibility), - opacity: toSafeString(style.opacity), - z_index: toSafeString(style.zIndex || "auto"), - position: toSafeString(style.position), - bg_color: toSafeString(effectiveBgColor || style.backgroundColor), - color: toSafeString(style.color), - cursor: toSafeString(style.cursor), - font_weight: toSafeString(style.fontWeight), - font_size: toSafeString(style.fontSize) - }, - attributes: { - role: toSafeString(el.getAttribute("role")), - type_: toSafeString(el.getAttribute("type")), - aria_label: "explicit_aria_label" === semanticText?.source ? semanticText.text : toSafeString(el.getAttribute("aria-label")), - 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, - href: toSafeString(el.href || el.getAttribute("href") || null), - class: toSafeString(getClassName(el)), - value: void 0 !== el.value ? toSafeString(el.value) : toSafeString(el.getAttribute("value")), - checked: void 0 !== el.checked ? String(el.checked) : null - }, - text: toSafeString(textVal), - in_viewport: inView, - is_occluded: occluded + } + + // Add iframe context so agents can switch frames in Playwright + adjusted.iframe_context = { + src: iframeSrc, + is_same_origin: isSameOrigin, + }; + + return adjusted; }); + + // Append flattened iframe elements to main array + allRawElements.push(...adjustedElements); + totalIframeElements += adjustedElements.length; + } }); - 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; - } - }); - } catch (error) {} - const 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); - if (!processed || !processed.elements) throw new Error("WASM processing returned invalid result"); - 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; - return { - status: "success", - url: window.location.href, - viewport: { - width: window.innerWidth, - height: window.innerHeight - }, - elements: cleanedElements, - raw_elements: cleanedRawElements, - screenshot: screenshot - }; + + // console.log(`[SentienceAPI] Merged ${iframeSnapshots.size} iframe(s). Total elements: ${allRawElements.length} (${rawData.length} main + ${totalIframeElements} iframe)`); + } } catch (error) { - return { - status: "error", - error: error.message || "Unknown error", - stack: error.stack - }; + console.warn('[SentienceAPI] Iframe collection failed:', error); } + } + + // Step 2: Send EVERYTHING to WASM (One giant flat list) + // Now WASM prunes iframe elements and main elements in one pass! + // No recursion needed - everything is already flat + console.log( + `[SentienceAPI] Sending ${allRawElements.length} total elements to WASM (${rawData.length} main + ${totalIframeElements} iframe)` + ); + const processed = await processSnapshotInBackground(allRawElements, options); + + if (!processed || !processed.elements) { + throw new Error('WASM processing returned invalid result'); + } + + // Step 3: Capture screenshot if requested + let screenshot = null; + if (options.screenshot) { + screenshot = await captureScreenshot(options.screenshot); + } + + // Step 4: Clean and return + const cleanedElements = cleanElement(processed.elements); + const cleanedRawElements = cleanElement(processed.raw_elements); + + // FIXED: Removed undefined 'totalIframeRawElements' + // FIXED: Logic updated for "Flatten Early" architecture. + // processed.elements ALREADY contains the merged iframe elements, + // so we simply use .length. No addition needed. + + const totalCount = cleanedElements.length; + const totalRaw = cleanedRawElements.length; + const iframeCount = totalIframeElements || 0; + + console.log( + `[SentienceAPI] ✓ Complete: ${totalCount} Smart Elements, ${totalRaw} Raw Elements (includes ${iframeCount} from iframes) (WASM took ${processed.duration?.toFixed(1)}ms)` + ); + + return { + status: 'success', + url: window.location.href, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + }, + elements: cleanedElements, + raw_elements: cleanedRawElements, + screenshot, + }; + } 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, + }; } - 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 - }; + } + + // 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); } - 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" - }; - 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; - } + + 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; + } } - 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 - } - }; + + 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; } - function click(id) { - const el = window.sentience_registry[id]; - return !!el && (el.click(), el.focus(), !0); + 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 } - 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); + + // Create reverse mapping for O(1) lookup (fixes registry lookup bug) + window.sentience_registry_map = new Map(); + window.sentience_registry.forEach((el, idx) => { + if (el) window.sentience_registry_map.set(el, idx); + }); + + // Create highlight box overlay + let highlightBox = document.getElementById('sentience-highlight-box'); + if (!highlightBox) { + highlightBox = document.createElement('div'); + highlightBox.id = 'sentience-highlight-box'; + highlightBox.style.cssText = ` + position: fixed; + pointer-events: none; + z-index: 2147483647; + border: 2px solid ${highlightColor}; + background: rgba(255, 0, 0, 0.1); + display: none; + transition: all 0.1s ease; + box-sizing: border-box; + `; + document.body.appendChild(highlightBox); + } + + // Create visual indicator (red border on page when recording) + let recordingIndicator = document.getElementById('sentience-recording-indicator'); + if (!recordingIndicator) { + recordingIndicator = document.createElement('div'); + recordingIndicator.id = 'sentience-recording-indicator'; + recordingIndicator.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + height: 3px; + background: ${highlightColor}; + z-index: 2147483646; + pointer-events: none; + `; + document.body.appendChild(recordingIndicator); + } + recordingIndicator.style.display = 'block'; + + // Hover handler (visual feedback) + const mouseOverHandler = (e) => { + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + + const rect = el.getBoundingClientRect(); + highlightBox.style.display = 'block'; + highlightBox.style.top = rect.top + window.scrollY + 'px'; + highlightBox.style.left = rect.left + window.scrollX + 'px'; + highlightBox.style.width = rect.width + 'px'; + highlightBox.style.height = rect.height + 'px'; + }; + + // Click handler (capture ground truth data) + const clickHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + + // Use Map for reliable O(1) lookup + const sentienceId = window.sentience_registry_map.get(el); + if (sentienceId === undefined) { + console.warn('⚠️ Element not found in Sentience Registry. Did you run snapshot() first?'); + alert('Element not in registry. Run `await window.sentience.snapshot()` first!'); + return; + } + + // Extract raw data (ground truth + raw signals, NOT model outputs) + const rawData = extractRawElementData(el); + const selector = getUniqueSelector(el); + const role = el.getAttribute('role') || el.tagName.toLowerCase(); + const text = getText(el); + + // Build golden set JSON (ground truth + raw signals only) + const snippet = { + task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`, + url: window.location.href, + timestamp: new Date().toISOString(), + target_criteria: { + id: sentienceId, + selector, + 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.'); }); - 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 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, - 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)); - })(); -}(); + }; + + // 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`); + } + + /** + * 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, + 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)'); + })(); + +})(); diff --git a/sentience/extension/pkg/sentience_core.js b/sentience/extension/pkg/sentience_core.js index 2696a64..b232d13 100644 --- a/sentience/extension/pkg/sentience_core.js +++ b/sentience/extension/pkg/sentience_core.js @@ -1,70 +1,112 @@ let wasm; function addHeapObject(obj) { - heap_next === heap.length && heap.push(heap.length + 1); + if (heap_next === heap.length) heap.push(heap.length + 1); const idx = heap_next; - return heap_next = heap[idx], heap[idx] = obj, idx; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; } function debugString(val) { + // primitive types const type = typeof val; - if ("number" == type || "boolean" == type || null == val) return `${val}`; - if ("string" == type) return `"${val}"`; - if ("symbol" == type) { + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { const description = val.description; - return null == description ? "Symbol" : `Symbol(${description})`; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } } - if ("function" == type) { + if (type == 'function') { const name = val.name; - return "string" == typeof name && name.length > 0 ? `Function(${name})` : "Function"; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } } + // objects if (Array.isArray(val)) { const length = val.length; - let debug = "["; - length > 0 && (debug += debugString(val[0])); - for (let i = 1; i < length; i++) debug += ", " + debugString(val[i]); - return debug += "]", debug; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; } + // Test for built-in const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); let className; - if (!(builtInMatches && builtInMatches.length > 1)) return toString.call(val); - if (className = builtInMatches[1], "Object" == className) try { - return "Object(" + JSON.stringify(val) + ")"; - } catch (_) { - return "Object"; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); } - return val instanceof Error ? `${val.name}: ${val.message}\n${val.stack}` : className; + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; } function dropObject(idx) { - idx < 132 || (heap[idx] = heap_next, heap_next = idx); + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; } function getArrayU8FromWasm0(ptr, len) { - return ptr >>>= 0, getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); } let cachedDataViewMemory0 = null; - function getDataViewMemory0() { - return (null === cachedDataViewMemory0 || !0 === cachedDataViewMemory0.buffer.detached || void 0 === cachedDataViewMemory0.buffer.detached && cachedDataViewMemory0.buffer !== wasm.memory.buffer) && (cachedDataViewMemory0 = new DataView(wasm.memory.buffer)), - cachedDataViewMemory0; + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; } function getStringFromWasm0(ptr, len) { - return decodeText(ptr >>>= 0, len); + ptr = ptr >>> 0; + return decodeText(ptr, len); } let cachedUint8ArrayMemory0 = null; - function getUint8ArrayMemory0() { - return null !== cachedUint8ArrayMemory0 && 0 !== cachedUint8ArrayMemory0.byteLength || (cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer)), - cachedUint8ArrayMemory0; + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; } -function getObject(idx) { - return heap[idx]; -} +function getObject(idx) { return heap[idx]; } function handleError(f, args) { try { @@ -74,250 +116,414 @@ function handleError(f, args) { } } -let heap = new Array(128).fill(void 0); - -heap.push(void 0, null, !0, !1); +let heap = new Array(128).fill(undefined); +heap.push(undefined, null, true, false); let heap_next = heap.length; function isLikeNone(x) { - return null == x; + return x === undefined || x === null; } function passStringToWasm0(arg, malloc, realloc) { - if (void 0 === realloc) { - const buf = cachedTextEncoder.encode(arg), ptr = malloc(buf.length, 1) >>> 0; - return getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf), WASM_VECTOR_LEN = buf.length, - ptr; + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; } - let len = arg.length, ptr = malloc(len, 1) >>> 0; + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + const mem = getUint8ArrayMemory0(); + let offset = 0; - for (;offset < len; offset++) { + + for (; offset < len; offset++) { const code = arg.charCodeAt(offset); - if (code > 127) break; + if (code > 0x7F) break; mem[ptr + offset] = code; } if (offset !== len) { - 0 !== offset && (arg = arg.slice(offset)), ptr = realloc(ptr, len, len = offset + 3 * arg.length, 1) >>> 0; + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); - offset += cachedTextEncoder.encodeInto(arg, view).written, ptr = realloc(ptr, len, offset, 1) >>> 0; + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; } - return WASM_VECTOR_LEN = offset, ptr; + + WASM_VECTOR_LEN = offset; + return ptr; } function takeObject(idx) { const ret = getObject(idx); - return dropObject(idx), ret; + dropObject(idx); + return ret; } -let cachedTextDecoder = new TextDecoder("utf-8", { - ignoreBOM: !0, - fatal: !0 -}); - +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); cachedTextDecoder.decode(); - const MAX_SAFARI_DECODE_BYTES = 2146435072; - let numBytesDecoded = 0; - function decodeText(ptr, len) { - return numBytesDecoded += len, numBytesDecoded >= MAX_SAFARI_DECODE_BYTES && (cachedTextDecoder = new TextDecoder("utf-8", { - ignoreBOM: !0, - fatal: !0 - }), cachedTextDecoder.decode(), numBytesDecoded = len), cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); } -const cachedTextEncoder = new TextEncoder; +const cachedTextEncoder = new TextEncoder(); -"encodeInto" in cachedTextEncoder || (cachedTextEncoder.encodeInto = function(arg, view) { - const buf = cachedTextEncoder.encode(arg); - return view.set(buf), { - read: arg.length, - written: buf.length - }; -}); +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + } +} let WASM_VECTOR_LEN = 0; +/** + * @param {any} val + * @returns {any} + */ export function analyze_page(val) { - return takeObject(wasm.analyze_page(addHeapObject(val))); + const ret = wasm.analyze_page(addHeapObject(val)); + return takeObject(ret); } +/** + * @param {any} val + * @param {any} options + * @returns {any} + */ export function analyze_page_with_options(val, options) { - return takeObject(wasm.analyze_page_with_options(addHeapObject(val), addHeapObject(options))); + const ret = wasm.analyze_page_with_options(addHeapObject(val), addHeapObject(options)); + return takeObject(ret); } +/** + * @param {any} _raw_elements + */ export function decide_and_act(_raw_elements) { wasm.decide_and_act(addHeapObject(_raw_elements)); } +/** + * Prune raw elements before sending to API + * This is a "dumb" filter that reduces payload size without leaking proprietary IP + * Filters out: tiny elements, invisible elements, non-interactive wrapper divs + * Amazon: 5000-6000 elements -> ~200-400 elements (~95% reduction) + * @param {any} val + * @returns {any} + */ export function prune_for_api(val) { - return takeObject(wasm.prune_for_api(addHeapObject(val))); + const ret = wasm.prune_for_api(addHeapObject(val)); + return takeObject(ret); } -const EXPECTED_RESPONSE_TYPES = new Set([ "basic", "cors", "default" ]); +const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); async function __wbg_load(module, imports) { - if ("function" == typeof Response && module instanceof Response) { - if ("function" == typeof WebAssembly.instantiateStreaming) try { - return await WebAssembly.instantiateStreaming(module, imports); - } catch (e) { - if (!(module.ok && EXPECTED_RESPONSE_TYPES.has(module.type)) || "application/wasm" === module.headers.get("Content-Type")) throw e; + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } } + const bytes = await module.arrayBuffer(); return await WebAssembly.instantiate(bytes, imports); - } - { + } else { const instance = await WebAssembly.instantiate(module, imports); - return instance instanceof WebAssembly.Instance ? { - instance: instance, - module: module - } : instance; + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } } } function __wbg_get_imports() { - const imports = { - wbg: {} - }; - return imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) { - return addHeapObject(Error(getStringFromWasm0(arg0, arg1))); - }, imports.wbg.__wbg_Number_2d1dcfcf4ec51736 = function(arg0) { - return Number(getObject(arg0)); - }, imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) { - const v = getObject(arg1), ret = "bigint" == typeof v ? v : void 0; - getDataViewMemory0().setBigInt64(arg0 + 8, isLikeNone(ret) ? BigInt(0) : ret, !0), - getDataViewMemory0().setInt32(arg0 + 0, !isLikeNone(ret), !0); - }, imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { - const v = getObject(arg0), ret = "boolean" == typeof v ? v : void 0; - return isLikeNone(ret) ? 16777215 : ret ? 1 : 0; - }, imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) { - const ptr1 = passStringToWasm0(debugString(getObject(arg1)), wasm.__wbindgen_export, wasm.__wbindgen_export2), len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4, len1, !0), getDataViewMemory0().setInt32(arg0 + 0, ptr1, !0); - }, imports.wbg.__wbg___wbindgen_in_0d3e1e8f0c669317 = function(arg0, arg1) { - return getObject(arg0) in getObject(arg1); - }, imports.wbg.__wbg___wbindgen_is_bigint_0e1a2e3f55cfae27 = function(arg0) { - return "bigint" == typeof getObject(arg0); - }, imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { - return "function" == typeof getObject(arg0); - }, imports.wbg.__wbg___wbindgen_is_object_ce774f3490692386 = function(arg0) { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_Number_2d1dcfcf4ec51736 = function(arg0) { + const ret = Number(getObject(arg0)); + return ret; + }; + imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) { + const v = getObject(arg1); + const ret = typeof(v) === 'bigint' ? v : undefined; + getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }; + imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { + const v = getObject(arg0); + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }; + imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg___wbindgen_in_0d3e1e8f0c669317 = function(arg0, arg1) { + const ret = getObject(arg0) in getObject(arg1); + return ret; + }; + imports.wbg.__wbg___wbindgen_is_bigint_0e1a2e3f55cfae27 = function(arg0) { + const ret = typeof(getObject(arg0)) === 'bigint'; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_object_ce774f3490692386 = function(arg0) { const val = getObject(arg0); - return "object" == typeof val && null !== val; - }, imports.wbg.__wbg___wbindgen_is_undefined_f6b95eab589e0269 = function(arg0) { - return void 0 === getObject(arg0); - }, imports.wbg.__wbg___wbindgen_jsval_eq_b6101cc9cef1fe36 = function(arg0, arg1) { - return getObject(arg0) === getObject(arg1); - }, imports.wbg.__wbg___wbindgen_jsval_loose_eq_766057600fdd1b0d = function(arg0, arg1) { - return getObject(arg0) == getObject(arg1); - }, imports.wbg.__wbg___wbindgen_number_get_9619185a74197f95 = function(arg0, arg1) { - const obj = getObject(arg1), ret = "number" == typeof obj ? obj : void 0; - getDataViewMemory0().setFloat64(arg0 + 8, isLikeNone(ret) ? 0 : ret, !0), getDataViewMemory0().setInt32(arg0 + 0, !isLikeNone(ret), !0); - }, imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) { - const obj = getObject(arg1), ret = "string" == typeof obj ? obj : void 0; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2), len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4, len1, !0), getDataViewMemory0().setInt32(arg0 + 0, ptr1, !0); - }, imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { + const ret = typeof(val) === 'object' && val !== null; + return ret; + }; + imports.wbg.__wbg___wbindgen_is_undefined_f6b95eab589e0269 = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }; + imports.wbg.__wbg___wbindgen_jsval_eq_b6101cc9cef1fe36 = function(arg0, arg1) { + const ret = getObject(arg0) === getObject(arg1); + return ret; + }; + imports.wbg.__wbg___wbindgen_jsval_loose_eq_766057600fdd1b0d = function(arg0, arg1) { + const ret = getObject(arg0) == getObject(arg1); + return ret; + }; + imports.wbg.__wbg___wbindgen_number_get_9619185a74197f95 = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }; + imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); - }, imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { - return handleError(function(arg0, arg1) { - return addHeapObject(getObject(arg0).call(getObject(arg1))); - }, arguments); - }, imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) { - return getObject(arg0).done; - }, imports.wbg.__wbg_error_7bc7d576a6aaf855 = function(arg0) {}, imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) { - return addHeapObject(getObject(arg0)[arg1 >>> 0]); - }, imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { - return handleError(function(arg0, arg1) { - return addHeapObject(Reflect.get(getObject(arg0), getObject(arg1))); - }, arguments); - }, imports.wbg.__wbg_get_with_ref_key_1dc361bd10053bfe = function(arg0, arg1) { - return addHeapObject(getObject(arg0)[getObject(arg1)]); - }, imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) { + }; + imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) { + const ret = getObject(arg0).done; + return ret; + }; + imports.wbg.__wbg_error_7bc7d576a6aaf855 = function(arg0) { + console.error(getObject(arg0)); + }; + imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_get_with_ref_key_1dc361bd10053bfe = function(arg0, arg1) { + const ret = getObject(arg0)[getObject(arg1)]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) { let result; try { result = getObject(arg0) instanceof ArrayBuffer; } catch (_) { - result = !1; + result = false; } - return result; - }, imports.wbg.__wbg_instanceof_Uint8Array_da54ccc9d3e09434 = function(arg0) { + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Uint8Array_da54ccc9d3e09434 = function(arg0) { let result; try { result = getObject(arg0) instanceof Uint8Array; } catch (_) { - result = !1; + result = false; } - return result; - }, imports.wbg.__wbg_isArray_51fd9e6422c0a395 = function(arg0) { - return Array.isArray(getObject(arg0)); - }, imports.wbg.__wbg_isSafeInteger_ae7d3f054d55fa16 = function(arg0) { - return Number.isSafeInteger(getObject(arg0)); - }, imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { - return addHeapObject(Symbol.iterator); - }, imports.wbg.__wbg_js_click_element_2fe1e774f3d232c7 = function(arg0) { + const ret = result; + return ret; + }; + imports.wbg.__wbg_isArray_51fd9e6422c0a395 = function(arg0) { + const ret = Array.isArray(getObject(arg0)); + return ret; + }; + imports.wbg.__wbg_isSafeInteger_ae7d3f054d55fa16 = function(arg0) { + const ret = Number.isSafeInteger(getObject(arg0)); + return ret; + }; + imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { + const ret = Symbol.iterator; + return addHeapObject(ret); + }; + imports.wbg.__wbg_js_click_element_2fe1e774f3d232c7 = function(arg0) { js_click_element(arg0); - }, imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { - return getObject(arg0).length; - }, imports.wbg.__wbg_length_d45040a40c570362 = function(arg0) { - return getObject(arg0).length; - }, imports.wbg.__wbg_new_1ba21ce319a06297 = function() { - return addHeapObject(new Object); - }, imports.wbg.__wbg_new_25f239778d6112b9 = function() { - return addHeapObject(new Array); - }, imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) { - return addHeapObject(new Uint8Array(getObject(arg0))); - }, imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) { - return addHeapObject(getObject(arg0).next); - }, imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { - return handleError(function(arg0) { - return addHeapObject(getObject(arg0).next()); - }, arguments); - }, imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { + }; + imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_length_d45040a40c570362 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_new_1ba21ce319a06297 = function() { + const ret = new Object(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_25f239778d6112b9 = function() { + const ret = new Array(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); + }; + imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2)); - }, imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { + }; + imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { getObject(arg0)[takeObject(arg1)] = takeObject(arg2); - }, imports.wbg.__wbg_set_7df433eea03a5c14 = function(arg0, arg1, arg2) { + }; + imports.wbg.__wbg_set_7df433eea03a5c14 = function(arg0, arg1, arg2) { getObject(arg0)[arg1 >>> 0] = takeObject(arg2); - }, imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) { - return addHeapObject(getObject(arg0).value); - }, imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { - return addHeapObject(getStringFromWasm0(arg0, arg1)); - }, imports.wbg.__wbindgen_cast_4625c577ab2ec9ee = function(arg0) { - return addHeapObject(BigInt.asUintN(64, arg0)); - }, imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { - return addHeapObject(arg0); - }, imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - return addHeapObject(getObject(arg0)); - }, imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + }; + imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_cast_4625c577ab2ec9ee = function(arg0) { + // Cast intrinsic for `U64 -> Externref`. + const ret = BigInt.asUintN(64, arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); - }, imports; + }; + + return imports; } function __wbg_finalize_init(instance, module) { - return wasm = instance.exports, __wbg_init.__wbindgen_wasm_module = module, cachedDataViewMemory0 = null, - cachedUint8ArrayMemory0 = null, wasm; + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + + + + return wasm; } function initSync(module) { - if (void 0 !== wasm) return wasm; - void 0 !== module && Object.getPrototypeOf(module) === Object.prototype && ({module: module} = module); + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + const imports = __wbg_get_imports(); - module instanceof WebAssembly.Module || (module = new WebAssembly.Module(module)); - return __wbg_finalize_init(new WebAssembly.Instance(module, imports), module); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); } async function __wbg_init(module_or_path) { - if (void 0 !== wasm) return wasm; - void 0 !== module_or_path && Object.getPrototypeOf(module_or_path) === Object.prototype && ({module_or_path: module_or_path} = module_or_path), - void 0 === module_or_path && (module_or_path = new URL("sentience_core_bg.wasm", import.meta.url)); + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('sentience_core_bg.wasm', import.meta.url); + } const imports = __wbg_get_imports(); - ("string" == typeof module_or_path || "function" == typeof Request && module_or_path instanceof Request || "function" == typeof URL && module_or_path instanceof URL) && (module_or_path = fetch(module_or_path)); - const {instance: instance, module: module} = await __wbg_load(await module_or_path, imports); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + return __wbg_finalize_init(instance, module); } export { initSync }; - export default __wbg_init; diff --git a/sentience/extension/pkg/sentience_core_bg.wasm b/sentience/extension/pkg/sentience_core_bg.wasm index 259298c3d76174fd80a930f93c18a2f0caa24b6b..b001b68c9613422d2d6fd7c88ee28e062472894e 100644 GIT binary patch delta 34281 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 delta 30370 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≠;bWbLE`ZUN1iP-=qWBJp{}9Pa+EmVej=8X(AD~m`vi5P+}vAL z=oQqZE#Xec-k>V!RxzR*mC$}Mw;S~>u7z$QZpXeoEbLxb44y;C1Um@=j{KyUD;A44 zyU{orB06@bc*l+I7}O{lWfRs5ITW)}ywIHn<=(FLCI#CprwW?AP?$Za_!tVWpP0hU zsT7`Rq3}o#>OSBW9Xf(dO=$`KRtdjpgNs&T5xV=&4@-scf%>aNaMa8n zbaB>6)W!D#96ociSTULk#YMf*s-_R6w})?KUa8Iv|BHqv-%eymH(8qzGm13G9|Kom zI}8M>t*a{@FNP!!jCeetiOU91_BrhTaY+VOWkZckk&7iGe;|Rw4F|k&S@GN4p;;*s;*hD|!Pd_A&xI*KN0gYx>s=sF;miMK+ zPV&Iak-DpNitE3JOt8#sL>TcLjJrZ2E;yM=1DYynqWWZdGJfX?%=h153BfCj8=OQL z;Y8L0I|sSL;K@BS&64Yn?YKe>DD%yF1?GXKe4~VEX7=&Q9@`#jS;Xqeym+USvb@q* zi$Ueo6G}IuoX(=r;<<9F;2n*ZBzj=0K`6L88s7|y+z@ZMM9uGsi3KI}&wbM9tu z1GC7{Ec{AC02gA->`!e1XJ8^=5E?~~{uszN;>`XeI0g%mkgu}U?v8iLS0(btp3HCw-PnmPoZJ-x-bV(PPbZpR#cPjBcJmv z=m3BDYw*|XP*_Aj)W@`l#ELfuQ7q9Fj48nlI64Tsz(P@RDun%(xcpS=mbDco1(K9A z0k!m)?}+t6ZKqa4K~iD4z;oXSIM)WH!osXv`+O z1cu)t`%LXGtfLA18#b6+=#=&UAyAej0U)*RAHcf?vdE0uyqE8f^qDaWjor zg$IuWwD3&?0jU2hf`HwJEz)u2viqO~Whes5?n4%onI(p}Pgsg(Z|wu zpPwdayYEetwB1*wDYV@er0O|cT8j-c- z)#K1-pDxiQwdVg^z2q0G{X=O8{Zot{O3w%2)Aax&x`+&eq?U?a!>An_%hAIqmh&9H z6p;duf2Iww1zuhYhf#qSRvM1V+F?}OZkv3X0+Wy!Rco5}2+wdDY{5*kYc6%s6XTGC zuC7F|jQPG&OdL*S^odwGocjBpW&Z%X`@6#_hA(_H^ACnta|S+HOWpML4X1Ya6hbp^ zKXGMy{pJ%?oHl}n`I@CuAeN4RqxiPyIuh$(g*bC0`@4$E)^X|VYw$u3rA6R`Q`@#CG!qlY#N207K-;r(Mcg?tN>pjo*6~4 z+=T|*ICj6;A7?YhF}U#W#Jsbpb8nyxQdR@j+1y0Hf`cT(K8(zE(K7?qXcn($GCw0R zd^F;Lx9i7?aK;#TPm_NEKH|i1VV{9XNu3Bu<(lKMKRpBS#l^xnle&kVkRBhL&xNA@ znbbaT2*)kEkNK&%^i1kxJ_MJFKU={I9vCST>)=dmRJVzb(KPcC6=1NWhhk~w`=a?Q z9Maa|LASX^v^|^pc;D8s1ljw`4P#4V*}*8U5VOywL8T|Q**e>7UD|A2-fYSk@dKji zY=mqNBaA+lib5M&nC>+ev$bA~8w*u=L0mnSPNFZxqhrZF3-3P=K<2+)gcPSznc_Gn z(=s>3D>+%_cX2YqFX3bcPLRs*!{k>Dar-zN&KHR~Bz#)gJ`QoQ8vJSv>qE`rr*V`Y zkVjAr_Nn7J(B;p?#plo^<_6c-h84py`jNzI=TPqs(orI#!ji6n&U+WD;wSjU>#yx^ zsOYq{982L744hZ?k<4{aFRU>c-JZd}VFWNw=5mCu_t}6b>WO|DC&!arLQT*dP8WzGxp#gj`E9~>BtsgIoWZ`$` zLs#}AilK{|OUV*XpHI0Ep)Pm4D%pkhxG9#x53FQft=Zx$AbT^CT1r&f2NJg4I?#KBG> zaq*!G>C$*JLMc#U1V50|F-NvGQn_WeR?2Y@KG|6+B*NY&JX!=v7@t#}jRh+{mIwYy zlGI0;LLX%ceUvHmQKryGnX*1|Mtx9UEtXzHuT>1jMLG1qGVtjcf9{%Y_79KIbY_bM z>3$%^yRr*$zmJmpG!B{~Iv5A!KmL|t=KDB2oA*IpY_J06bK=jx#ZLU7ur8)9-R0$L zAocP!V18-HheinSvG*sB)S7+xJ~8)V>XrKnABABK+2c~DV$QxMHeF0ZqcL6CG1OUF|?)*H9DZ9L`VRgoNlJQOftbjA+$d*Yk% z)LH*GiL)<*VL0hBxC<{CTqvinm|91%{xTXU#!aADS}oiStl~5r2zHC%6R0$8a}u(7 zw}?+bqu0eV6R1o0UPJbPVJ{M&PoVi?#pTq2o);S~2b(e}C@SaaW*H*xg&fDh*nS`| zDa{S%_lS~-)H{FQTD4>~`5^22OV+s6od|s zIwCR!=jY3&P+Nq30PBNfz+_a_trUAE!$!O~nR4_kD@DbWL}5oufg7RXWc{Gq>S#+{ zj(B|%rW~X_V*3;rh*!ly(D!5C;lIu$zL`R$ei&cr{9H7Zio(0$NP%kvC9B1fsaQP+ zU`cT)@docyfNm4dO{FpV&O3yE8kOqJcZl+7G$`$#G)#K+;ci6Y=kYLU3&ma2Xdrfs zUDK$0uWhhH-J$sMO90+;_)of^gr$!^>!R8GpkEpTev`k%5I;|&j`4*!kt*|Ek`y@t z1RS+Ae`J?XN}kU?i?K`#=%0MWISFf2F)J{NP4Vs+uf7RrFN0%AP|3$imV#Z7=DSsMcvUv1v@X}Cb z7qhASNLXJj96L@t+h^hu>1SPhK9h=iZ{`n(yyj5eE9@7z;4z1W3s}#RPEKN%u~@-` zbp^F&lekgXS5TYK54>i3o1Yq*tl@q6E``Kdrv?8lvGxk;z%8Fn=9-eZW|`wAqNT|* z$?Er$xxdO>E)X}fXMOD42WC^Z?rV&26g9l%E_x5!D`H6Yp*;;4lz9{7y)mjabr40|}gQY-X1a34Mp#dE0R_}$V0=!Ls&+>U7B_AE%4 zO|3*fxLW=RbZq)1`hJ;Dtv#Ajdsx-Bm4lO;9c(i1sOH1s!8tI0?~3>4Pf= z1fC@x?=1^OySX$fCkH)SSaJA^r7FzCM-VY#F15|ali_e)qFwScPzdK%(J+_t@^}4G z{SsLp$Dj8+arb z88DW8xGezeyJ9e$@rVoNQC56OHT2G7jzx!Zpx7Mj+GT(R&7P^SRkusL(^Fxa85S`6 zq{4RA02VTPrNZj?gK^zFO~Le3{>6cLbP;ZghRmnFU0`K3^G1We%2#X}=75ZFrpvd| zBlAVEdOqc~zs)`5@pL3nU|=+pk2<^NQ~UB2__I?Z!WpW$nF+5owVdUa&DP{ci=qV- zE&R%0qNF6oJ>!E=$v%5u6BbZmykD|cmoYQ}t<)w%B|yrY-7L)<9Zo}UJTvCQMu9A# zfo}epGFob$cWWft78I15CM9BelyEdV8-LQxJVfDi5aUkaTjKa=UWx0-6=J}Z^xODy zPrioH+lO9-^t%ItADeUk=1S(}GeZ@ghc=k6yM--Wcvlr{yr0(1pJf;L3*r3t8p13E z^hox(LPgDEiIk~wbJoP@oR%`k6lL@Nvj#IkEBBVHow%vCraF}gC2 zjBXwV$j=#=e=jOBROucashQ67k+yNuY)*ZQU5v!M1G8nIg$EGz%fKcDG2?&Xf1i)o zbX66$#@**{D!7(#KR9jn6_b16|BY_exPjUQM|n*x zq-oldqUHwbb?PJ(JENEYuR{Kel=3Ljw3AStHG9_NjllIFKW+M?Nt0)3T7xKzQ>XF+ zF8G|u6Bk@_?bXvKEtocK^7N_G76A1K>OevJ!A*ZeiY-|C*(>J8X`DC^r;~^E@tK+q zU|*ydy4DXV_j3wTA5wh!DOn!l!p}w;2K={fIvHuYsIH=u#P%w>=AxN?QwyMe0n#+2 zRY)U9mm%eLM_oFlC^IvCkg`YyAx%d*8Y#CsA1Sw+h?M!6DpoI~@o`_k)chz$kaDwJ zx7-eB4DWz4ci+WLdm`m-PD0AV8Gw}87>YEAbmsIcrZ1Q>eddD6^RzWC?s^xu4E0-( zXBkePGH=2aldtNE|6{`06Rw;zeRiXZH25T;I^XI%yY`;MubGIREX1=D9uwd6>x%Z{L)IZm{lIBmi_{9iW8 z)_D=dNTP23f_dCh>kBx~)aBPMm~2g$C%#)uH}@$4pc{U=<7a|(`Rv&Q)*^H?<;^_Zp@)7AavGcv?5x-Y0@x%YPXNH&KryqX$<7WVV z@CreSf$Q+$jW!5Br{d={VJ=00bFk>K6k*k~#brxraQm|%dLHT~U~~Ve9ae}fOQ~Zp z+cdR8R1FjRmr^IPMc`J7mo@^*V+nzPg_JQ^!-n7|!SU(H55>>0O&f2e3{U19$fW?~ z4OlB(kj~k(dpXY0nP(t>CVtMs&p_*X>v};obc(>^H7^fhj;N@izf*Tnwt{+Pc1MSu z0Nsoq?y$R2{X!tozD6@us zij;W@#?+i Date: Fri, 9 Jan 2026 19:16:51 -0800 Subject: [PATCH 4/6] fix none for dominant group key --- sentience/__init__.py | 7 +------ sentience/ordinal.py | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/sentience/__init__.py b/sentience/__init__.py index af3ace5..d3663df 100644 --- a/sentience/__init__.py +++ b/sentience/__init__.py @@ -79,12 +79,7 @@ ) # Ordinal support (Phase 3) -from .ordinal import ( - OrdinalIntent, - boost_ordinal_elements, - detect_ordinal_intent, - select_by_ordinal, -) +from .ordinal import OrdinalIntent, boost_ordinal_elements, detect_ordinal_intent, select_by_ordinal from .overlay import clear_overlay, show_overlay from .query import find, query from .read import read diff --git a/sentience/ordinal.py b/sentience/ordinal.py index cd40237..ee66a37 100644 --- a/sentience/ordinal.py +++ b/sentience/ordinal.py @@ -18,9 +18,9 @@ element = select_by_ordinal(elements, dominant_group_key, intent) """ +import re from dataclasses import dataclass from typing import Literal -import re from sentience.models import Element @@ -278,5 +278,3 @@ def boost_ordinal_elements( result.append(copy) return result - - From 3eb35cb1112e90093af95aeb50ddc5fa684c8f1a Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Fri, 9 Jan 2026 21:34:15 -0800 Subject: [PATCH 5/6] optional href field to element --- sentience/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentience/models.py b/sentience/models.py index fbce127..6c99db4 100644 --- a/sentience/models.py +++ b/sentience/models.py @@ -61,6 +61,9 @@ class Element(BaseModel): group_key: str | None = None # Geometric bucket key for ordinal grouping group_index: int | None = None # Position within group (0-indexed, sorted by doc_y) + # Hyperlink URL (for link elements) + href: str | None = None + class Snapshot(BaseModel): """Snapshot response from extension""" From e75250282b13b850742a45c60492338b7e814111 Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Sat, 10 Jan 2026 06:50:54 -0800 Subject: [PATCH 6/6] new field in_dominant_group --- sentience/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentience/models.py b/sentience/models.py index 6c99db4..c286b85 100644 --- a/sentience/models.py +++ b/sentience/models.py @@ -64,6 +64,11 @@ class Element(BaseModel): # Hyperlink URL (for link elements) href: str | None = None + # Phase 3.2: Pre-computed dominant group membership (uses fuzzy matching) + # This field is computed by the gateway so downstream consumers don't need to + # implement fuzzy matching logic themselves. + in_dominant_group: bool | None = None + class Snapshot(BaseModel): """Snapshot response from extension"""