camel-ai 0.2.74a5__py3-none-any.whl → 0.2.75a2__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 camel-ai might be problematic. Click here for more details.

Files changed (68) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +2 -2
  3. camel/interpreters/e2b_interpreter.py +34 -1
  4. camel/models/aiml_model.py +1 -16
  5. camel/models/anthropic_model.py +6 -22
  6. camel/models/aws_bedrock_model.py +1 -16
  7. camel/models/azure_openai_model.py +1 -16
  8. camel/models/base_model.py +0 -12
  9. camel/models/cohere_model.py +1 -16
  10. camel/models/crynux_model.py +1 -16
  11. camel/models/deepseek_model.py +1 -16
  12. camel/models/gemini_model.py +1 -16
  13. camel/models/groq_model.py +1 -17
  14. camel/models/internlm_model.py +1 -16
  15. camel/models/litellm_model.py +1 -16
  16. camel/models/lmstudio_model.py +1 -17
  17. camel/models/mistral_model.py +1 -16
  18. camel/models/modelscope_model.py +1 -16
  19. camel/models/moonshot_model.py +6 -22
  20. camel/models/nemotron_model.py +0 -5
  21. camel/models/netmind_model.py +1 -16
  22. camel/models/novita_model.py +1 -16
  23. camel/models/nvidia_model.py +1 -16
  24. camel/models/ollama_model.py +1 -16
  25. camel/models/openai_compatible_model.py +0 -3
  26. camel/models/openai_model.py +1 -16
  27. camel/models/openrouter_model.py +1 -17
  28. camel/models/ppio_model.py +1 -16
  29. camel/models/qianfan_model.py +1 -16
  30. camel/models/qwen_model.py +1 -16
  31. camel/models/reka_model.py +1 -16
  32. camel/models/samba_model.py +0 -32
  33. camel/models/sglang_model.py +1 -16
  34. camel/models/siliconflow_model.py +1 -16
  35. camel/models/stub_model.py +0 -4
  36. camel/models/togetherai_model.py +1 -16
  37. camel/models/vllm_model.py +1 -16
  38. camel/models/volcano_model.py +0 -17
  39. camel/models/watsonx_model.py +1 -16
  40. camel/models/yi_model.py +1 -16
  41. camel/models/zhipuai_model.py +1 -16
  42. camel/societies/workforce/prompts.py +1 -8
  43. camel/toolkits/__init__.py +0 -2
  44. camel/toolkits/hybrid_browser_toolkit/config_loader.py +3 -0
  45. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +225 -0
  46. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +164 -8
  47. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +2 -0
  48. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +106 -1
  49. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +19 -1
  50. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +20 -0
  51. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +41 -0
  52. camel/toolkits/hybrid_browser_toolkit_py/actions.py +158 -0
  53. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +55 -8
  54. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +43 -0
  55. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +312 -3
  56. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +10 -4
  57. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +45 -4
  58. camel/toolkits/search_toolkit.py +140 -27
  59. camel/types/__init__.py +2 -2
  60. camel/types/enums.py +20 -1
  61. camel/types/openai_types.py +2 -2
  62. camel/utils/mcp.py +2 -2
  63. camel/utils/token_counting.py +18 -3
  64. {camel_ai-0.2.74a5.dist-info → camel_ai-0.2.75a2.dist-info}/METADATA +6 -6
  65. {camel_ai-0.2.74a5.dist-info → camel_ai-0.2.75a2.dist-info}/RECORD +67 -68
  66. camel/toolkits/openai_agent_toolkit.py +0 -135
  67. {camel_ai-0.2.74a5.dist-info → camel_ai-0.2.75a2.dist-info}/WHEEL +0 -0
  68. {camel_ai-0.2.74a5.dist-info → camel_ai-0.2.75a2.dist-info}/licenses/LICENSE +0 -0
@@ -73,11 +73,16 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
73
73
  "browser_select",
74
74
  "browser_scroll",
75
75
  "browser_enter",
76
+ "browser_mouse_control",
77
+ "browser_mouse_drag",
78
+ "browser_press_key",
76
79
  "browser_wait_user",
77
80
  "browser_solve_task",
78
81
  "browser_switch_tab",
79
82
  "browser_close_tab",
80
83
  "browser_get_tab_info",
84
+ "browser_console_view",
85
+ "browser_console_exec",
81
86
  ]
82
87
 
83
88
  def __init__(
@@ -99,6 +104,7 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
99
104
  screenshot_timeout: Optional[int] = None,
100
105
  page_stability_timeout: Optional[int] = None,
101
106
  dom_content_loaded_timeout: Optional[int] = None,
107
+ viewport_limit: bool = False,
102
108
  ) -> None:
103
109
  r"""Initialize the HybridBrowserToolkit.
104
110
 
@@ -182,6 +188,10 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
182
188
  HYBRID_BROWSER_DOM_CONTENT_LOADED_TIMEOUT or defaults to
183
189
  5000ms.
184
190
  Defaults to `None`.
191
+ viewport_limit (bool): When True, only return snapshot results
192
+ visible in the current viewport. When False, return all
193
+ elements on the page regardless of visibility.
194
+ Defaults to `False`.
185
195
  """
186
196
  super().__init__()
187
197
  RegisteredAgentToolkit.__init__(self)
@@ -193,6 +203,7 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
193
203
  self._browser_log_to_file = browser_log_to_file
194
204
  self._default_start_url = default_start_url
195
205
  self._session_id = session_id or "default"
206
+ self._viewport_limit = viewport_limit
196
207
 
197
208
  # Store timeout configuration
198
209
  self._default_timeout = default_timeout
@@ -309,7 +320,7 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
309
320
  # Try to close browser with a timeout to prevent hanging
310
321
  try:
311
322
  loop.run_until_complete(
312
- asyncio.wait_for(self.close_browser(), timeout=2.0)
323
+ asyncio.wait_for(self.browser_close(), timeout=2.0)
313
324
  )
314
325
  except asyncio.TimeoutError:
315
326
  pass # Skip cleanup if it takes too long
@@ -550,7 +561,7 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
550
561
  )
551
562
 
552
563
  async def _get_unified_analysis(
553
- self, max_retries: int = 3
564
+ self, max_retries: int = 3, viewport_limit: Optional[bool] = None
554
565
  ) -> Dict[str, Any]:
555
566
  r"""Get unified analysis data from the page with retry mechanism for
556
567
  navigation issues."""
@@ -573,7 +584,15 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
573
584
  # Don't fail if DOM wait times out
574
585
  pass
575
586
 
576
- result = await page.evaluate(self._unified_script)
587
+ # Use instance viewport_limit if parameter not provided
588
+ use_viewport_limit = (
589
+ viewport_limit
590
+ if viewport_limit is not None
591
+ else self._viewport_limit
592
+ )
593
+ result = await page.evaluate(
594
+ self._unified_script, use_viewport_limit
595
+ )
577
596
 
578
597
  if not isinstance(result, dict):
579
598
  logger.warning(f"Invalid result type: {type(result)}")
@@ -1703,6 +1722,149 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
1703
1722
 
1704
1723
  return result
1705
1724
 
1725
+ @action_logger
1726
+ async def browser_mouse_control(
1727
+ self, *, control: str, x: float, y: float
1728
+ ) -> Dict[str, Any]:
1729
+ r"""Control the mouse to interact with browser with x, y coordinates
1730
+
1731
+ Args:
1732
+ control (str): The action to perform: 'click', 'right_click'
1733
+ or 'dblclick'.
1734
+ x (float): x-coordinate for the control action.
1735
+ y (float): y-coordinate for the control action.
1736
+
1737
+ Returns:
1738
+ Dict[str, Any]: A dictionary with the result of the action:
1739
+ - "result" (str): Confirmation of the action.
1740
+ - "snapshot" (str): A new page snapshot.
1741
+ - "tabs" (List[Dict]): Information about all open tabs.
1742
+ - "current_tab" (int): Index of the active tab.
1743
+ - "total_tabs" (int): Total number of open tabs.
1744
+ """
1745
+ if control not in ("click", "right_click", "dblclick"):
1746
+ tab_info = await self._get_tab_info_for_output()
1747
+ return {
1748
+ "result": "Error: supported control actions are "
1749
+ "'click' or 'dblclick'",
1750
+ "snapshot": "",
1751
+ **tab_info,
1752
+ }
1753
+
1754
+ action = {"type": "mouse_control", "control": control, "x": x, "y": y}
1755
+
1756
+ result = await self._exec_with_snapshot(action)
1757
+
1758
+ # Add tab information to the result
1759
+ tab_info = await self._get_tab_info_for_output()
1760
+ result.update(tab_info)
1761
+
1762
+ return result
1763
+
1764
+ @action_logger
1765
+ async def browser_mouse_drag(
1766
+ self, *, from_ref: str, to_ref: str
1767
+ ) -> Dict[str, Any]:
1768
+ r"""Control the mouse to drag and drop in the browser using ref IDs.
1769
+
1770
+ Args:
1771
+ from_ref (str): The `ref` ID of the source element to drag from.
1772
+ to_ref (str): The `ref` ID of the target element to drag to.
1773
+
1774
+ Returns:
1775
+ Dict[str, Any]: A dictionary with the result of the action:
1776
+ - "result" (str): Confirmation of the action.
1777
+ - "snapshot" (str): A new page snapshot.
1778
+ - "tabs" (List[Dict]): Information about all open tabs.
1779
+ - "current_tab" (int): Index of the active tab.
1780
+ - "total_tabs" (int): Total number of open tabs.
1781
+ """
1782
+ # Validate refs
1783
+ self._validate_ref(from_ref, "drag source")
1784
+ self._validate_ref(to_ref, "drag target")
1785
+
1786
+ # Get element analysis to find coordinates
1787
+ analysis = await self._get_unified_analysis()
1788
+ elements = analysis.get("elements", {})
1789
+
1790
+ if from_ref not in elements:
1791
+ logger.error(
1792
+ f"Error: Source element reference '{from_ref}' not found."
1793
+ )
1794
+ snapshot = self._format_snapshot_from_analysis(analysis)
1795
+ tab_info = await self._get_tab_info_for_output()
1796
+ return {
1797
+ "result": (
1798
+ f"Error: Source element reference '{from_ref}' not found."
1799
+ ),
1800
+ "snapshot": snapshot,
1801
+ **tab_info,
1802
+ }
1803
+
1804
+ if to_ref not in elements:
1805
+ logger.error(
1806
+ f"Error: Target element reference '{to_ref}' not found."
1807
+ )
1808
+ snapshot = self._format_snapshot_from_analysis(analysis)
1809
+ tab_info = await self._get_tab_info_for_output()
1810
+ return {
1811
+ "result": (
1812
+ f"Error: Target element reference '{to_ref}' not found."
1813
+ ),
1814
+ "snapshot": snapshot,
1815
+ **tab_info,
1816
+ }
1817
+
1818
+ action = {
1819
+ "type": "mouse_drag",
1820
+ "from_ref": from_ref,
1821
+ "to_ref": to_ref,
1822
+ }
1823
+
1824
+ result = await self._exec_with_snapshot(action)
1825
+
1826
+ # Add tab information to the result
1827
+ tab_info = await self._get_tab_info_for_output()
1828
+ result.update(tab_info)
1829
+
1830
+ return result
1831
+
1832
+ @action_logger
1833
+ async def browser_press_key(self, *, keys: List[str]) -> Dict[str, Any]:
1834
+ r"""Press key and key combinations.
1835
+ Supports single key press or combination of keys by concatenating
1836
+ them with '+' separator.
1837
+
1838
+ Args:
1839
+ keys (List[str]): key or list of keys.
1840
+
1841
+ Returns:
1842
+ Dict[str, Any]: A dictionary with the result of the action:
1843
+ - "result" (str): Confirmation of the action.
1844
+ - "snapshot" (str): A new page snapshot.
1845
+ - "tabs" (List[Dict]): Information about all open tabs.
1846
+ - "current_tab" (int): Index of the active tab.
1847
+ - "total_tabs" (int): Total number of open tabs.
1848
+ """
1849
+ if not isinstance(keys, list) or not all(
1850
+ isinstance(item, str) for item in keys
1851
+ ):
1852
+ tab_info = await self._get_tab_info_for_output()
1853
+ return {
1854
+ "result": "Error: Expected keys as a list of strings.",
1855
+ "snapshot": "",
1856
+ **tab_info,
1857
+ }
1858
+ action = {"type": "press_key", "keys": keys}
1859
+
1860
+ result = await self._exec_with_snapshot(action)
1861
+
1862
+ # Add tab information to the result
1863
+ tab_info = await self._get_tab_info_for_output()
1864
+ result.update(tab_info)
1865
+
1866
+ return result
1867
+
1706
1868
  @action_logger
1707
1869
  async def browser_wait_user(
1708
1870
  self, timeout_sec: Optional[float] = None
@@ -1830,6 +1992,148 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
1830
1992
  await agent.process_command(task_prompt, max_steps=max_steps)
1831
1993
  return "Task processing finished - see stdout for detailed trace."
1832
1994
 
1995
+ @action_logger
1996
+ async def browser_console_view(self) -> Dict[str, Any]:
1997
+ r"""View current page console logs.
1998
+
1999
+ Returns:
2000
+ Dict[str, Any]: A dictionary with the result of the action:
2001
+ - console_messages (List[Dict]) : collection of logs from the
2002
+ browser console
2003
+ """
2004
+ try:
2005
+ logs = await self._session.get_console_logs()
2006
+ # make output JSON serializable
2007
+ return {"console_messages": list(logs)}
2008
+ except Exception as e:
2009
+ logger.warning(f"Failed to retrieve logs: {e}")
2010
+ return {"console_messages": []}
2011
+
2012
+ async def browser_console_exec(self, code: str) -> Dict[str, Any]:
2013
+ r"""Execute javascript code in the console of the current page and get
2014
+ results.
2015
+
2016
+ Args:
2017
+ code (str): JavaScript code for execution.
2018
+
2019
+ Returns:
2020
+ Dict[str, Any]: A dictionary with the result of the action:
2021
+ - "result" (str): Result of the action.
2022
+ - "console_output" (List[str]): Console log outputs during
2023
+ execution.
2024
+ - "snapshot" (str): A new page snapshot.
2025
+ - "tabs" (List[Dict]): Information about all open tabs.
2026
+ - "current_tab" (int): Index of the active tab.
2027
+ - "total_tabs" (int): Total number of open tabs.
2028
+ """
2029
+ page = await self._require_page()
2030
+
2031
+ try:
2032
+ logger.info("Executing JavaScript code in browser console.")
2033
+ exec_start = time.time()
2034
+
2035
+ # Wrap the code to capture console.log output and handle
2036
+ # expressions
2037
+ wrapped_code = (
2038
+ """
2039
+ (function() {
2040
+ const _logs = [];
2041
+ const originalLog = console.log;
2042
+ console.log = function(...args) {
2043
+ _logs.push(args.map(arg => {
2044
+ try {
2045
+ return typeof arg === 'object' ?
2046
+ JSON.stringify(arg) : String(arg);
2047
+ } catch (e) {
2048
+ return String(arg);
2049
+ }
2050
+ }).join(' '));
2051
+ originalLog.apply(console, args);
2052
+ };
2053
+
2054
+ let result;
2055
+ try {
2056
+ // First try to evaluate as an expression
2057
+ // (like browser console)
2058
+ result = eval("""
2059
+ + repr(code)
2060
+ + """);
2061
+ } catch (e) {
2062
+ // If that fails, execute as statements
2063
+ try {
2064
+ result = (function() { """
2065
+ + code
2066
+ + """ })();
2067
+ } catch (error) {
2068
+ console.log = originalLog;
2069
+ throw error;
2070
+ }
2071
+ }
2072
+
2073
+ console.log = originalLog;
2074
+ return { result, logs: _logs };
2075
+ })()
2076
+ """
2077
+ )
2078
+
2079
+ eval_result = await page.evaluate(wrapped_code)
2080
+ result = eval_result.get('result')
2081
+ console_logs = eval_result.get('logs', [])
2082
+
2083
+ exec_time = time.time() - exec_start
2084
+ logger.info(f"Code execution completed in {exec_time:.2f}s.")
2085
+
2086
+ import asyncio
2087
+ import json
2088
+
2089
+ await asyncio.sleep(0.2)
2090
+
2091
+ # Get snapshot
2092
+ logger.info("Capturing page snapshot after code execution.")
2093
+ snapshot_start = time.time()
2094
+ snapshot = await self._session.get_snapshot(
2095
+ force_refresh=True, diff_only=False
2096
+ )
2097
+ snapshot_time = time.time() - snapshot_start
2098
+ logger.info(
2099
+ f"Code execution snapshot captured in " f"{snapshot_time:.2f}s"
2100
+ )
2101
+
2102
+ # Get tab information
2103
+ tab_info = await self._get_tab_info_for_output()
2104
+
2105
+ # Properly serialize the result
2106
+ try:
2107
+ result_str = json.dumps(result, indent=2)
2108
+ except (TypeError, ValueError):
2109
+ result_str = str(result)
2110
+
2111
+ return {
2112
+ "result": f"Code execution result: {result_str}",
2113
+ "console_output": console_logs,
2114
+ "snapshot": snapshot,
2115
+ **tab_info,
2116
+ }
2117
+
2118
+ except Exception as e:
2119
+ logger.warning(f"Code execution failed: {e}")
2120
+ # Get tab information for error case
2121
+ try:
2122
+ tab_info = await self._get_tab_info_for_output()
2123
+ except Exception:
2124
+ tab_info = {
2125
+ "tabs": [],
2126
+ "current_tab": 0,
2127
+ "total_tabs": 0,
2128
+ }
2129
+
2130
+ return {
2131
+ "result": f"Code execution failed: {e}",
2132
+ "console_output": [],
2133
+ "snapshot": "",
2134
+ **tab_info,
2135
+ }
2136
+
1833
2137
  def get_log_summary(self) -> Dict[str, Any]:
1834
2138
  r"""Get a summary of logged actions."""
1835
2139
  if not self.log_buffer:
@@ -2045,11 +2349,16 @@ class HybridBrowserToolkit(BaseToolkit, RegisteredAgentToolkit):
2045
2349
  "browser_select": self.browser_select,
2046
2350
  "browser_scroll": self.browser_scroll,
2047
2351
  "browser_enter": self.browser_enter,
2352
+ "browser_mouse_control": self.browser_mouse_control,
2353
+ "browser_mouse_drag": self.browser_mouse_drag,
2354
+ "browser_press_key": self.browser_press_key,
2048
2355
  "browser_wait_user": self.browser_wait_user,
2049
2356
  "browser_solve_task": self.browser_solve_task,
2050
2357
  "browser_switch_tab": self.browser_switch_tab,
2051
2358
  "browser_close_tab": self.browser_close_tab,
2052
2359
  "browser_get_tab_info": self.browser_get_tab_info,
2360
+ "browser_console_view": self.browser_console_view,
2361
+ "browser_console_exec": self.browser_console_exec,
2053
2362
  }
2054
2363
 
2055
2364
  enabled_tools = []
@@ -43,7 +43,11 @@ class PageSnapshot:
43
43
  # Public API
44
44
  # ---------------------------------------------------------------------
45
45
  async def capture(
46
- self, *, force_refresh: bool = False, diff_only: bool = False
46
+ self,
47
+ *,
48
+ force_refresh: bool = False,
49
+ diff_only: bool = False,
50
+ viewport_limit: bool = False,
47
51
  ) -> str:
48
52
  """Return current snapshot or just the diff to previous one."""
49
53
  try:
@@ -65,7 +69,9 @@ class PageSnapshot:
65
69
  )
66
70
 
67
71
  logger.debug("Capturing page snapshot …")
68
- snapshot_result = await self._get_snapshot_direct()
72
+ snapshot_result = await self._get_snapshot_direct(
73
+ viewport_limit=viewport_limit
74
+ )
69
75
 
70
76
  # Extract snapshot text from the unified analyzer result
71
77
  if (
@@ -111,7 +117,7 @@ class PageSnapshot:
111
117
  _snapshot_js_cache: Optional[str] = None # class-level cache
112
118
 
113
119
  async def _get_snapshot_direct(
114
- self,
120
+ self, viewport_limit: bool = False
115
121
  ) -> Optional[Union[str, Dict[str, Any]]]:
116
122
  r"""Evaluate the snapshot-extraction JS with simple retry logic.
117
123
 
@@ -133,7 +139,7 @@ class PageSnapshot:
133
139
  retries: int = 3
134
140
  while retries > 0:
135
141
  try:
136
- return await self.page.evaluate(js_code)
142
+ return await self.page.evaluate(js_code, viewport_limit)
137
143
  except Exception as e:
138
144
  msg = str(e)
139
145
 
@@ -1,4 +1,4 @@
1
- (() => {
1
+ ((viewport_limit = false) => {
2
2
  // Unified analyzer that combines visual and structural analysis
3
3
  // Preserves complete snapshot.js logic while adding visual coordinate information
4
4
 
@@ -406,6 +406,11 @@
406
406
  if (tagName === 'header') return 'banner';
407
407
  if (tagName === 'footer') return 'contentinfo';
408
408
  if (tagName === 'fieldset') return 'group';
409
+
410
+ // Enhanced role mappings for table elements
411
+ if (tagName === 'table') return 'table';
412
+ if (tagName === 'tr') return 'row';
413
+ if (tagName === 'td' || tagName === 'th') return 'cell';
409
414
 
410
415
  return 'generic';
411
416
  }
@@ -484,6 +489,9 @@
484
489
 
485
490
  // Add a heuristic to ignore code-like text that might be in the DOM
486
491
  if ((text.match(/[;:{}]/g)?.length || 0) > 2) return '';
492
+
493
+
494
+
487
495
  return text;
488
496
  }
489
497
 
@@ -578,6 +586,8 @@
578
586
  const level = getAriaLevel(element);
579
587
  if (level > 0) node.level = level;
580
588
 
589
+
590
+
581
591
  return node;
582
592
  }
583
593
 
@@ -725,6 +735,9 @@
725
735
  if (isRedundantWrapper) {
726
736
  return node.children;
727
737
  }
738
+
739
+
740
+
728
741
  return [node];
729
742
  }
730
743
 
@@ -815,6 +828,23 @@
815
828
 
816
829
  // === Visual analysis functions from page_script.js ===
817
830
 
831
+ // Check if element is within the current viewport
832
+ function isInViewport(element) {
833
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
834
+
835
+ try {
836
+ const rect = element.getBoundingClientRect();
837
+ return (
838
+ rect.top >= 0 &&
839
+ rect.left >= 0 &&
840
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
841
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
842
+ );
843
+ } catch (e) {
844
+ return false;
845
+ }
846
+ }
847
+
818
848
  // From page_script.js - check if element is topmost at coordinates
819
849
  function isTopmost(element, x, y) {
820
850
  let hit = document.elementFromPoint(x, y);
@@ -855,10 +885,21 @@
855
885
 
856
886
  // === Unified analysis function ===
857
887
 
858
- function collectElementsFromTree(node, elementsMap) {
888
+ function collectElementsFromTree(node, elementsMap, viewportLimitEnabled = false) {
859
889
  if (typeof node === 'string') return;
860
890
 
861
891
  if (node.element && node.ref) {
892
+ // If viewport_limit is enabled, only include elements that are in the viewport
893
+ if (viewportLimitEnabled && !isInViewport(node.element)) {
894
+ // Skip this element but still process its children
895
+ if (node.children) {
896
+ for (const child of node.children) {
897
+ collectElementsFromTree(child, elementsMap, viewportLimitEnabled);
898
+ }
899
+ }
900
+ return;
901
+ }
902
+
862
903
  // Get visual coordinates for this element
863
904
  const coordinates = getElementCoordinates(node.element);
864
905
 
@@ -891,7 +932,7 @@
891
932
  // Recursively process children
892
933
  if (node.children) {
893
934
  for (const child of node.children) {
894
- collectElementsFromTree(child, elementsMap);
935
+ collectElementsFromTree(child, elementsMap, viewportLimitEnabled);
895
936
  }
896
937
  }
897
938
  }
@@ -931,7 +972,7 @@
931
972
  [tree] = normalizeTree(tree);
932
973
 
933
974
  const elementsMap = {};
934
- collectElementsFromTree(tree, elementsMap);
975
+ collectElementsFromTree(tree, elementsMap, viewport_limit);
935
976
 
936
977
  // Verify uniqueness of aria-ref attributes (debugging aid)
937
978
  const ariaRefCounts = {};