sentienceapi 0.90.16__py3-none-any.whl → 0.98.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -1,1473 +1,1193 @@
1
- // injected_api.js - MAIN WORLD (NO WASM! CSP-Resistant!)
2
- // This script ONLY collects raw DOM data and sends it to background for processing
3
- (async () => {
4
- // console.log('[SentienceAPI] Initializing (CSP-Resistant Mode)...');
5
-
6
- // Wait for Extension ID from content.js
7
- const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId;
8
- let extId = getExtensionId();
9
-
10
- if (!extId) {
11
- await new Promise(resolve => {
12
- const check = setInterval(() => {
13
- extId = getExtensionId();
14
- if (extId) { clearInterval(check); resolve(); }
15
- }, 50);
16
- setTimeout(() => resolve(), 5000); // Max 5s wait
17
- });
1
+ !function() {
2
+ "use strict";
3
+ function getAllElements(root = document) {
4
+ const elements = [], filter = {
5
+ 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
6
+ }, walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter);
7
+ for (;walker.nextNode(); ) {
8
+ const node = walker.currentNode;
9
+ node.isConnected && (elements.push(node), node.shadowRoot && elements.push(...getAllElements(node.shadowRoot)));
10
+ }
11
+ return elements;
18
12
  }
19
-
20
- if (!extId) {
21
- console.error('[SentienceAPI] Failed to get extension ID');
22
- return;
13
+ const CAPTCHA_TEXT_KEYWORDS = [ "verify you are human", "captcha", "human verification", "unusual traffic", "are you a robot", "security check", "prove you are human", "bot detection", "automated access" ], CAPTCHA_URL_HINTS = [ "captcha", "challenge", "verify" ], CAPTCHA_IFRAME_HINTS = {
14
+ recaptcha: [ "recaptcha", "google.com/recaptcha" ],
15
+ hcaptcha: [ "hcaptcha.com" ],
16
+ turnstile: [ "challenges.cloudflare.com", "turnstile" ],
17
+ arkose: [ "arkoselabs.com", "funcaptcha.com", "client-api.arkoselabs.com" ],
18
+ awswaf: [ "amazonaws.com/captcha", "awswaf.com" ]
19
+ }, CAPTCHA_SCRIPT_HINTS = {
20
+ recaptcha: [ "recaptcha" ],
21
+ hcaptcha: [ "hcaptcha" ],
22
+ turnstile: [ "turnstile", "challenges.cloudflare.com" ],
23
+ arkose: [ "arkoselabs", "funcaptcha" ],
24
+ awswaf: [ "captcha.awswaf", "awswaf-captcha" ]
25
+ }, CAPTCHA_CONTAINER_SELECTORS = [ {
26
+ selector: ".g-recaptcha",
27
+ provider: "recaptcha"
28
+ }, {
29
+ selector: "#g-recaptcha",
30
+ provider: "recaptcha"
31
+ }, {
32
+ selector: "[data-sitekey]",
33
+ provider: "unknown"
34
+ }, {
35
+ selector: 'iframe[title*="recaptcha" i]',
36
+ provider: "recaptcha"
37
+ }, {
38
+ selector: ".h-captcha",
39
+ provider: "hcaptcha"
40
+ }, {
41
+ selector: "#h-captcha",
42
+ provider: "hcaptcha"
43
+ }, {
44
+ selector: 'iframe[title*="hcaptcha" i]',
45
+ provider: "hcaptcha"
46
+ }, {
47
+ selector: ".cf-turnstile",
48
+ provider: "turnstile"
49
+ }, {
50
+ selector: "[data-cf-turnstile-sitekey]",
51
+ provider: "turnstile"
52
+ }, {
53
+ selector: 'iframe[src*="challenges.cloudflare.com"]',
54
+ provider: "turnstile"
55
+ }, {
56
+ selector: "#FunCaptcha",
57
+ provider: "arkose"
58
+ }, {
59
+ selector: ".funcaptcha",
60
+ provider: "arkose"
61
+ }, {
62
+ selector: "[data-arkose-public-key]",
63
+ provider: "arkose"
64
+ }, {
65
+ selector: 'iframe[src*="arkoselabs"]',
66
+ provider: "arkose"
67
+ }, {
68
+ selector: "#captcha-container",
69
+ provider: "awswaf"
70
+ }, {
71
+ selector: "[data-awswaf-captcha]",
72
+ provider: "awswaf"
73
+ }, {
74
+ selector: 'iframe[title*="captcha" i]',
75
+ provider: "unknown"
76
+ } ];
77
+ function addEvidence(list, value) {
78
+ value && (list.length >= 5 || list.push(value));
23
79
  }
24
-
25
- // console.log('[SentienceAPI] Extension ID:', extId);
26
-
27
- // Registry for click actions (still needed for click() function)
28
- window.sentience_registry = [];
29
-
30
- // --- HELPER: Deep Walker with Native Filter ---
31
- function getAllElements(root = document) {
32
- const elements = [];
33
- const filter = {
34
- acceptNode: function(node) {
35
- // Skip metadata and script/style tags
36
- if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD'].includes(node.tagName)) {
37
- return NodeFilter.FILTER_REJECT;
80
+ function truncateText(text, maxLen) {
81
+ return text ? text.length <= maxLen ? text : text.slice(0, maxLen) : "";
82
+ }
83
+ function matchHints(value, hints) {
84
+ const lower = String(value || "").toLowerCase();
85
+ return !!lower && hints.some(hint => lower.includes(hint));
86
+ }
87
+ function detectCaptcha() {
88
+ const evidence = {
89
+ text_hits: [],
90
+ selector_hits: [],
91
+ iframe_src_hits: [],
92
+ url_hits: []
93
+ };
94
+ let hasIframeHit = !1, hasContainerHit = !1, hasScriptHit = !1, hasKeywordHit = !1, hasUrlHit = !1;
95
+ const providerSignals = {
96
+ recaptcha: 0,
97
+ hcaptcha: 0,
98
+ turnstile: 0,
99
+ arkose: 0,
100
+ awswaf: 0
101
+ };
102
+ try {
103
+ const iframes = document.querySelectorAll("iframe");
104
+ for (const iframe of iframes) {
105
+ const src = iframe.getAttribute("src") || "", title = iframe.getAttribute("title") || "";
106
+ if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0,
107
+ providerSignals[provider] += 1, addEvidence(evidence.iframe_src_hits, truncateText(src, 120)));
108
+ if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0,
109
+ addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]')), evidence.iframe_src_hits.length >= 5) break;
110
+ }
111
+ } catch (e) {}
112
+ try {
113
+ const scripts = document.querySelectorAll("script[src]");
114
+ for (const script of scripts) {
115
+ const src = script.getAttribute("src") || "";
116
+ if (src) {
117
+ for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) matchHints(src, hints) && (hasScriptHit = !0,
118
+ providerSignals[provider] += 1, addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`));
119
+ if (evidence.selector_hits.length >= 5) break;
38
120
  }
39
- // Skip deep SVG children
40
- if (node.parentNode && node.parentNode.tagName === 'SVG' && node.tagName !== 'SVG') {
41
- return NodeFilter.FILTER_REJECT;
121
+ }
122
+ } catch (e) {}
123
+ for (const {selector: selector, provider: provider} of CAPTCHA_CONTAINER_SELECTORS) try {
124
+ document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector),
125
+ "unknown" !== provider && (providerSignals[provider] += 1));
126
+ } catch (e) {}
127
+ const textSnippet = function() {
128
+ try {
129
+ const candidates = document.querySelectorAll("h1, h2, h3, h4, p, label, button, form, div, span");
130
+ let combined = "", count = 0;
131
+ for (const node of candidates) {
132
+ if (count >= 30 || combined.length >= 2e3) break;
133
+ if (!node || "string" != typeof node.innerText) continue;
134
+ if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue;
135
+ const text = node.innerText.replace(/\s+/g, " ").trim();
136
+ text && (combined += `${text} `, count += 1);
42
137
  }
43
- return NodeFilter.FILTER_ACCEPT;
138
+ if (combined = combined.trim(), combined) return truncateText(combined, 2e3);
139
+ } catch (e) {}
140
+ try {
141
+ let bodyText = document.body?.innerText || "";
142
+ return !bodyText && document.body?.textContent && (bodyText = document.body.textContent),
143
+ truncateText(bodyText.replace(/\s+/g, " ").trim(), 2e3);
144
+ } catch (e) {
145
+ return "";
44
146
  }
147
+ }();
148
+ if (textSnippet) {
149
+ const lowerText = textSnippet.toLowerCase();
150
+ for (const keyword of CAPTCHA_TEXT_KEYWORDS) lowerText.includes(keyword) && (hasKeywordHit = !0,
151
+ addEvidence(evidence.text_hits, keyword));
152
+ }
153
+ try {
154
+ const lowerUrl = (window.location?.href || "").toLowerCase();
155
+ for (const hint of CAPTCHA_URL_HINTS) lowerUrl.includes(hint) && (hasUrlHit = !0,
156
+ addEvidence(evidence.url_hits, hint));
157
+ } catch (e) {}
158
+ let confidence = 0;
159
+ hasIframeHit && (confidence += .7), hasContainerHit && (confidence += .5), hasScriptHit && (confidence += .5),
160
+ hasKeywordHit && (confidence += .3), hasUrlHit && (confidence += .2), confidence = Math.min(1, confidence),
161
+ hasIframeHit && (confidence = Math.max(confidence, .8)), !hasKeywordHit || hasIframeHit || hasContainerHit || hasScriptHit || hasUrlHit || (confidence = Math.min(confidence, .4));
162
+ const detected = confidence >= .7;
163
+ let providerHint = null;
164
+ return providerSignals.recaptcha > 0 ? providerHint = "recaptcha" : providerSignals.hcaptcha > 0 ? providerHint = "hcaptcha" : providerSignals.turnstile > 0 ? providerHint = "turnstile" : providerSignals.arkose > 0 ? providerHint = "arkose" : providerSignals.awswaf > 0 ? providerHint = "awswaf" : detected && (providerHint = "unknown"),
165
+ {
166
+ detected: detected,
167
+ provider_hint: providerHint,
168
+ confidence: confidence,
169
+ evidence: evidence
45
170
  };
46
-
47
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter);
48
- while(walker.nextNode()) {
49
- const node = walker.currentNode;
50
- if (node.isConnected) {
51
- elements.push(node);
52
- if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot));
171
+ }
172
+ const DEFAULT_INFERENCE_CONFIG = {
173
+ allowedTags: [ "label", "span", "div" ],
174
+ allowedRoles: [],
175
+ allowedClassPatterns: [],
176
+ maxParentDepth: 2,
177
+ maxSiblingDistance: 1,
178
+ requireSameContainer: !0,
179
+ containerTags: [ "form", "fieldset", "div" ],
180
+ methods: {
181
+ explicitLabel: !0,
182
+ ariaLabelledby: !0,
183
+ parentTraversal: !0,
184
+ siblingProximity: !0
185
+ }
186
+ };
187
+ function isInferenceSource(el, config) {
188
+ if (!el || !el.tagName) return !1;
189
+ const tag = el.tagName.toLowerCase(), role = el.getAttribute ? el.getAttribute("role") : "", className = ((el.className || "") + "").toLowerCase();
190
+ if (config.allowedTags.includes(tag)) return !0;
191
+ if (config.allowedRoles.length > 0 && role && config.allowedRoles.includes(role)) return !0;
192
+ if (config.allowedClassPatterns.length > 0) for (const pattern of config.allowedClassPatterns) if (className.includes(pattern.toLowerCase())) return !0;
193
+ return !1;
194
+ }
195
+ function isInSameValidContainer(element, candidate, limits) {
196
+ if (!element || !candidate) return !1;
197
+ if (limits.requireSameContainer) {
198
+ const commonParent = function(el1, el2) {
199
+ if (!el1 || !el2) return null;
200
+ const doc = "undefined" != typeof global && global.document || "undefined" != typeof window && window.document || "undefined" != typeof document && document || null, parents1 = [];
201
+ let current = el1;
202
+ for (;current && (parents1.push(current), current.parentElement) && (!doc || current !== doc.body && current !== doc.documentElement); ) current = current.parentElement;
203
+ for (current = el2; current; ) {
204
+ if (-1 !== parents1.indexOf(current)) return current;
205
+ if (!current.parentElement) break;
206
+ if (doc && (current === doc.body || current === doc.documentElement)) break;
207
+ current = current.parentElement;
208
+ }
209
+ return null;
210
+ }(element, candidate);
211
+ if (!commonParent) return !1;
212
+ if (!function(el, validTags) {
213
+ if (!el || !el.tagName) return !1;
214
+ const tag = el.tagName.toLowerCase();
215
+ let className = "";
216
+ try {
217
+ className = (el.className || "") + "";
218
+ } catch (e) {
219
+ className = "";
220
+ }
221
+ return validTags.includes(tag) || className.toLowerCase().includes("form") || className.toLowerCase().includes("field");
222
+ }(commonParent, limits.containerTags)) return !1;
223
+ }
224
+ return !0;
225
+ }
226
+ function getInferredLabel(el, options = {}) {
227
+ if (!el) return null;
228
+ const {enableInference: enableInference = !0, inferenceConfig: inferenceConfig = {}} = options;
229
+ if (!enableInference) return null;
230
+ 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;
231
+ let innerTextValue = "";
232
+ try {
233
+ innerTextValue = el.innerText || "";
234
+ } catch (e) {
235
+ innerTextValue = "";
236
+ }
237
+ const hasInnerText = "INPUT" !== el.tagName && "IMG" !== el.tagName && innerTextValue && innerTextValue.trim();
238
+ if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) return null;
239
+ const config = function(userConfig = {}) {
240
+ return {
241
+ ...DEFAULT_INFERENCE_CONFIG,
242
+ ...userConfig,
243
+ methods: {
244
+ ...DEFAULT_INFERENCE_CONFIG.methods,
245
+ ...userConfig.methods || {}
246
+ }
247
+ };
248
+ }(inferenceConfig);
249
+ if (config.methods.explicitLabel && el.labels && el.labels.length > 0) {
250
+ const label = el.labels[0];
251
+ if (isInferenceSource(label, config)) {
252
+ const text = (label.innerText || "").trim();
253
+ if (text) return {
254
+ text: text,
255
+ source: "explicit_label"
256
+ };
53
257
  }
54
258
  }
55
- return elements;
259
+ if (config.methods.ariaLabelledby && el.hasAttribute && el.hasAttribute("aria-labelledby")) {
260
+ const labelIdsAttr = el.getAttribute("aria-labelledby");
261
+ if (labelIdsAttr) {
262
+ 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)();
263
+ if (doc && doc.getElementById) for (const labelId of labelIds) {
264
+ if (!labelId.trim()) continue;
265
+ let labelEl = null;
266
+ try {
267
+ labelEl = doc.getElementById(labelId);
268
+ } catch (e) {
269
+ continue;
270
+ }
271
+ if (labelEl) {
272
+ let text = "";
273
+ try {
274
+ if (text = (labelEl.innerText || "").trim(), !text && labelEl.textContent && (text = labelEl.textContent.trim()),
275
+ !text && labelEl.getAttribute) {
276
+ const ariaLabel = labelEl.getAttribute("aria-label");
277
+ ariaLabel && (text = ariaLabel.trim());
278
+ }
279
+ } catch (e) {
280
+ continue;
281
+ }
282
+ text && labelTexts.push(text);
283
+ }
284
+ } else ;
285
+ if (labelTexts.length > 0) return {
286
+ text: labelTexts.join(" "),
287
+ source: "aria_labelledby"
288
+ };
289
+ }
290
+ }
291
+ if (config.methods.parentTraversal) {
292
+ let parent = el.parentElement, depth = 0;
293
+ for (;parent && depth < config.maxParentDepth; ) {
294
+ if (isInferenceSource(parent, config)) {
295
+ const text = (parent.innerText || "").trim();
296
+ if (text) return {
297
+ text: text,
298
+ source: "parent_label"
299
+ };
300
+ }
301
+ parent = parent.parentElement, depth++;
302
+ }
303
+ }
304
+ if (config.methods.siblingProximity) {
305
+ const prev = el.previousElementSibling;
306
+ if (prev && isInferenceSource(prev, config) && isInSameValidContainer(el, prev, {
307
+ requireSameContainer: config.requireSameContainer,
308
+ containerTags: config.containerTags
309
+ })) {
310
+ const text = (prev.innerText || "").trim();
311
+ if (text) return {
312
+ text: text,
313
+ source: "sibling_label"
314
+ };
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+ function normalizeNearbyText(text) {
320
+ return text ? text.replace(/\s+/g, " ").trim() : "";
321
+ }
322
+ function isInteractableElement(el) {
323
+ if (!el || !el.tagName) return !1;
324
+ 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");
325
+ if ([ "button", "input", "textarea", "select", "option", "details", "summary", "a" ].includes(tag)) return !("a" === tag && !hasHref);
326
+ if (role && [ "button", "link", "tab", "menuitem", "checkbox", "radio", "switch", "slider", "combobox", "textbox", "searchbox", "spinbutton" ].includes(role.toLowerCase())) return !0;
327
+ if (hasTabIndex) return !0;
328
+ if (el.onclick || el.onkeydown || el.onkeypress || el.onkeyup) return !0;
329
+ if (el.getAttribute) {
330
+ if (el.getAttribute("onclick") || el.getAttribute("onkeydown") || el.getAttribute("onkeypress") || el.getAttribute("onkeyup")) return !0;
331
+ }
332
+ return !1;
56
333
  }
57
-
58
- // --- HELPER: Smart Text Extractor ---
59
334
  function getText(el) {
60
- if (el.getAttribute('aria-label')) return el.getAttribute('aria-label');
61
- if (el.tagName === 'INPUT') return el.value || el.placeholder || '';
62
- if (el.tagName === 'IMG') return el.alt || '';
63
- return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100);
335
+ if (el.getAttribute("aria-label")) return el.getAttribute("aria-label");
336
+ if ("INPUT" === el.tagName) {
337
+ const t = el.getAttribute && el.getAttribute("type") || el.type || "";
338
+ return "password" === String(t).toLowerCase() ? el.placeholder || "" : el.value || el.placeholder || "";
339
+ }
340
+ return "IMG" === el.tagName ? el.alt || "" : (el.innerText || "").replace(/\s+/g, " ").trim().substring(0, 100);
64
341
  }
65
-
66
- // --- HELPER: Safe Class Name Extractor (Handles SVGAnimatedString) ---
67
342
  function getClassName(el) {
68
- if (!el || !el.className) return '';
69
-
70
- // Handle string (HTML elements)
71
- if (typeof el.className === 'string') return el.className;
72
-
73
- // Handle SVGAnimatedString (SVG elements)
74
- if (typeof el.className === 'object') {
75
- if ('baseVal' in el.className && typeof el.className.baseVal === 'string') {
76
- return el.className.baseVal;
77
- }
78
- if ('animVal' in el.className && typeof el.className.animVal === 'string') {
79
- return el.className.animVal;
80
- }
81
- // Fallback: convert to string
343
+ if (!el || !el.className) return "";
344
+ if ("string" == typeof el.className) return el.className;
345
+ if ("object" == typeof el.className) {
346
+ if ("baseVal" in el.className && "string" == typeof el.className.baseVal) return el.className.baseVal;
347
+ if ("animVal" in el.className && "string" == typeof el.className.animVal) return el.className.animVal;
82
348
  try {
83
349
  return String(el.className);
84
350
  } catch (e) {
85
- return '';
351
+ return "";
86
352
  }
87
353
  }
88
-
89
- return '';
354
+ return "";
90
355
  }
91
-
92
- // --- HELPER: Paranoid String Converter (Handles SVGAnimatedString) ---
93
356
  function toSafeString(value) {
94
- if (value === null || value === undefined) return null;
95
-
96
- // 1. If it's already a primitive string, return it
97
- if (typeof value === 'string') return value;
98
-
99
- // 2. Handle SVG objects (SVGAnimatedString, SVGAnimatedNumber, etc.)
100
- if (typeof value === 'object') {
101
- // Try extracting baseVal (standard SVG property)
102
- if ('baseVal' in value && typeof value.baseVal === 'string') {
103
- return value.baseVal;
104
- }
105
- // Try animVal as fallback
106
- if ('animVal' in value && typeof value.animVal === 'string') {
107
- return value.animVal;
108
- }
109
- // Fallback: Force to string (prevents WASM crash even if data is less useful)
110
- // This prevents the "Invalid Type" crash, even if the data is "[object SVGAnimatedString]"
357
+ if (null == value) return null;
358
+ if ("string" == typeof value) return value;
359
+ if ("object" == typeof value) {
360
+ if ("baseVal" in value && "string" == typeof value.baseVal) return value.baseVal;
361
+ if ("animVal" in value && "string" == typeof value.animVal) return value.animVal;
111
362
  try {
112
363
  return String(value);
113
364
  } catch (e) {
114
365
  return null;
115
366
  }
116
367
  }
117
-
118
- // 3. Last resort cast for primitives
119
368
  try {
120
369
  return String(value);
121
370
  } catch (e) {
122
371
  return null;
123
372
  }
124
373
  }
125
-
126
- // --- HELPER: Get SVG Fill/Stroke Color ---
127
- // For SVG elements, get the fill or stroke color (SVGs use fill/stroke, not backgroundColor)
128
374
  function getSVGColor(el) {
129
- if (!el || el.tagName !== 'SVG') return null;
130
-
131
- const style = window.getComputedStyle(el);
132
-
133
- // Try fill first (most common for SVG icons)
134
- const fill = style.fill;
135
- if (fill && fill !== 'none' && fill !== 'transparent' && fill !== 'rgba(0, 0, 0, 0)') {
136
- // Convert fill to rgb() format if needed
375
+ if (!el || "SVG" !== el.tagName) return null;
376
+ const style = window.getComputedStyle(el), fill = style.fill;
377
+ if (fill && "none" !== fill && "transparent" !== fill && "rgba(0, 0, 0, 0)" !== fill) {
137
378
  const rgbaMatch = fill.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
138
379
  if (rgbaMatch) {
139
- const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0;
140
- if (alpha >= 0.9) {
141
- return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
142
- }
143
- } else if (fill.startsWith('rgb(')) {
144
- return fill;
145
- }
380
+ if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
381
+ } else if (fill.startsWith("rgb(")) return fill;
146
382
  }
147
-
148
- // Fallback to stroke if fill is not available
149
383
  const stroke = style.stroke;
150
- if (stroke && stroke !== 'none' && stroke !== 'transparent' && stroke !== 'rgba(0, 0, 0, 0)') {
384
+ if (stroke && "none" !== stroke && "transparent" !== stroke && "rgba(0, 0, 0, 0)" !== stroke) {
151
385
  const rgbaMatch = stroke.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
152
386
  if (rgbaMatch) {
153
- const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0;
154
- if (alpha >= 0.9) {
155
- return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
156
- }
157
- } else if (stroke.startsWith('rgb(')) {
158
- return stroke;
159
- }
387
+ if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
388
+ } else if (stroke.startsWith("rgb(")) return stroke;
160
389
  }
161
-
162
390
  return null;
163
391
  }
164
-
165
- // --- HELPER: Get Effective Background Color ---
166
- // Traverses up the DOM tree to find the nearest non-transparent background color
167
- // For SVGs, also checks fill/stroke properties
168
- // This handles rgba(0,0,0,0) and transparent values that browsers commonly return
169
- function getEffectiveBackgroundColor(el) {
170
- if (!el) return null;
171
-
172
- // For SVG elements, use fill/stroke instead of backgroundColor
173
- if (el.tagName === 'SVG') {
174
- const svgColor = getSVGColor(el);
175
- if (svgColor) return svgColor;
176
- }
177
-
178
- let current = el;
179
- const maxDepth = 10; // Prevent infinite loops
180
- let depth = 0;
181
-
182
- while (current && depth < maxDepth) {
183
- const style = window.getComputedStyle(current);
184
-
185
- // For SVG elements in the tree, also check fill/stroke
186
- if (current.tagName === 'SVG') {
187
- const svgColor = getSVGColor(current);
188
- if (svgColor) return svgColor;
189
- }
190
-
191
- const bgColor = style.backgroundColor;
192
-
193
- if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {
194
- // Check if it's rgba with alpha < 1 (semi-transparent)
195
- const rgbaMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
196
- if (rgbaMatch) {
197
- const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1.0;
198
- // If alpha is high enough (>= 0.9), consider it opaque enough
199
- if (alpha >= 0.9) {
200
- // Convert to rgb() format for Gateway compatibility
201
- return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
202
- }
203
- // If semi-transparent, continue up the tree
204
- } else if (bgColor.startsWith('rgb(')) {
205
- // Already in rgb() format, use it
206
- return bgColor;
207
- } else {
208
- // Named color or other format, return as-is
209
- return bgColor;
210
- }
211
- }
212
-
213
- // Move up the DOM tree
214
- current = current.parentElement;
215
- depth++;
216
- }
217
-
218
- // Fallback: return null if nothing found
219
- return null;
220
- }
221
-
222
- // --- HELPER: Viewport Check ---
223
- function isInViewport(rect) {
224
- return (
225
- rect.top < window.innerHeight && rect.bottom > 0 &&
226
- rect.left < window.innerWidth && rect.right > 0
227
- );
228
- }
229
-
230
- // --- HELPER: Occlusion Check (Optimized to avoid layout thrashing) ---
231
- // Only checks occlusion for elements likely to be occluded (high z-index, positioned)
232
- // This avoids forced reflow for most elements, dramatically improving performance
233
- function isOccluded(el, rect, style) {
234
- // Fast path: Skip occlusion check for most elements
235
- // Only check for elements that are likely to be occluded (overlays, modals, tooltips)
236
- const zIndex = parseInt(style.zIndex, 10);
237
- const position = style.position;
238
-
239
- // Skip occlusion check for normal flow elements (vast majority)
240
- // Only check for positioned elements or high z-index (likely overlays)
241
- if (position === 'static' && (isNaN(zIndex) || zIndex <= 10)) {
242
- return false; // Assume not occluded for performance
243
- }
244
-
245
- // For positioned/high z-index elements, do the expensive check
246
- const cx = rect.x + rect.width / 2;
247
- const cy = rect.y + rect.height / 2;
248
-
249
- if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return false;
250
-
251
- const topEl = document.elementFromPoint(cx, cy);
252
- if (!topEl) return false;
253
-
254
- return !(el === topEl || el.contains(topEl) || topEl.contains(el));
255
- }
256
-
257
- // --- HELPER: Screenshot Bridge ---
258
- function captureScreenshot(options) {
259
- return new Promise(resolve => {
260
- const requestId = Math.random().toString(36).substring(7);
261
- const listener = (e) => {
262
- if (e.data.type === 'SENTIENCE_SCREENSHOT_RESULT' && e.data.requestId === requestId) {
263
- window.removeEventListener('message', listener);
264
- resolve(e.data.screenshot);
265
- }
266
- };
267
- window.addEventListener('message', listener);
268
- window.postMessage({ type: 'SENTIENCE_SCREENSHOT_REQUEST', requestId, options }, '*');
269
- setTimeout(() => {
270
- window.removeEventListener('message', listener);
271
- resolve(null);
272
- }, 10000); // 10s timeout
273
- });
274
- }
275
-
276
- // --- HELPER: Snapshot Processing Bridge (NEW!) ---
277
- function processSnapshotInBackground(rawData, options) {
278
- return new Promise((resolve, reject) => {
279
- const requestId = Math.random().toString(36).substring(7);
280
- const TIMEOUT_MS = 25000; // 25 seconds (longer than content.js timeout)
281
- let resolved = false;
282
-
283
- const timeout = setTimeout(() => {
284
- if (!resolved) {
285
- resolved = true;
286
- window.removeEventListener('message', listener);
287
- reject(new Error('WASM processing timeout - extension may be unresponsive. Try reloading the extension.'));
288
- }
289
- }, TIMEOUT_MS);
290
-
291
- const listener = (e) => {
292
- if (e.data.type === 'SENTIENCE_SNAPSHOT_RESULT' && e.data.requestId === requestId) {
293
- if (resolved) return; // Already handled
294
- resolved = true;
295
- clearTimeout(timeout);
296
- window.removeEventListener('message', listener);
297
-
298
- if (e.data.error) {
299
- reject(new Error(e.data.error));
300
- } else {
301
- resolve({
302
- elements: e.data.elements,
303
- raw_elements: e.data.raw_elements,
304
- duration: e.data.duration
305
- });
306
- }
307
- }
308
- };
309
-
310
- window.addEventListener('message', listener);
311
-
312
- try {
313
- window.postMessage({
314
- type: 'SENTIENCE_SNAPSHOT_REQUEST',
315
- requestId,
316
- rawData,
317
- options
318
- }, '*');
319
- } catch (error) {
320
- if (!resolved) {
321
- resolved = true;
322
- clearTimeout(timeout);
323
- window.removeEventListener('message', listener);
324
- reject(new Error(`Failed to send snapshot request: ${error.message}`));
325
- }
326
- }
327
- });
328
- }
329
-
330
- // --- HELPER: Raw HTML Extractor (unchanged) ---
331
392
  function getRawHTML(root) {
332
- const sourceRoot = root || document.body;
333
- const clone = sourceRoot.cloneNode(true);
334
-
335
- const unwantedTags = ['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg'];
336
- unwantedTags.forEach(tag => {
337
- const elements = clone.querySelectorAll(tag);
338
- elements.forEach(el => {
339
- if (el.parentNode) el.parentNode.removeChild(el);
393
+ const sourceRoot = root || document.body, clone = sourceRoot.cloneNode(!0);
394
+ [ "nav", "footer", "header", "script", "style", "noscript", "iframe", "svg" ].forEach(tag => {
395
+ clone.querySelectorAll(tag).forEach(el => {
396
+ el.parentNode && el.parentNode.removeChild(el);
340
397
  });
341
398
  });
342
-
343
- // Remove invisible elements
344
- const invisibleSelectors = [];
345
- const walker = document.createTreeWalker(sourceRoot, NodeFilter.SHOW_ELEMENT, null, false);
399
+ const invisibleSelectors = [], walker = document.createTreeWalker(sourceRoot, NodeFilter.SHOW_ELEMENT, null, !1);
346
400
  let node;
347
- while (node = walker.nextNode()) {
401
+ for (;node = walker.nextNode(); ) {
348
402
  const tag = node.tagName.toLowerCase();
349
- if (tag === 'head' || tag === 'title') continue;
350
-
403
+ if ("head" === tag || "title" === tag) continue;
351
404
  const style = window.getComputedStyle(node);
352
- if (style.display === 'none' || style.visibility === 'hidden' ||
353
- (node.offsetWidth === 0 && node.offsetHeight === 0)) {
405
+ if ("none" === style.display || "hidden" === style.visibility || 0 === node.offsetWidth && 0 === node.offsetHeight) {
354
406
  let selector = tag;
355
- if (node.id) {
356
- selector = `#${node.id}`;
357
- } else if (node.className && typeof node.className === 'string') {
407
+ if (node.id) selector = `#${node.id}`; else if (node.className && "string" == typeof node.className) {
358
408
  const classes = node.className.trim().split(/\s+/).filter(c => c);
359
- if (classes.length > 0) {
360
- selector = `${tag}.${classes.join('.')}`;
361
- }
409
+ classes.length > 0 && (selector = `${tag}.${classes.join(".")}`);
362
410
  }
363
411
  invisibleSelectors.push(selector);
364
412
  }
365
413
  }
366
-
367
414
  invisibleSelectors.forEach(selector => {
368
415
  try {
369
- const elements = clone.querySelectorAll(selector);
370
- elements.forEach(el => {
371
- if (el.parentNode) el.parentNode.removeChild(el);
416
+ clone.querySelectorAll(selector).forEach(el => {
417
+ el.parentNode && el.parentNode.removeChild(el);
372
418
  });
373
- } catch (e) {
374
- // Invalid selector, skip
375
- }
419
+ } catch (e) {}
376
420
  });
377
-
378
- // Resolve relative URLs
379
- const links = clone.querySelectorAll('a[href]');
380
- links.forEach(link => {
381
- const href = link.getAttribute('href');
382
- if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('#')) {
383
- try {
384
- link.setAttribute('href', new URL(href, document.baseURI).href);
385
- } catch (e) {}
386
- }
387
- });
388
-
389
- const images = clone.querySelectorAll('img[src]');
390
- images.forEach(img => {
391
- const src = img.getAttribute('src');
392
- if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:')) {
393
- try {
394
- img.setAttribute('src', new URL(src, document.baseURI).href);
395
- } catch (e) {}
396
- }
421
+ clone.querySelectorAll("a[href]").forEach(link => {
422
+ const href = link.getAttribute("href");
423
+ if (href && !href.startsWith("http://") && !href.startsWith("https://") && !href.startsWith("#")) try {
424
+ link.setAttribute("href", new URL(href, document.baseURI).href);
425
+ } catch (e) {}
397
426
  });
398
-
399
- return clone.innerHTML;
400
- }
401
-
402
- // --- HELPER: Markdown Converter (unchanged) ---
403
- function convertToMarkdown(root) {
404
- const rawHTML = getRawHTML(root);
405
- const tempDiv = document.createElement('div');
406
- tempDiv.innerHTML = rawHTML;
407
-
408
- let markdown = '';
409
- let insideLink = false;
410
-
411
- function walk(node) {
412
- if (node.nodeType === Node.TEXT_NODE) {
413
- const text = node.textContent.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
414
- if (text.trim()) markdown += text;
415
- return;
416
- }
417
-
418
- if (node.nodeType !== Node.ELEMENT_NODE) return;
419
-
420
- const tag = node.tagName.toLowerCase();
421
-
422
- // Prefix
423
- if (tag === 'h1') markdown += '\n# ';
424
- if (tag === 'h2') markdown += '\n## ';
425
- if (tag === 'h3') markdown += '\n### ';
426
- if (tag === 'li') markdown += '\n- ';
427
- if (!insideLink && (tag === 'p' || tag === 'div' || tag === 'br')) markdown += '\n';
428
- if (tag === 'strong' || tag === 'b') markdown += '**';
429
- if (tag === 'em' || tag === 'i') markdown += '_';
430
- if (tag === 'a') {
431
- markdown += '[';
432
- insideLink = true;
433
- }
434
-
435
- // Children
436
- if (node.shadowRoot) {
437
- Array.from(node.shadowRoot.childNodes).forEach(walk);
438
- } else {
439
- node.childNodes.forEach(walk);
440
- }
441
-
442
- // Suffix
443
- if (tag === 'a') {
444
- const href = node.getAttribute('href');
445
- if (href) markdown += `](${href})`;
446
- else markdown += ']';
447
- insideLink = false;
448
- }
449
- if (tag === 'strong' || tag === 'b') markdown += '**';
450
- if (tag === 'em' || tag === 'i') markdown += '_';
451
- if (!insideLink && (tag === 'h1' || tag === 'h2' || tag === 'h3' || tag === 'p' || tag === 'div')) markdown += '\n';
452
- }
453
-
454
- walk(tempDiv);
455
- return markdown.replace(/\n{3,}/g, '\n\n').trim();
456
- }
457
-
458
- // --- HELPER: Text Extractor (unchanged) ---
459
- function convertToText(root) {
460
- let text = '';
461
- function walk(node) {
462
- if (node.nodeType === Node.TEXT_NODE) {
463
- text += node.textContent;
464
- return;
465
- }
466
- if (node.nodeType === Node.ELEMENT_NODE) {
467
- const tag = node.tagName.toLowerCase();
468
- if (['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg'].includes(tag)) return;
469
-
470
- const style = window.getComputedStyle(node);
471
- if (style.display === 'none' || style.visibility === 'hidden') return;
472
-
473
- const isBlock = style.display === 'block' || style.display === 'flex' || node.tagName === 'P' || node.tagName === 'DIV';
474
- if (isBlock) text += ' ';
475
-
476
- if (node.shadowRoot) {
477
- Array.from(node.shadowRoot.childNodes).forEach(walk);
478
- } else {
479
- node.childNodes.forEach(walk);
480
- }
481
-
482
- if (isBlock) text += '\n';
483
- }
484
- }
485
- walk(root || document.body);
486
- return text.replace(/\n{3,}/g, '\n\n').trim();
427
+ return clone.querySelectorAll("img[src]").forEach(img => {
428
+ const src = img.getAttribute("src");
429
+ if (src && !src.startsWith("http://") && !src.startsWith("https://") && !src.startsWith("data:")) try {
430
+ img.setAttribute("src", new URL(src, document.baseURI).href);
431
+ } catch (e) {}
432
+ }), clone.innerHTML;
487
433
  }
488
-
489
- // --- HELPER: Clean null/undefined fields ---
490
434
  function cleanElement(obj) {
491
- if (Array.isArray(obj)) {
492
- return obj.map(cleanElement);
493
- }
494
- if (obj !== null && typeof obj === 'object') {
435
+ if (Array.isArray(obj)) return obj.map(cleanElement);
436
+ if (null !== obj && "object" == typeof obj) {
495
437
  const cleaned = {};
496
- for (const [key, value] of Object.entries(obj)) {
497
- if (value !== null && value !== undefined) {
498
- if (typeof value === 'object') {
499
- const deepClean = cleanElement(value);
500
- if (Object.keys(deepClean).length > 0) {
501
- cleaned[key] = deepClean;
502
- }
503
- } else {
504
- cleaned[key] = value;
505
- }
506
- }
507
- }
438
+ for (const [key, value] of Object.entries(obj)) if (null != value) if ("object" == typeof value) {
439
+ const deepClean = cleanElement(value);
440
+ Object.keys(deepClean).length > 0 && (cleaned[key] = deepClean);
441
+ } else cleaned[key] = value;
508
442
  return cleaned;
509
443
  }
510
444
  return obj;
511
445
  }
512
-
513
- // --- HELPER: Extract Raw Element Data (for Golden Set) ---
514
- function extractRawElementData(el) {
515
- const style = window.getComputedStyle(el);
516
- const rect = el.getBoundingClientRect();
517
-
518
- return {
519
- tag: el.tagName,
520
- rect: {
521
- x: Math.round(rect.x),
522
- y: Math.round(rect.y),
523
- width: Math.round(rect.width),
524
- height: Math.round(rect.height)
525
- },
526
- styles: {
527
- cursor: style.cursor || null,
528
- backgroundColor: style.backgroundColor || null,
529
- color: style.color || null,
530
- fontWeight: style.fontWeight || null,
531
- fontSize: style.fontSize || null,
532
- display: style.display || null,
533
- position: style.position || null,
534
- zIndex: style.zIndex || null,
535
- opacity: style.opacity || null,
536
- visibility: style.visibility || null
537
- },
538
- attributes: {
539
- role: el.getAttribute('role') || null,
540
- type: el.getAttribute('type') || null,
541
- ariaLabel: el.getAttribute('aria-label') || null,
542
- id: el.id || null,
543
- className: el.className || null
544
- }
545
- };
546
- }
547
-
548
- // --- HELPER: Generate Unique CSS Selector (for Golden Set) ---
549
- function getUniqueSelector(el) {
550
- if (!el || !el.tagName) return '';
551
-
552
- // If element has a unique ID, use it
553
- if (el.id) {
554
- return `#${el.id}`;
555
- }
556
-
557
- // Try data attributes or aria-label for uniqueness
558
- for (const attr of el.attributes) {
559
- if (attr.name.startsWith('data-') || attr.name === 'aria-label') {
560
- const value = attr.value ? attr.value.replace(/"/g, '\\"') : '';
561
- return `${el.tagName.toLowerCase()}[${attr.name}="${value}"]`;
562
- }
563
- }
564
-
565
- // Build path with classes and nth-child for uniqueness
566
- const path = [];
567
- let current = el;
568
-
569
- while (current && current !== document.body && current !== document.documentElement) {
570
- let selector = current.tagName.toLowerCase();
571
-
572
- // If current element has ID, use it and stop
573
- if (current.id) {
574
- selector = `#${current.id}`;
575
- path.unshift(selector);
576
- break;
577
- }
578
-
579
- // Add class if available
580
- if (current.className && typeof current.className === 'string') {
581
- const classes = current.className.trim().split(/\s+/).filter(c => c);
582
- if (classes.length > 0) {
583
- // Use first class for simplicity
584
- selector += `.${classes[0]}`;
585
- }
586
- }
587
-
588
- // Add nth-of-type if needed for uniqueness
589
- if (current.parentElement) {
590
- const siblings = Array.from(current.parentElement.children);
591
- const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName);
592
- const index = sameTagSiblings.indexOf(current);
593
- if (index > 0 || sameTagSiblings.length > 1) {
594
- selector += `:nth-of-type(${index + 1})`;
595
- }
596
- }
597
-
598
- path.unshift(selector);
599
- current = current.parentElement;
600
- }
601
-
602
- return path.join(' > ') || el.tagName.toLowerCase();
603
- }
604
-
605
- // --- HELPER: Wait for DOM Stability (SPA Hydration) ---
606
- // Waits for the DOM to stabilize before taking a snapshot
607
- // Useful for React/Vue apps that render empty skeletons before hydration
608
- async function waitForStability(options = {}) {
609
- const {
610
- minNodeCount = 500,
611
- quietPeriod = 200, // milliseconds
612
- maxWait = 5000 // maximum wait time
613
- } = options;
614
-
615
- const startTime = Date.now();
616
-
617
- return new Promise((resolve) => {
618
- // Check if DOM already has enough nodes
619
- const nodeCount = document.querySelectorAll('*').length;
620
- if (nodeCount >= minNodeCount) {
621
- // DOM seems ready, but wait for quiet period to ensure stability
622
- let lastChange = Date.now();
623
- const observer = new MutationObserver(() => {
624
- lastChange = Date.now();
625
- });
626
-
627
- observer.observe(document.body, {
628
- childList: true,
629
- subtree: true,
630
- attributes: false
631
- });
632
-
633
- const checkStable = () => {
634
- const timeSinceLastChange = Date.now() - lastChange;
635
- const totalWait = Date.now() - startTime;
636
-
637
- if (timeSinceLastChange >= quietPeriod) {
638
- observer.disconnect();
639
- resolve();
640
- } else if (totalWait >= maxWait) {
641
- observer.disconnect();
642
- console.warn('[SentienceAPI] DOM stability timeout - proceeding anyway');
643
- resolve();
644
- } else {
645
- setTimeout(checkStable, 50);
646
- }
647
- };
648
-
649
- checkStable();
650
- } else {
651
- // DOM doesn't have enough nodes yet, wait for them
652
- const observer = new MutationObserver(() => {
653
- const currentCount = document.querySelectorAll('*').length;
654
- const totalWait = Date.now() - startTime;
655
-
656
- if (currentCount >= minNodeCount) {
657
- observer.disconnect();
658
- // Now wait for quiet period
446
+ async function snapshot(options = {}) {
447
+ try {
448
+ !1 !== options.waitForStability && await async function(options = {}) {
449
+ const {minNodeCount: minNodeCount = 500, quietPeriod: quietPeriod = 200, maxWait: maxWait = 5e3} = options, startTime = Date.now();
450
+ try {
451
+ window.__sentience_lastMutationTs = performance.now();
452
+ } catch (e) {}
453
+ return new Promise(resolve => {
454
+ if (document.querySelectorAll("*").length >= minNodeCount) {
659
455
  let lastChange = Date.now();
660
- const quietObserver = new MutationObserver(() => {
456
+ const observer = new MutationObserver(() => {
661
457
  lastChange = Date.now();
458
+ try {
459
+ window.__sentience_lastMutationTs = performance.now();
460
+ } catch (e) {}
662
461
  });
663
-
664
- quietObserver.observe(document.body, {
665
- childList: true,
666
- subtree: true,
667
- attributes: false
462
+ observer.observe(document.body, {
463
+ childList: !0,
464
+ subtree: !0,
465
+ attributes: !1
668
466
  });
669
-
670
- const checkQuiet = () => {
671
- const timeSinceLastChange = Date.now() - lastChange;
672
- const totalWait = Date.now() - startTime;
673
-
674
- if (timeSinceLastChange >= quietPeriod) {
675
- quietObserver.disconnect();
676
- resolve();
677
- } else if (totalWait >= maxWait) {
678
- quietObserver.disconnect();
679
- console.warn('[SentienceAPI] DOM stability timeout - proceeding anyway');
680
- resolve();
681
- } else {
682
- setTimeout(checkQuiet, 50);
683
- }
467
+ const checkStable = () => {
468
+ const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime;
469
+ timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(),
470
+ resolve()) : setTimeout(checkStable, 50);
684
471
  };
685
-
686
- checkQuiet();
687
- } else if (totalWait >= maxWait) {
688
- observer.disconnect();
689
- console.warn('[SentienceAPI] DOM node count timeout - proceeding anyway');
690
- resolve();
472
+ checkStable();
473
+ } else {
474
+ const observer = new MutationObserver(() => {
475
+ const currentCount = document.querySelectorAll("*").length, totalWait = Date.now() - startTime;
476
+ try {
477
+ window.__sentience_lastMutationTs = performance.now();
478
+ } catch (e) {}
479
+ if (currentCount >= minNodeCount) {
480
+ observer.disconnect();
481
+ let lastChange = Date.now();
482
+ const quietObserver = new MutationObserver(() => {
483
+ lastChange = Date.now();
484
+ try {
485
+ window.__sentience_lastMutationTs = performance.now();
486
+ } catch (e) {}
487
+ });
488
+ quietObserver.observe(document.body, {
489
+ childList: !0,
490
+ subtree: !0,
491
+ attributes: !1
492
+ });
493
+ const checkQuiet = () => {
494
+ const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime;
495
+ timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(),
496
+ resolve()) : setTimeout(checkQuiet, 50);
497
+ };
498
+ checkQuiet();
499
+ } else totalWait >= maxWait && (observer.disconnect(), resolve());
500
+ });
501
+ observer.observe(document.body, {
502
+ childList: !0,
503
+ subtree: !0,
504
+ attributes: !1
505
+ }), setTimeout(() => {
506
+ observer.disconnect(), resolve();
507
+ }, maxWait);
691
508
  }
692
509
  });
693
-
694
- observer.observe(document.body, {
695
- childList: true,
696
- subtree: true,
697
- attributes: false
698
- });
699
-
700
- // Timeout fallback
701
- setTimeout(() => {
702
- observer.disconnect();
703
- console.warn('[SentienceAPI] DOM stability max wait reached - proceeding');
704
- resolve();
705
- }, maxWait);
706
- }
707
- });
708
- }
709
-
710
- // --- HELPER: Collect Iframe Snapshots (Frame Stitching) ---
711
- // Recursively collects snapshot data from all child iframes
712
- // This enables detection of elements inside iframes (e.g., Stripe forms)
713
- //
714
- // NOTE: Cross-origin iframes cannot be accessed due to browser security (Same-Origin Policy).
715
- // Only same-origin iframes will return snapshot data. Cross-origin iframes will be skipped
716
- // with a warning. For cross-origin iframes, users must manually switch frames using
717
- // Playwright's page.frame() API.
718
- async function collectIframeSnapshots(options = {}) {
719
- const iframeData = new Map(); // Map of iframe element -> snapshot data
720
-
721
- // Find all iframe elements in current document
722
- const iframes = Array.from(document.querySelectorAll('iframe'));
723
-
724
- if (iframes.length === 0) {
725
- return iframeData;
726
- }
727
-
728
- console.log(`[SentienceAPI] Found ${iframes.length} iframe(s), requesting snapshots...`);
729
- // Request snapshot from each iframe
730
- const iframePromises = iframes.map((iframe, idx) => {
731
- // OPTIMIZATION: Skip common ad domains to save time
732
- const src = iframe.src || '';
733
- if (src.includes('doubleclick') || src.includes('googleadservices') || src.includes('ads system')) {
734
- console.log(`[SentienceAPI] Skipping ad iframe: ${src.substring(0, 30)}...`);
735
- return Promise.resolve(null);
736
- }
737
-
738
- return new Promise((resolve) => {
739
- const requestId = `iframe-${idx}-${Date.now()}`;
740
-
741
- // 1. EXTENDED TIMEOUT (Handle slow children)
742
- const timeout = setTimeout(() => {
743
- console.warn(`[SentienceAPI] ⚠️ Iframe ${idx} snapshot TIMEOUT (id: ${requestId})`);
744
- resolve(null);
745
- }, 5000); // Increased to 5s to handle slow processing
746
-
747
- // 2. ROBUST LISTENER with debugging
748
- const listener = (event) => {
749
- // Debug: Log all SENTIENCE_IFRAME_SNAPSHOT_RESPONSE messages to see what's happening
750
- if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE') {
751
- // Only log if it's not our request (for debugging)
752
- if (event.data?.requestId !== requestId) {
753
- // console.log(`[SentienceAPI] Received response for different request: ${event.data.requestId} (expected: ${requestId})`);
754
- }
510
+ }(options.waitForStability || {});
511
+ const rawData = [];
512
+ window.sentience_registry = [];
513
+ getAllElements().forEach((el, idx) => {
514
+ if (!el.getBoundingClientRect) return;
515
+ const rect = el.getBoundingClientRect();
516
+ if (rect.width < 5 || rect.height < 5) return;
517
+ const tagName = el.tagName.toLowerCase();
518
+ if ("span" === tagName) {
519
+ if (el.closest("a")) return;
520
+ const childLink = el.querySelector("a[href]");
521
+ if (childLink && childLink.href) return;
522
+ options.debug && el.className && el.className.includes("titleline");
523
+ }
524
+ window.sentience_registry[idx] = el;
525
+ const inputType = "input" === tagName ? toSafeString(el.getAttribute && el.getAttribute("type") || el.type || null) : null, isPasswordInput = inputType && "password" === inputType.toLowerCase(), semanticText = function(el, options = {}) {
526
+ if (!el) return {
527
+ text: "",
528
+ source: null
529
+ };
530
+ const explicitAriaLabel = el.getAttribute ? el.getAttribute("aria-label") : null;
531
+ if (explicitAriaLabel && explicitAriaLabel.trim()) return {
532
+ text: explicitAriaLabel.trim(),
533
+ source: "explicit_aria_label"
534
+ };
535
+ if ("INPUT" === el.tagName) {
536
+ const t = el.getAttribute && el.getAttribute("type") || el.type || "", isPassword = "password" === String(t).toLowerCase(), value = (isPassword ? el.placeholder || "" : el.value || el.placeholder || "").trim();
537
+ if (value) return {
538
+ text: value,
539
+ source: isPassword ? "input_placeholder" : "input_value"
540
+ };
755
541
  }
756
-
757
- // Check if this is the response we're waiting for
758
- if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE' &&
759
- event.data?.requestId === requestId) {
760
-
761
- clearTimeout(timeout);
762
- window.removeEventListener('message', listener);
763
-
764
- if (event.data.error) {
765
- console.warn(`[SentienceAPI] Iframe ${idx} returned error:`, event.data.error);
766
- resolve(null);
767
- } else {
768
- const elementCount = event.data.snapshot?.raw_elements?.length || 0;
769
- console.log(`[SentienceAPI] Received ${elementCount} elements from Iframe ${idx} (id: ${requestId})`);
770
- resolve({
771
- iframe: iframe,
772
- data: event.data.snapshot,
773
- error: null
774
- });
542
+ if ("IMG" === el.tagName) {
543
+ const alt = (el.alt || "").trim();
544
+ if (alt) return {
545
+ text: alt,
546
+ source: "img_alt"
547
+ };
548
+ }
549
+ const innerText = (el.innerText || "").trim();
550
+ if (innerText) return {
551
+ text: innerText.substring(0, 100),
552
+ source: "inner_text"
553
+ };
554
+ const inferred = getInferredLabel(el, {
555
+ enableInference: !1 !== options.enableInference,
556
+ inferenceConfig: options.inferenceConfig
557
+ });
558
+ return inferred || {
559
+ text: "",
560
+ source: null
561
+ };
562
+ }(el, {
563
+ enableInference: !1 !== options.enableInference,
564
+ inferenceConfig: options.inferenceConfig
565
+ }), textVal = semanticText.text || getText(el), inferredRole = function(el, options = {}) {
566
+ const {enableInference: enableInference = !0} = options;
567
+ if (!enableInference) return null;
568
+ if (!isInteractableElement(el)) return null;
569
+ const hasAriaLabel = el.getAttribute ? el.getAttribute("aria-label") : null, hasExplicitRole = el.getAttribute ? el.getAttribute("role") : null;
570
+ if (hasAriaLabel || hasExplicitRole) return null;
571
+ const tag = el.tagName.toLowerCase();
572
+ 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;
573
+ }(el, {
574
+ enableInference: !1 !== options.enableInference,
575
+ inferenceConfig: options.inferenceConfig
576
+ }), inView = function(rect) {
577
+ return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
578
+ }(rect), style = window.getComputedStyle(el), occluded = !!inView && function(el, rect, style) {
579
+ const zIndex = parseInt(style.zIndex, 10);
580
+ if ("static" === style.position && (isNaN(zIndex) || zIndex <= 10)) return !1;
581
+ const cx = rect.x + rect.width / 2, cy = rect.y + rect.height / 2;
582
+ if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return !1;
583
+ const topEl = document.elementFromPoint(cx, cy);
584
+ return !!topEl && !(el === topEl || el.contains(topEl) || topEl.contains(el));
585
+ }(el, rect, style), effectiveBgColor = function(el) {
586
+ if (!el) return null;
587
+ if ("SVG" === el.tagName) {
588
+ const svgColor = getSVGColor(el);
589
+ if (svgColor) return svgColor;
590
+ }
591
+ let current = el, depth = 0;
592
+ for (;current && depth < 10; ) {
593
+ const style = window.getComputedStyle(current);
594
+ if ("SVG" === current.tagName) {
595
+ const svgColor = getSVGColor(current);
596
+ if (svgColor) return svgColor;
775
597
  }
598
+ const bgColor = style.backgroundColor;
599
+ if (bgColor && "transparent" !== bgColor && "rgba(0, 0, 0, 0)" !== bgColor) {
600
+ const rgbaMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
601
+ if (!rgbaMatch) return bgColor.startsWith("rgb("), bgColor;
602
+ if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
603
+ }
604
+ current = current.parentElement, depth++;
776
605
  }
777
- };
778
-
779
- window.addEventListener('message', listener);
780
-
781
- // 3. SEND REQUEST with error handling
606
+ return null;
607
+ }(el);
608
+ let safeValue = null, valueRedacted = null;
782
609
  try {
783
- if (iframe.contentWindow) {
784
- // console.log(`[SentienceAPI] Sending request to Iframe ${idx} (id: ${requestId})`);
785
- iframe.contentWindow.postMessage({
786
- type: 'SENTIENCE_IFRAME_SNAPSHOT_REQUEST',
787
- requestId: requestId,
788
- options: {
789
- ...options,
790
- collectIframes: true // Enable recursion for nested iframes
791
- }
792
- }, '*'); // Use '*' for cross-origin, but browser will enforce same-origin policy
793
- } else {
794
- console.warn(`[SentienceAPI] Iframe ${idx} contentWindow is inaccessible (Cross-Origin?)`);
795
- clearTimeout(timeout);
796
- window.removeEventListener('message', listener);
797
- resolve(null);
610
+ if (void 0 !== el.value || el.getAttribute && null !== el.getAttribute("value")) if (isPasswordInput) safeValue = null,
611
+ valueRedacted = "true"; else {
612
+ const rawValue = void 0 !== el.value ? String(el.value) : String(el.getAttribute("value"));
613
+ safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue, valueRedacted = "false";
798
614
  }
799
- } catch (error) {
800
- console.error(`[SentienceAPI] Failed to postMessage to Iframe ${idx}:`, error);
801
- clearTimeout(timeout);
802
- window.removeEventListener('message', listener);
803
- resolve(null);
804
- }
805
- });
806
- });
807
-
808
- // Wait for all iframe responses
809
- const results = await Promise.all(iframePromises);
810
-
811
- // Store iframe data
812
- results.forEach((result, idx) => {
813
- if (result && result.data && !result.error) {
814
- iframeData.set(iframes[idx], result.data);
815
- console.log(`[SentienceAPI] ✓ Collected snapshot from iframe ${idx}`);
816
- } else if (result && result.error) {
817
- console.warn(`[SentienceAPI] Iframe ${idx} snapshot error:`, result.error);
818
- } else if (!result) {
819
- console.warn(`[SentienceAPI] Iframe ${idx} returned no data (timeout or error)`);
820
- }
821
- });
822
-
823
- return iframeData;
824
- }
825
-
826
- // --- HELPER: Handle Iframe Snapshot Request (for child frames) ---
827
- // When a parent frame requests snapshot, this handler responds with local snapshot
828
- // NOTE: Recursion is safe because querySelectorAll('iframe') only finds direct children.
829
- // Iframe A can ask Iframe B, but won't go back up to parent (no circular dependency risk).
830
- function setupIframeSnapshotHandler() {
831
- window.addEventListener('message', async (event) => {
832
- // Security: only respond to snapshot requests from parent frames
833
- if (event.data?.type === 'SENTIENCE_IFRAME_SNAPSHOT_REQUEST') {
834
- const { requestId, options } = event.data;
835
-
836
- try {
837
- // Generate snapshot for this iframe's content
838
- // Allow recursive collection - querySelectorAll('iframe') only finds direct children,
839
- // so Iframe A will ask Iframe B, but won't go back up to parent (safe recursion)
840
- // waitForStability: false makes performance better - i.e. don't wait for children frames
841
- const snapshotOptions = { ...options, collectIframes: true, waitForStability: options.waitForStability === false ? false : false };
842
- const snapshot = await window.sentience.snapshot(snapshotOptions);
843
-
844
- // Send response back to parent
845
- if (event.source && event.source.postMessage) {
846
- event.source.postMessage({
847
- type: 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE',
848
- requestId: requestId,
849
- snapshot: snapshot,
850
- error: null
851
- }, '*');
615
+ } catch (e) {}
616
+ const accessibleName = toSafeString(function(el) {
617
+ if (!el || !el.getAttribute) return "";
618
+ const ariaLabel = el.getAttribute("aria-label");
619
+ if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim().substring(0, 200);
620
+ const labelledBy = el.getAttribute("aria-labelledby");
621
+ if (labelledBy && labelledBy.trim()) {
622
+ const ids = labelledBy.split(/\s+/).filter(id => id.trim()), texts = [];
623
+ for (const id of ids) try {
624
+ const ref = document.getElementById(id);
625
+ if (!ref) continue;
626
+ const txt = (ref.innerText || ref.textContent || ref.getAttribute?.("aria-label") || "").toString().trim();
627
+ txt && texts.push(txt);
628
+ } catch (e) {}
629
+ if (texts.length > 0) return texts.join(" ").substring(0, 200);
852
630
  }
853
- } catch (error) {
854
- // Send error response
855
- if (event.source && event.source.postMessage) {
856
- event.source.postMessage({
857
- type: 'SENTIENCE_IFRAME_SNAPSHOT_RESPONSE',
858
- requestId: requestId,
859
- snapshot: null,
860
- error: error.message
861
- }, '*');
631
+ try {
632
+ if (el.labels && el.labels.length > 0) {
633
+ const t = (el.labels[0].innerText || el.labels[0].textContent || "").toString().trim();
634
+ if (t) return t.substring(0, 200);
635
+ }
636
+ } catch (e) {}
637
+ try {
638
+ const parentLabel = el.closest && el.closest("label");
639
+ if (parentLabel) {
640
+ const t = (parentLabel.innerText || parentLabel.textContent || "").toString().trim();
641
+ if (t) return t.substring(0, 200);
642
+ }
643
+ } catch (e) {}
644
+ const tag = (el.tagName || "").toUpperCase();
645
+ if ("INPUT" === tag || "TEXTAREA" === tag) {
646
+ const ph = (el.getAttribute("placeholder") || "").toString().trim();
647
+ if (ph) return ph.substring(0, 200);
862
648
  }
863
- }
864
- }
865
- });
866
- }
867
-
868
- // Setup iframe handler when script loads (only once)
869
- if (!window.sentience_iframe_handler_setup) {
870
- setupIframeSnapshotHandler();
871
- window.sentience_iframe_handler_setup = true;
872
- }
873
-
874
- // --- GLOBAL API ---
875
- window.sentience = {
876
- // 1. Geometry snapshot (NEW ARCHITECTURE - No WASM in Main World!)
877
- snapshot: async (options = {}) => {
878
- try {
879
- // Step 0: Wait for DOM stability if requested (for SPA hydration)
880
- if (options.waitForStability !== false) {
881
- await waitForStability(options.waitForStability || {});
882
- }
883
-
884
- // Step 1: Collect raw DOM data (Main World - CSP can't block this!)
885
- const rawData = [];
886
- window.sentience_registry = [];
887
-
888
- const nodes = getAllElements();
889
-
890
- nodes.forEach((el, idx) => {
891
- if (!el.getBoundingClientRect) return;
892
- const rect = el.getBoundingClientRect();
893
- if (rect.width < 5 || rect.height < 5) return;
894
-
895
- window.sentience_registry[idx] = el;
896
-
897
- const textVal = getText(el);
898
- const inView = isInViewport(rect);
899
-
900
- // Get computed style once (needed for both occlusion check and data collection)
901
- const style = window.getComputedStyle(el);
902
-
903
- // Only check occlusion for elements likely to be occluded (optimized)
904
- // This avoids layout thrashing for the vast majority of elements
905
- const occluded = inView ? isOccluded(el, rect, style) : false;
906
-
907
- // Get effective background color (traverses DOM to find non-transparent color)
908
- const effectiveBgColor = getEffectiveBackgroundColor(el);
909
-
910
- rawData.push({
911
- id: idx,
912
- tag: el.tagName.toLowerCase(),
913
- rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
914
- styles: {
915
- display: toSafeString(style.display),
916
- visibility: toSafeString(style.visibility),
917
- opacity: toSafeString(style.opacity),
918
- z_index: toSafeString(style.zIndex || "auto"),
919
- position: toSafeString(style.position),
920
- bg_color: toSafeString(effectiveBgColor || style.backgroundColor),
921
- color: toSafeString(style.color),
922
- cursor: toSafeString(style.cursor),
923
- font_weight: toSafeString(style.fontWeight),
924
- font_size: toSafeString(style.fontSize)
925
- },
926
- attributes: {
927
- role: toSafeString(el.getAttribute('role')),
928
- type_: toSafeString(el.getAttribute('type')),
929
- aria_label: toSafeString(el.getAttribute('aria-label')),
930
- href: toSafeString(el.href || el.getAttribute('href') || null),
931
- class: toSafeString(getClassName(el)),
932
- // Capture dynamic input state (not just initial attributes)
933
- value: el.value !== undefined ? toSafeString(el.value) : toSafeString(el.getAttribute('value')),
934
- checked: el.checked !== undefined ? String(el.checked) : null
935
- },
936
- text: toSafeString(textVal),
937
- in_viewport: inView,
938
- is_occluded: occluded
939
- });
649
+ const title = el.getAttribute("title");
650
+ return title && title.trim() ? title.trim().substring(0, 200) : "";
651
+ }(el) || null), nearbyText = isInteractableElement(el) ? function(el, options = {}) {
652
+ if (!el) return null;
653
+ const maxLen = "number" == typeof options.maxLen ? options.maxLen : 80, ownText = normalizeNearbyText(el.innerText || ""), candidates = [], collect = node => {
654
+ if (!node) return;
655
+ let text = "";
656
+ try {
657
+ text = normalizeNearbyText(node.innerText || node.textContent || "");
658
+ } catch (e) {
659
+ text = "";
660
+ }
661
+ text && text !== ownText && candidates.push(text);
662
+ };
663
+ if (collect(el.previousElementSibling), collect(el.nextElementSibling), 0 === candidates.length && el.parentElement) {
664
+ let parentText = "";
665
+ try {
666
+ parentText = normalizeNearbyText(el.parentElement.innerText || "");
667
+ } catch (e) {
668
+ parentText = "";
669
+ }
670
+ parentText && parentText !== ownText && parentText.length <= 120 && candidates.push(parentText);
671
+ }
672
+ if (0 === candidates.length) return null;
673
+ let text = candidates[0];
674
+ return text.length > maxLen && (text = text.slice(0, maxLen).trim()), text || null;
675
+ }(el, {
676
+ maxLen: 80
677
+ }) : null;
678
+ rawData.push({
679
+ id: idx,
680
+ tag: tagName,
681
+ rect: {
682
+ x: rect.x,
683
+ y: rect.y,
684
+ width: rect.width,
685
+ height: rect.height
686
+ },
687
+ styles: {
688
+ display: toSafeString(style.display),
689
+ visibility: toSafeString(style.visibility),
690
+ opacity: toSafeString(style.opacity),
691
+ z_index: toSafeString(style.zIndex || "auto"),
692
+ position: toSafeString(style.position),
693
+ bg_color: toSafeString(effectiveBgColor || style.backgroundColor),
694
+ color: toSafeString(style.color),
695
+ cursor: toSafeString(style.cursor),
696
+ font_weight: toSafeString(style.fontWeight),
697
+ font_size: toSafeString(style.fontSize)
698
+ },
699
+ attributes: {
700
+ role: toSafeString(el.getAttribute("role")),
701
+ type_: toSafeString(el.getAttribute("type")),
702
+ input_type: inputType,
703
+ aria_label: "explicit_aria_label" === semanticText?.source ? semanticText.text : toSafeString(el.getAttribute("aria-label")),
704
+ name: accessibleName,
705
+ inferred_label: semanticText?.source && ![ "explicit_aria_label", "input_value", "img_alt", "inner_text" ].includes(semanticText.source) ? toSafeString(semanticText.text) : null,
706
+ label_source: semanticText?.source || null,
707
+ inferred_role: inferredRole ? toSafeString(inferredRole) : null,
708
+ nearby_text: toSafeString(nearbyText),
709
+ href: toSafeString(el.href || el.getAttribute("href") || el.closest && el.closest("a")?.href || null),
710
+ class: toSafeString(getClassName(el)),
711
+ value: null !== safeValue ? toSafeString(safeValue) : null,
712
+ value_redacted: valueRedacted,
713
+ checked: void 0 !== el.checked ? String(el.checked) : null,
714
+ disabled: void 0 !== el.disabled ? String(el.disabled) : null,
715
+ aria_checked: toSafeString(el.getAttribute("aria-checked")),
716
+ aria_disabled: toSafeString(el.getAttribute("aria-disabled")),
717
+ aria_expanded: toSafeString(el.getAttribute("aria-expanded"))
718
+ },
719
+ text: toSafeString(textVal),
720
+ in_viewport: inView,
721
+ is_occluded: occluded,
722
+ scroll_y: window.scrollY
940
723
  });
941
-
942
- console.log(`[SentienceAPI] Collected ${rawData.length} elements from main frame`);
943
-
944
- // Step 1.5: Collect iframe snapshots and FLATTEN immediately
945
- // "Flatten Early" architecture: Merge iframe elements into main array before WASM
946
- // This allows WASM to process all elements uniformly (no recursion needed)
947
- let allRawElements = [...rawData]; // Start with main frame elements
948
- let totalIframeElements = 0;
949
-
950
- if (options.collectIframes !== false) {
951
- try {
952
- console.log(`[SentienceAPI] Starting iframe collection...`);
953
- const iframeSnapshots = await collectIframeSnapshots(options);
954
- console.log(`[SentienceAPI] Iframe collection complete. Received ${iframeSnapshots.size} snapshot(s)`);
955
-
956
- if (iframeSnapshots.size > 0) {
957
- // FLATTEN IMMEDIATELY: Don't nest them. Just append them with coordinate translation.
958
- iframeSnapshots.forEach((iframeSnapshot, iframeEl) => {
959
- // Debug: Log structure to verify data is correct
960
- // console.log(`[SentienceAPI] Processing iframe snapshot:`, iframeSnapshot);
961
-
962
- if (iframeSnapshot && iframeSnapshot.raw_elements) {
963
- const rawElementsCount = iframeSnapshot.raw_elements.length;
964
- console.log(`[SentienceAPI] Processing ${rawElementsCount} elements from iframe (src: ${iframeEl.src || 'unknown'})`);
965
- // Get iframe's bounding rect (offset for coordinate translation)
966
- const iframeRect = iframeEl.getBoundingClientRect();
967
- const offset = { x: iframeRect.x, y: iframeRect.y };
968
-
969
- // Get iframe context for frame switching (Playwright needs this)
970
- const iframeSrc = iframeEl.src || iframeEl.getAttribute('src') || '';
971
- let isSameOrigin = false;
972
- try {
973
- // Try to access contentWindow to check if same-origin
974
- isSameOrigin = iframeEl.contentWindow !== null;
975
- } catch (e) {
976
- isSameOrigin = false;
724
+ });
725
+ const allRawElements = [ ...rawData ];
726
+ let totalIframeElements = 0;
727
+ if (!1 !== options.collectIframes) try {
728
+ const iframeSnapshots = await async function(options = {}) {
729
+ const iframeData = new Map, iframes = Array.from(document.querySelectorAll("iframe"));
730
+ if (0 === iframes.length) return iframeData;
731
+ const iframePromises = iframes.map((iframe, idx) => {
732
+ const src = iframe.src || "";
733
+ return src.includes("doubleclick") || src.includes("googleadservices") || src.includes("ads system") ? Promise.resolve(null) : new Promise(resolve => {
734
+ const requestId = `iframe-${idx}-${Date.now()}`, timeout = setTimeout(() => {
735
+ resolve(null);
736
+ }, 5e3), listener = event => {
737
+ "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data, "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data?.requestId === requestId && (clearTimeout(timeout),
738
+ window.removeEventListener("message", listener), event.data.error ? resolve(null) : (event.data.snapshot,
739
+ resolve({
740
+ iframe: iframe,
741
+ data: event.data.snapshot,
742
+ error: null
743
+ })));
744
+ };
745
+ window.addEventListener("message", listener);
746
+ try {
747
+ iframe.contentWindow ? iframe.contentWindow.postMessage({
748
+ type: "SENTIENCE_IFRAME_SNAPSHOT_REQUEST",
749
+ requestId: requestId,
750
+ options: {
751
+ ...options,
752
+ collectIframes: !0
977
753
  }
978
-
979
- // Adjust coordinates and add iframe context to each element
980
- const adjustedElements = iframeSnapshot.raw_elements.map(el => {
981
- const adjusted = { ...el };
982
-
983
- // Adjust rect coordinates to parent viewport
984
- if (adjusted.rect) {
985
- adjusted.rect = {
986
- ...adjusted.rect,
987
- x: adjusted.rect.x + offset.x,
988
- y: adjusted.rect.y + offset.y
989
- };
990
- }
991
-
992
- // Add iframe context so agents can switch frames in Playwright
993
- adjusted.iframe_context = {
994
- src: iframeSrc,
995
- is_same_origin: isSameOrigin
996
- };
997
-
998
- return adjusted;
999
- });
1000
-
1001
- // Append flattened iframe elements to main array
1002
- allRawElements.push(...adjustedElements);
1003
- totalIframeElements += adjustedElements.length;
1004
- }
754
+ }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener),
755
+ resolve(null));
756
+ } catch (error) {
757
+ clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null);
758
+ }
759
+ });
760
+ });
761
+ return (await Promise.all(iframePromises)).forEach((result, idx) => {
762
+ result && result.data && !result.error ? iframeData.set(iframes[idx], result.data) : result && result.error;
763
+ }), iframeData;
764
+ }(options);
765
+ iframeSnapshots.size > 0 && iframeSnapshots.forEach((iframeSnapshot, iframeEl) => {
766
+ if (iframeSnapshot && iframeSnapshot.raw_elements) {
767
+ iframeSnapshot.raw_elements.length;
768
+ const iframeRect = iframeEl.getBoundingClientRect(), offset = {
769
+ x: iframeRect.x,
770
+ y: iframeRect.y
771
+ }, iframeSrc = iframeEl.src || iframeEl.getAttribute("src") || "";
772
+ let isSameOrigin = !1;
773
+ try {
774
+ isSameOrigin = null !== iframeEl.contentWindow;
775
+ } catch (e) {
776
+ isSameOrigin = !1;
777
+ }
778
+ const adjustedElements = iframeSnapshot.raw_elements.map(el => {
779
+ const adjusted = {
780
+ ...el
781
+ };
782
+ return adjusted.rect && (adjusted.rect = {
783
+ ...adjusted.rect,
784
+ x: adjusted.rect.x + offset.x,
785
+ y: adjusted.rect.y + offset.y
786
+ }), adjusted.iframe_context = {
787
+ src: iframeSrc,
788
+ is_same_origin: isSameOrigin
789
+ }, adjusted;
790
+ });
791
+ allRawElements.push(...adjustedElements), totalIframeElements += adjustedElements.length;
792
+ }
793
+ });
794
+ } catch (error) {}
795
+ const processed = await function(rawData, options) {
796
+ return new Promise((resolve, reject) => {
797
+ const requestId = Math.random().toString(36).substring(7);
798
+ let resolved = !1;
799
+ const timeout = setTimeout(() => {
800
+ resolved || (resolved = !0, window.removeEventListener("message", listener), reject(new Error("WASM processing timeout - extension may be unresponsive. Try reloading the extension.")));
801
+ }, 25e3), listener = e => {
802
+ if ("SENTIENCE_SNAPSHOT_RESULT" === e.data.type && e.data.requestId === requestId) {
803
+ if (resolved) return;
804
+ resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener),
805
+ e.data.error ? reject(new Error(e.data.error)) : resolve({
806
+ elements: e.data.elements,
807
+ raw_elements: e.data.raw_elements,
808
+ duration: e.data.duration
1005
809
  });
1006
-
1007
- // console.log(`[SentienceAPI] Merged ${iframeSnapshots.size} iframe(s). Total elements: ${allRawElements.length} (${rawData.length} main + ${totalIframeElements} iframe)`);
1008
810
  }
811
+ };
812
+ window.addEventListener("message", listener);
813
+ try {
814
+ window.postMessage({
815
+ type: "SENTIENCE_SNAPSHOT_REQUEST",
816
+ requestId: requestId,
817
+ rawData: rawData,
818
+ options: options
819
+ }, "*");
1009
820
  } catch (error) {
1010
- console.warn('[SentienceAPI] Iframe collection failed:', error);
821
+ resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener),
822
+ reject(new Error(`Failed to send snapshot request: ${error.message}`)));
1011
823
  }
1012
- }
1013
-
1014
- // Step 2: Send EVERYTHING to WASM (One giant flat list)
1015
- // Now WASM prunes iframe elements and main elements in one pass!
1016
- // No recursion needed - everything is already flat
1017
- console.log(`[SentienceAPI] Sending ${allRawElements.length} total elements to WASM (${rawData.length} main + ${totalIframeElements} iframe)`);
1018
- const processed = await processSnapshotInBackground(allRawElements, options);
1019
-
1020
- if (!processed || !processed.elements) {
1021
- throw new Error('WASM processing returned invalid result');
1022
- }
1023
-
1024
- // Step 3: Capture screenshot if requested
1025
- let screenshot = null;
1026
- if (options.screenshot) {
1027
- screenshot = await captureScreenshot(options.screenshot);
1028
- }
1029
-
1030
- // Step 4: Clean and return
1031
- const cleanedElements = cleanElement(processed.elements);
1032
- const cleanedRawElements = cleanElement(processed.raw_elements);
1033
-
1034
- // FIXED: Removed undefined 'totalIframeRawElements'
1035
- // FIXED: Logic updated for "Flatten Early" architecture.
1036
- // processed.elements ALREADY contains the merged iframe elements,
1037
- // so we simply use .length. No addition needed.
1038
-
1039
- const totalCount = cleanedElements.length;
1040
- const totalRaw = cleanedRawElements.length;
1041
- const iframeCount = totalIframeElements || 0;
1042
-
1043
- console.log(`[SentienceAPI] ✓ Complete: ${totalCount} Smart Elements, ${totalRaw} Raw Elements (includes ${iframeCount} from iframes) (WASM took ${processed.duration?.toFixed(1)}ms)`);
1044
-
1045
- return {
1046
- status: "success",
1047
- url: window.location.href,
1048
- viewport: {
1049
- width: window.innerWidth,
1050
- height: window.innerHeight
824
+ });
825
+ }(allRawElements, options);
826
+ if (!processed || !processed.elements) throw new Error("WASM processing returned invalid result");
827
+ let screenshot = null;
828
+ options.screenshot && (screenshot = await function(options) {
829
+ return new Promise(resolve => {
830
+ const requestId = Math.random().toString(36).substring(7), listener = e => {
831
+ "SENTIENCE_SCREENSHOT_RESULT" === e.data.type && e.data.requestId === requestId && (window.removeEventListener("message", listener),
832
+ resolve(e.data.screenshot));
833
+ };
834
+ window.addEventListener("message", listener), window.postMessage({
835
+ type: "SENTIENCE_SCREENSHOT_REQUEST",
836
+ requestId: requestId,
837
+ options: options
838
+ }, "*"), setTimeout(() => {
839
+ window.removeEventListener("message", listener), resolve(null);
840
+ }, 1e4);
841
+ });
842
+ }(options.screenshot));
843
+ const cleanedElements = cleanElement(processed.elements), cleanedRawElements = cleanElement(processed.raw_elements);
844
+ cleanedElements.length, cleanedRawElements.length;
845
+ let diagnostics;
846
+ try {
847
+ const lastMutationTs = window.__sentience_lastMutationTs, now = performance.now(), quietMs = "number" == typeof lastMutationTs && Number.isFinite(lastMutationTs) ? Math.max(0, now - lastMutationTs) : null, nodeCount = document.querySelectorAll("*").length;
848
+ diagnostics = {
849
+ metrics: {
850
+ ready_state: document.readyState || null,
851
+ quiet_ms: quietMs,
852
+ node_count: nodeCount
1051
853
  },
1052
- elements: cleanedElements,
1053
- raw_elements: cleanedRawElements,
1054
- screenshot: screenshot
854
+ captcha: detectCaptcha()
1055
855
  };
1056
- } catch (error) {
1057
- console.error('[SentienceAPI] snapshot() failed:', error);
1058
- console.error('[SentienceAPI] Error stack:', error.stack);
1059
- return {
1060
- status: "error",
1061
- error: error.message || 'Unknown error',
1062
- stack: error.stack
1063
- };
1064
- }
1065
- },
1066
-
1067
- // 2. Read Content (unchanged)
1068
- read: (options = {}) => {
1069
- const format = options.format || 'raw';
1070
- let content;
1071
-
1072
- if (format === 'raw') {
1073
- content = getRawHTML(document.body);
1074
- } else if (format === 'markdown') {
1075
- content = convertToMarkdown(document.body);
1076
- } else {
1077
- content = convertToText(document.body);
1078
- }
1079
-
856
+ } catch (e) {}
1080
857
  return {
1081
858
  status: "success",
1082
859
  url: window.location.href,
1083
- format: format,
1084
- content: content,
1085
- length: content.length
860
+ viewport: {
861
+ width: window.innerWidth,
862
+ height: window.innerHeight
863
+ },
864
+ elements: cleanedElements,
865
+ raw_elements: cleanedRawElements,
866
+ screenshot: screenshot,
867
+ diagnostics: diagnostics
1086
868
  };
1087
- },
1088
-
1089
- // 2b. Find Text Rectangle - Get exact pixel coordinates of specific text
1090
- findTextRect: (options = {}) => {
1091
- const {
1092
- text,
1093
- containerElement = document.body,
1094
- caseSensitive = false,
1095
- wholeWord = false,
1096
- maxResults = 10
1097
- } = options;
1098
-
1099
- if (!text || text.trim().length === 0) {
1100
- return {
1101
- status: "error",
1102
- error: "Text parameter is required"
1103
- };
1104
- }
1105
-
1106
- const results = [];
1107
- const searchText = caseSensitive ? text : text.toLowerCase();
1108
-
1109
- // Helper function to find text in a single text node
1110
- function findInTextNode(textNode) {
1111
- const nodeText = textNode.nodeValue;
1112
- const searchableText = caseSensitive ? nodeText : nodeText.toLowerCase();
1113
-
1114
- let startIndex = 0;
1115
- while (startIndex < nodeText.length && results.length < maxResults) {
1116
- const foundIndex = searchableText.indexOf(searchText, startIndex);
1117
-
1118
- if (foundIndex === -1) break;
1119
-
1120
- // Check whole word matching if required
1121
- if (wholeWord) {
1122
- const before = foundIndex > 0 ? nodeText[foundIndex - 1] : ' ';
1123
- const after = foundIndex + text.length < nodeText.length
1124
- ? nodeText[foundIndex + text.length]
1125
- : ' ';
1126
-
1127
- // Check if surrounded by word boundaries
1128
- if (!/\s/.test(before) || !/\s/.test(after)) {
1129
- startIndex = foundIndex + 1;
1130
- continue;
1131
- }
1132
- }
1133
-
1134
- try {
1135
- // Create range for this occurrence
1136
- const range = document.createRange();
1137
- range.setStart(textNode, foundIndex);
1138
- range.setEnd(textNode, foundIndex + text.length);
1139
-
1140
- const rect = range.getBoundingClientRect();
1141
-
1142
- // Only include visible rectangles
1143
- if (rect.width > 0 && rect.height > 0) {
1144
- results.push({
1145
- text: nodeText.substring(foundIndex, foundIndex + text.length),
1146
- rect: {
1147
- x: rect.left + window.scrollX,
1148
- y: rect.top + window.scrollY,
1149
- width: rect.width,
1150
- height: rect.height,
1151
- left: rect.left + window.scrollX,
1152
- top: rect.top + window.scrollY,
1153
- right: rect.right + window.scrollX,
1154
- bottom: rect.bottom + window.scrollY
1155
- },
1156
- viewport_rect: {
1157
- x: rect.left,
1158
- y: rect.top,
1159
- width: rect.width,
1160
- height: rect.height
1161
- },
1162
- context: {
1163
- before: nodeText.substring(Math.max(0, foundIndex - 20), foundIndex),
1164
- after: nodeText.substring(foundIndex + text.length, Math.min(nodeText.length, foundIndex + text.length + 20))
1165
- },
1166
- in_viewport: (
1167
- rect.top >= 0 &&
1168
- rect.left >= 0 &&
1169
- rect.bottom <= window.innerHeight &&
1170
- rect.right <= window.innerWidth
1171
- )
1172
- });
1173
- }
1174
- } catch (e) {
1175
- console.warn('[SentienceAPI] Failed to get rect for text:', e);
1176
- }
1177
-
1178
- startIndex = foundIndex + 1;
869
+ } catch (error) {
870
+ return {
871
+ status: "error",
872
+ error: error.message || "Unknown error",
873
+ stack: error.stack
874
+ };
875
+ }
876
+ }
877
+ function read(options = {}) {
878
+ const format = options.format || "raw";
879
+ let content;
880
+ return content = "raw" === format ? getRawHTML(document.body) : "markdown" === format ? function(root) {
881
+ const rawHTML = getRawHTML(root), tempDiv = document.createElement("div");
882
+ tempDiv.innerHTML = rawHTML;
883
+ let markdown = "", insideLink = !1;
884
+ return function walk(node) {
885
+ if (node.nodeType === Node.TEXT_NODE) {
886
+ const text = node.textContent.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ");
887
+ return void (text.trim() && (markdown += text));
1179
888
  }
1180
- }
1181
-
1182
- // Tree walker to find all text nodes
1183
- const walker = document.createTreeWalker(
1184
- containerElement,
1185
- NodeFilter.SHOW_TEXT,
1186
- {
1187
- acceptNode: function(node) {
1188
- // Skip script, style, and empty text nodes
1189
- const parent = node.parentElement;
1190
- if (!parent) return NodeFilter.FILTER_REJECT;
1191
-
1192
- const tagName = parent.tagName.toLowerCase();
1193
- if (tagName === 'script' || tagName === 'style' || tagName === 'noscript') {
1194
- return NodeFilter.FILTER_REJECT;
1195
- }
1196
-
1197
- // Skip whitespace-only nodes
1198
- if (!node.nodeValue || node.nodeValue.trim().length === 0) {
1199
- return NodeFilter.FILTER_REJECT;
1200
- }
1201
-
1202
- // Check if element is visible
1203
- const computedStyle = window.getComputedStyle(parent);
1204
- if (computedStyle.display === 'none' ||
1205
- computedStyle.visibility === 'hidden' ||
1206
- computedStyle.opacity === '0') {
1207
- return NodeFilter.FILTER_REJECT;
1208
- }
1209
-
1210
- return NodeFilter.FILTER_ACCEPT;
1211
- }
889
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
890
+ const tag = node.tagName.toLowerCase();
891
+ if ("h1" === tag && (markdown += "\n# "), "h2" === tag && (markdown += "\n## "),
892
+ "h3" === tag && (markdown += "\n### "), "li" === tag && (markdown += "\n- "), insideLink || "p" !== tag && "div" !== tag && "br" !== tag || (markdown += "\n"),
893
+ "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"),
894
+ "a" === tag && (markdown += "[", insideLink = !0), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk),
895
+ "a" === tag) {
896
+ const href = node.getAttribute("href");
897
+ markdown += href ? `](${href})` : "]", insideLink = !1;
1212
898
  }
1213
- );
1214
-
1215
- // Walk through all text nodes
1216
- let currentNode;
1217
- while ((currentNode = walker.nextNode()) && results.length < maxResults) {
1218
- findInTextNode(currentNode);
1219
- }
1220
-
1221
- return {
1222
- status: "success",
1223
- query: text,
1224
- case_sensitive: caseSensitive,
1225
- whole_word: wholeWord,
1226
- matches: results.length,
1227
- results: results,
1228
- viewport: {
1229
- width: window.innerWidth,
1230
- height: window.innerHeight,
1231
- scroll_x: window.scrollX,
1232
- scroll_y: window.scrollY
899
+ "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"),
900
+ insideLink || "h1" !== tag && "h2" !== tag && "h3" !== tag && "p" !== tag && "div" !== tag || (markdown += "\n");
901
+ }(tempDiv), markdown.replace(/\n{3,}/g, "\n\n").trim();
902
+ }(document.body) : function(root) {
903
+ let text = "";
904
+ return function walk(node) {
905
+ if (node.nodeType !== Node.TEXT_NODE) {
906
+ if (node.nodeType === Node.ELEMENT_NODE) {
907
+ const tag = node.tagName.toLowerCase();
908
+ if ([ "nav", "footer", "header", "script", "style", "noscript", "iframe", "svg" ].includes(tag)) return;
909
+ const style = window.getComputedStyle(node);
910
+ if ("none" === style.display || "hidden" === style.visibility) return;
911
+ const isBlock = "block" === style.display || "flex" === style.display || "P" === node.tagName || "DIV" === node.tagName;
912
+ isBlock && (text += " "), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk),
913
+ isBlock && (text += "\n");
914
+ }
915
+ } else text += node.textContent;
916
+ }(root || document.body), text.replace(/\n{3,}/g, "\n\n").trim();
917
+ }(document.body), {
918
+ status: "success",
919
+ url: window.location.href,
920
+ format: format,
921
+ content: content,
922
+ length: content.length
923
+ };
924
+ }
925
+ function findTextRect(options = {}) {
926
+ const {text: text, containerElement: containerElement = document.body, caseSensitive: caseSensitive = !1, wholeWord: wholeWord = !1, maxResults: maxResults = 10} = options;
927
+ if (!text || 0 === text.trim().length) return {
928
+ status: "error",
929
+ error: "Text parameter is required"
930
+ };
931
+ const results = [], searchText = caseSensitive ? text : text.toLowerCase();
932
+ function findInTextNode(textNode) {
933
+ const nodeText = textNode.nodeValue, searchableText = caseSensitive ? nodeText : nodeText.toLowerCase();
934
+ let startIndex = 0;
935
+ for (;startIndex < nodeText.length && results.length < maxResults; ) {
936
+ const foundIndex = searchableText.indexOf(searchText, startIndex);
937
+ if (-1 === foundIndex) break;
938
+ if (wholeWord) {
939
+ const before = foundIndex > 0 ? nodeText[foundIndex - 1] : " ", after = foundIndex + text.length < nodeText.length ? nodeText[foundIndex + text.length] : " ";
940
+ if (!/\s/.test(before) || !/\s/.test(after)) {
941
+ startIndex = foundIndex + 1;
942
+ continue;
943
+ }
1233
944
  }
1234
- };
1235
- },
1236
-
1237
- // 3. Click Action (unchanged)
1238
- click: (id) => {
1239
- const el = window.sentience_registry[id];
1240
- if (el) {
1241
- el.click();
1242
- el.focus();
1243
- return true;
1244
- }
1245
- return false;
1246
- },
1247
-
1248
- // 4. Inspector Mode: Start Recording for Golden Set Collection
1249
- startRecording: (options = {}) => {
1250
- const {
1251
- highlightColor = '#ff0000',
1252
- successColor = '#00ff00',
1253
- autoDisableTimeout = 30 * 60 * 1000, // 30 minutes default
1254
- keyboardShortcut = 'Ctrl+Shift+I'
1255
- } = options;
1256
-
1257
- console.log("🔴 [Sentience] Recording Mode STARTED. Click an element to copy its Ground Truth JSON.");
1258
- console.log(` Press ${keyboardShortcut} or call stopRecording() to stop.`);
1259
-
1260
- // Validate registry is populated
1261
- if (!window.sentience_registry || window.sentience_registry.length === 0) {
1262
- console.warn("⚠️ Registry empty. Call `await window.sentience.snapshot()` first to populate registry.");
1263
- alert("Registry empty. Run `await window.sentience.snapshot()` first!");
1264
- return () => {}; // Return no-op cleanup function
945
+ try {
946
+ const range = document.createRange();
947
+ range.setStart(textNode, foundIndex), range.setEnd(textNode, foundIndex + text.length);
948
+ const rect = range.getBoundingClientRect();
949
+ rect.width > 0 && rect.height > 0 && results.push({
950
+ text: nodeText.substring(foundIndex, foundIndex + text.length),
951
+ rect: {
952
+ x: rect.left + window.scrollX,
953
+ y: rect.top + window.scrollY,
954
+ width: rect.width,
955
+ height: rect.height,
956
+ left: rect.left + window.scrollX,
957
+ top: rect.top + window.scrollY,
958
+ right: rect.right + window.scrollX,
959
+ bottom: rect.bottom + window.scrollY
960
+ },
961
+ viewport_rect: {
962
+ x: rect.left,
963
+ y: rect.top,
964
+ width: rect.width,
965
+ height: rect.height
966
+ },
967
+ context: {
968
+ before: nodeText.substring(Math.max(0, foundIndex - 20), foundIndex),
969
+ after: nodeText.substring(foundIndex + text.length, Math.min(nodeText.length, foundIndex + text.length + 20))
970
+ },
971
+ in_viewport: rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
972
+ });
973
+ } catch (e) {}
974
+ startIndex = foundIndex + 1;
1265
975
  }
1266
-
1267
- // Create reverse mapping for O(1) lookup (fixes registry lookup bug)
1268
- window.sentience_registry_map = new Map();
1269
- window.sentience_registry.forEach((el, idx) => {
1270
- if (el) window.sentience_registry_map.set(el, idx);
1271
- });
1272
-
1273
- // Create highlight box overlay
1274
- let highlightBox = document.getElementById('sentience-highlight-box');
1275
- if (!highlightBox) {
1276
- highlightBox = document.createElement('div');
1277
- highlightBox.id = 'sentience-highlight-box';
1278
- highlightBox.style.cssText = `
1279
- position: fixed;
1280
- pointer-events: none;
1281
- z-index: 2147483647;
1282
- border: 2px solid ${highlightColor};
1283
- background: rgba(255, 0, 0, 0.1);
1284
- display: none;
1285
- transition: all 0.1s ease;
1286
- box-sizing: border-box;
1287
- `;
1288
- document.body.appendChild(highlightBox);
976
+ }
977
+ const walker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT, {
978
+ acceptNode(node) {
979
+ const parent = node.parentElement;
980
+ if (!parent) return NodeFilter.FILTER_REJECT;
981
+ const tagName = parent.tagName.toLowerCase();
982
+ if ("script" === tagName || "style" === tagName || "noscript" === tagName) return NodeFilter.FILTER_REJECT;
983
+ if (!node.nodeValue || 0 === node.nodeValue.trim().length) return NodeFilter.FILTER_REJECT;
984
+ const computedStyle = window.getComputedStyle(parent);
985
+ return "none" === computedStyle.display || "hidden" === computedStyle.visibility || "0" === computedStyle.opacity ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
1289
986
  }
1290
-
1291
- // Create visual indicator (red border on page when recording)
1292
- let recordingIndicator = document.getElementById('sentience-recording-indicator');
1293
- if (!recordingIndicator) {
1294
- recordingIndicator = document.createElement('div');
1295
- recordingIndicator.id = 'sentience-recording-indicator';
1296
- recordingIndicator.style.cssText = `
1297
- position: fixed;
1298
- top: 0;
1299
- left: 0;
1300
- right: 0;
1301
- height: 3px;
1302
- background: ${highlightColor};
1303
- z-index: 2147483646;
1304
- pointer-events: none;
1305
- `;
1306
- document.body.appendChild(recordingIndicator);
987
+ });
988
+ let currentNode;
989
+ for (;(currentNode = walker.nextNode()) && results.length < maxResults; ) findInTextNode(currentNode);
990
+ return {
991
+ status: "success",
992
+ query: text,
993
+ case_sensitive: caseSensitive,
994
+ whole_word: wholeWord,
995
+ matches: results.length,
996
+ results: results,
997
+ viewport: {
998
+ width: window.innerWidth,
999
+ height: window.innerHeight,
1000
+ scroll_x: window.scrollX,
1001
+ scroll_y: window.scrollY
1307
1002
  }
1308
- recordingIndicator.style.display = 'block';
1309
-
1310
- // Hover handler (visual feedback)
1311
- const mouseOverHandler = (e) => {
1312
- const el = e.target;
1313
- if (!el || el === highlightBox || el === recordingIndicator) return;
1314
-
1315
- const rect = el.getBoundingClientRect();
1316
- highlightBox.style.display = 'block';
1317
- highlightBox.style.top = (rect.top + window.scrollY) + 'px';
1318
- highlightBox.style.left = (rect.left + window.scrollX) + 'px';
1319
- highlightBox.style.width = rect.width + 'px';
1320
- highlightBox.style.height = rect.height + 'px';
1321
- };
1322
-
1323
- // Click handler (capture ground truth data)
1324
- const clickHandler = (e) => {
1325
- e.preventDefault();
1326
- e.stopPropagation();
1327
-
1328
- const el = e.target;
1329
- if (!el || el === highlightBox || el === recordingIndicator) return;
1330
-
1331
- // Use Map for reliable O(1) lookup
1332
- const sentienceId = window.sentience_registry_map.get(el);
1333
- if (sentienceId === undefined) {
1334
- console.warn("⚠️ Element not found in Sentience Registry. Did you run snapshot() first?");
1335
- alert("Element not in registry. Run `await window.sentience.snapshot()` first!");
1336
- return;
1337
- }
1338
-
1339
- // Extract raw data (ground truth + raw signals, NOT model outputs)
1340
- const rawData = extractRawElementData(el);
1341
- const selector = getUniqueSelector(el);
1342
- const role = el.getAttribute('role') || el.tagName.toLowerCase();
1343
- const text = getText(el);
1344
-
1345
- // Build golden set JSON (ground truth + raw signals only)
1346
- const snippet = {
1347
- task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`,
1348
- url: window.location.href,
1349
- timestamp: new Date().toISOString(),
1350
- target_criteria: {
1351
- id: sentienceId,
1352
- selector: selector,
1353
- role: role,
1354
- text: text.substring(0, 50)
1003
+ };
1004
+ }
1005
+ function click(id) {
1006
+ const el = window.sentience_registry[id];
1007
+ return !!el && (el.click(), el.focus(), !0);
1008
+ }
1009
+ function startRecording(options = {}) {
1010
+ const {highlightColor: highlightColor = "#ff0000", successColor: successColor = "#00ff00", autoDisableTimeout: autoDisableTimeout = 18e5, keyboardShortcut: keyboardShortcut = "Ctrl+Shift+I"} = options;
1011
+ if (!window.sentience_registry || 0 === window.sentience_registry.length) return alert("Registry empty. Run `await window.sentience.snapshot()` first!"),
1012
+ () => {};
1013
+ window.sentience_registry_map = new Map, window.sentience_registry.forEach((el, idx) => {
1014
+ el && window.sentience_registry_map.set(el, idx);
1015
+ });
1016
+ let highlightBox = document.getElementById("sentience-highlight-box");
1017
+ highlightBox || (highlightBox = document.createElement("div"), highlightBox.id = "sentience-highlight-box",
1018
+ 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 `,
1019
+ document.body.appendChild(highlightBox));
1020
+ let recordingIndicator = document.getElementById("sentience-recording-indicator");
1021
+ recordingIndicator || (recordingIndicator = document.createElement("div"), recordingIndicator.id = "sentience-recording-indicator",
1022
+ 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 `,
1023
+ document.body.appendChild(recordingIndicator)), recordingIndicator.style.display = "block";
1024
+ const mouseOverHandler = e => {
1025
+ const el = e.target;
1026
+ if (!el || el === highlightBox || el === recordingIndicator) return;
1027
+ const rect = el.getBoundingClientRect();
1028
+ highlightBox.style.display = "block", highlightBox.style.top = rect.top + window.scrollY + "px",
1029
+ highlightBox.style.left = rect.left + window.scrollX + "px", highlightBox.style.width = rect.width + "px",
1030
+ highlightBox.style.height = rect.height + "px";
1031
+ }, clickHandler = e => {
1032
+ e.preventDefault(), e.stopPropagation();
1033
+ const el = e.target;
1034
+ if (!el || el === highlightBox || el === recordingIndicator) return;
1035
+ const sentienceId = window.sentience_registry_map.get(el);
1036
+ if (void 0 === sentienceId) return void alert("Element not in registry. Run `await window.sentience.snapshot()` first!");
1037
+ const rawData = function(el) {
1038
+ const style = window.getComputedStyle(el), rect = el.getBoundingClientRect();
1039
+ return {
1040
+ tag: el.tagName,
1041
+ rect: {
1042
+ x: Math.round(rect.x),
1043
+ y: Math.round(rect.y),
1044
+ width: Math.round(rect.width),
1045
+ height: Math.round(rect.height)
1355
1046
  },
1356
- debug_snapshot: rawData
1047
+ styles: {
1048
+ cursor: style.cursor || null,
1049
+ backgroundColor: style.backgroundColor || null,
1050
+ color: style.color || null,
1051
+ fontWeight: style.fontWeight || null,
1052
+ fontSize: style.fontSize || null,
1053
+ display: style.display || null,
1054
+ position: style.position || null,
1055
+ zIndex: style.zIndex || null,
1056
+ opacity: style.opacity || null,
1057
+ visibility: style.visibility || null
1058
+ },
1059
+ attributes: {
1060
+ role: el.getAttribute("role") || null,
1061
+ type: el.getAttribute("type") || null,
1062
+ ariaLabel: el.getAttribute("aria-label") || null,
1063
+ id: el.id || null,
1064
+ className: el.className || null
1065
+ }
1357
1066
  };
1358
-
1359
- // Copy to clipboard
1360
- const jsonString = JSON.stringify(snippet, null, 2);
1361
- navigator.clipboard.writeText(jsonString).then(() => {
1362
- console.log("✅ Copied Ground Truth to clipboard:", snippet);
1363
-
1364
- // Flash green to indicate success
1365
- highlightBox.style.border = `2px solid ${successColor}`;
1366
- highlightBox.style.background = 'rgba(0, 255, 0, 0.2)';
1367
- setTimeout(() => {
1368
- highlightBox.style.border = `2px solid ${highlightColor}`;
1369
- highlightBox.style.background = 'rgba(255, 0, 0, 0.1)';
1370
- }, 500);
1371
- }).catch(err => {
1372
- console.error("❌ Failed to copy to clipboard:", err);
1373
- alert("Failed to copy to clipboard. Check console for JSON.");
1374
- });
1375
- };
1376
-
1377
- // Auto-disable timeout
1378
- let timeoutId = null;
1379
-
1380
- // Cleanup function to stop recording (defined before use)
1381
- const stopRecording = () => {
1382
- document.removeEventListener('mouseover', mouseOverHandler, true);
1383
- document.removeEventListener('click', clickHandler, true);
1384
- document.removeEventListener('keydown', keyboardHandler, true);
1385
-
1386
- if (timeoutId) {
1387
- clearTimeout(timeoutId);
1388
- timeoutId = null;
1389
- }
1390
-
1391
- if (highlightBox) {
1392
- highlightBox.style.display = 'none';
1393
- }
1394
-
1395
- if (recordingIndicator) {
1396
- recordingIndicator.style.display = 'none';
1397
- }
1398
-
1399
- // Clean up registry map (optional, but good practice)
1400
- if (window.sentience_registry_map) {
1401
- window.sentience_registry_map.clear();
1067
+ }(el), selector = function(el) {
1068
+ if (!el || !el.tagName) return "";
1069
+ if (el.id) return `#${el.id}`;
1070
+ for (const attr of el.attributes) if (attr.name.startsWith("data-") || "aria-label" === attr.name) {
1071
+ const value = attr.value ? attr.value.replace(/"/g, '\\"') : "";
1072
+ return `${el.tagName.toLowerCase()}[${attr.name}="${value}"]`;
1402
1073
  }
1403
-
1404
- // Remove global reference
1405
- if (window.sentience_stopRecording === stopRecording) {
1406
- delete window.sentience_stopRecording;
1407
- }
1408
-
1409
- console.log("⚪ [Sentience] Recording Mode STOPPED.");
1410
- };
1411
-
1412
- // Keyboard shortcut handler (defined after stopRecording)
1413
- const keyboardHandler = (e) => {
1414
- // Ctrl+Shift+I or Cmd+Shift+I
1415
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'I') {
1416
- e.preventDefault();
1417
- stopRecording();
1074
+ const path = [];
1075
+ let current = el;
1076
+ for (;current && current !== document.body && current !== document.documentElement; ) {
1077
+ let selector = current.tagName.toLowerCase();
1078
+ if (current.id) {
1079
+ selector = `#${current.id}`, path.unshift(selector);
1080
+ break;
1081
+ }
1082
+ if (current.className && "string" == typeof current.className) {
1083
+ const classes = current.className.trim().split(/\s+/).filter(c => c);
1084
+ classes.length > 0 && (selector += `.${classes[0]}`);
1085
+ }
1086
+ if (current.parentElement) {
1087
+ const sameTagSiblings = Array.from(current.parentElement.children).filter(s => s.tagName === current.tagName), index = sameTagSiblings.indexOf(current);
1088
+ (index > 0 || sameTagSiblings.length > 1) && (selector += `:nth-of-type(${index + 1})`);
1089
+ }
1090
+ path.unshift(selector), current = current.parentElement;
1418
1091
  }
1419
- };
1420
-
1421
- // Attach event listeners (use capture phase to intercept early)
1422
- document.addEventListener('mouseover', mouseOverHandler, true);
1423
- document.addEventListener('click', clickHandler, true);
1424
- document.addEventListener('keydown', keyboardHandler, true);
1425
-
1426
- // Set up auto-disable timeout
1427
- if (autoDisableTimeout > 0) {
1428
- timeoutId = setTimeout(() => {
1429
- console.log("⏰ [Sentience] Recording Mode auto-disabled after timeout.");
1430
- stopRecording();
1431
- }, autoDisableTimeout);
1432
- }
1433
-
1434
- // Store stop function globally for keyboard shortcut access
1435
- window.sentience_stopRecording = stopRecording;
1436
-
1437
- return stopRecording;
1438
- }
1439
- };
1440
-
1441
- /**
1442
- * Show overlay highlighting specific elements with Shadow DOM
1443
- * @param {Array} elements - List of elements with bbox, importance, visual_cues
1444
- * @param {number} targetElementId - Optional ID of target element (shown in red)
1445
- */
1446
- window.sentience.showOverlay = function(elements, targetElementId = null) {
1447
- if (!elements || !Array.isArray(elements)) {
1448
- console.warn('[Sentience] showOverlay: elements must be an array');
1449
- return;
1450
- }
1451
-
1452
- window.postMessage({
1453
- type: 'SENTIENCE_SHOW_OVERLAY',
1092
+ return path.join(" > ") || el.tagName.toLowerCase();
1093
+ }(el), role = el.getAttribute("role") || el.tagName.toLowerCase(), text = getText(el), snippet = {
1094
+ task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? "..." : ""}`,
1095
+ url: window.location.href,
1096
+ timestamp: (new Date).toISOString(),
1097
+ target_criteria: {
1098
+ id: sentienceId,
1099
+ selector: selector,
1100
+ role: role,
1101
+ text: text.substring(0, 50)
1102
+ },
1103
+ debug_snapshot: rawData
1104
+ }, jsonString = JSON.stringify(snippet, null, 2);
1105
+ navigator.clipboard.writeText(jsonString).then(() => {
1106
+ highlightBox.style.border = `2px solid ${successColor}`, highlightBox.style.background = "rgba(0, 255, 0, 0.2)",
1107
+ setTimeout(() => {
1108
+ highlightBox.style.border = `2px solid ${highlightColor}`, highlightBox.style.background = "rgba(255, 0, 0, 0.1)";
1109
+ }, 500);
1110
+ }).catch(err => {
1111
+ alert("Failed to copy to clipboard. Check console for JSON.");
1112
+ });
1113
+ };
1114
+ let timeoutId = null;
1115
+ const stopRecording = () => {
1116
+ document.removeEventListener("mouseover", mouseOverHandler, !0), document.removeEventListener("click", clickHandler, !0),
1117
+ document.removeEventListener("keydown", keyboardHandler, !0), timeoutId && (clearTimeout(timeoutId),
1118
+ timeoutId = null), highlightBox && (highlightBox.style.display = "none"), recordingIndicator && (recordingIndicator.style.display = "none"),
1119
+ window.sentience_registry_map && window.sentience_registry_map.clear(), window.sentience_stopRecording === stopRecording && delete window.sentience_stopRecording;
1120
+ }, keyboardHandler = e => {
1121
+ (e.ctrlKey || e.metaKey) && e.shiftKey && "I" === e.key && (e.preventDefault(),
1122
+ stopRecording());
1123
+ };
1124
+ return document.addEventListener("mouseover", mouseOverHandler, !0), document.addEventListener("click", clickHandler, !0),
1125
+ document.addEventListener("keydown", keyboardHandler, !0), autoDisableTimeout > 0 && (timeoutId = setTimeout(() => {
1126
+ stopRecording();
1127
+ }, autoDisableTimeout)), window.sentience_stopRecording = stopRecording, stopRecording;
1128
+ }
1129
+ function showOverlay(elements, targetElementId = null) {
1130
+ elements && Array.isArray(elements) && window.postMessage({
1131
+ type: "SENTIENCE_SHOW_OVERLAY",
1454
1132
  elements: elements,
1455
1133
  targetElementId: targetElementId,
1456
1134
  timestamp: Date.now()
1457
- }, '*');
1458
-
1459
- console.log(`[Sentience] Overlay requested for ${elements.length} elements`);
1460
- };
1461
-
1462
- /**
1463
- * Clear overlay manually
1464
- */
1465
- window.sentience.clearOverlay = function() {
1135
+ }, "*");
1136
+ }
1137
+ function showGrid(grids, targetGridId = null) {
1138
+ grids && Array.isArray(grids) && window.postMessage({
1139
+ type: "SENTIENCE_SHOW_GRID_OVERLAY",
1140
+ grids: grids,
1141
+ targetGridId: targetGridId,
1142
+ timestamp: Date.now()
1143
+ }, "*");
1144
+ }
1145
+ function clearOverlay() {
1466
1146
  window.postMessage({
1467
- type: 'SENTIENCE_CLEAR_OVERLAY'
1468
- }, '*');
1469
- console.log('[Sentience] Overlay cleared');
1470
- };
1471
-
1472
- console.log('[SentienceAPI] Ready! (CSP-Resistant - WASM runs in background)');
1473
- })();
1147
+ type: "SENTIENCE_CLEAR_OVERLAY"
1148
+ }, "*");
1149
+ }
1150
+ (async () => {
1151
+ const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId;
1152
+ let extId = getExtensionId();
1153
+ extId || await new Promise(resolve => {
1154
+ const check = setInterval(() => {
1155
+ extId = getExtensionId(), extId && (clearInterval(check), resolve());
1156
+ }, 50);
1157
+ setTimeout(() => resolve(), 5e3);
1158
+ }), extId && (window.sentience_registry = [], window.sentience = {
1159
+ snapshot: snapshot,
1160
+ read: read,
1161
+ findTextRect: findTextRect,
1162
+ click: click,
1163
+ startRecording: startRecording,
1164
+ showOverlay: showOverlay,
1165
+ showGrid: showGrid,
1166
+ clearOverlay: clearOverlay
1167
+ }, window.sentience_iframe_handler_setup || (window.addEventListener("message", async event => {
1168
+ if ("SENTIENCE_IFRAME_SNAPSHOT_REQUEST" === event.data?.type) {
1169
+ const {requestId: requestId, options: options} = event.data;
1170
+ try {
1171
+ const snapshotOptions = {
1172
+ ...options,
1173
+ collectIframes: !0,
1174
+ waitForStability: (options.waitForStability, !1)
1175
+ }, snapshot = await window.sentience.snapshot(snapshotOptions);
1176
+ event.source && event.source.postMessage && event.source.postMessage({
1177
+ type: "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE",
1178
+ requestId: requestId,
1179
+ snapshot: snapshot,
1180
+ error: null
1181
+ }, "*");
1182
+ } catch (error) {
1183
+ event.source && event.source.postMessage && event.source.postMessage({
1184
+ type: "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE",
1185
+ requestId: requestId,
1186
+ snapshot: null,
1187
+ error: error.message
1188
+ }, "*");
1189
+ }
1190
+ }
1191
+ }), window.sentience_iframe_handler_setup = !0));
1192
+ })();
1193
+ }();