    Examples:
        new_tab()
    """
    global page
    # set the new page as the active page
    page = page.context.new_page()
    # trigger the callback that sets this page as active in browsergym
    page.evaluate(
        """\
const event = new Event('pageshow', {
    bubbles: true,  // Whether the event bubbles up through the DOM or not
    cancelable: false  // Whether the event can be canceled
});
window.dispatchEvent(event);
"""
    )

# https://playwright.dev/python/docs/api/class-page#page-close
def tab_close():
    """
    Close the current tab.

    Examples:
        tab_close()
    """
    global page
    context = page.context
    page.close()
    # set most recent page as active page, or open a new page if needed
    if context.pages:
        # TODO: do something more elaborate? (active page history)
        page = context.pages[-1]
    else:
        page = context.new_page()
    # trigger the callback that sets this page as active in browsergym
    page.evaluate(
        """\
const event = new Event('pageshow', {
    bubbles: true,  // Whether the event bubbles up through the DOM or not
    cancelable: false  // Whether the event can be canceled
});
window.dispatchEvent(event);
"""
    )

# https://playwright.dev/python/docs/api/class-page#page-bring-to-front
def tab_focus(index: int):
    """
    Bring tab to front (activate tab).

    Examples:
        tab_focus(2)
    """
    global page  # set the focused page as the active page
    page = page.context.pages[index]
    page.bring_to_front()
    # trigger the callback that sets this page as active in browsergym
    page.evaluate(
        """\
const event = new Event('pageshow', {
    bubbles: true,  // Whether the event bubbles up through the DOM or not
    cancelable: false  // Whether the event can be canceled
});


    page.close()
    # set most recent page as active page, or open a new page if needed
    if context.pages:
        # TODO: do something more elaborate? (active page history)
        page = context.pages[-1]
    else:
        page = context.new_page()
    # trigger the callback that sets this page as active in browsergym
    page.evaluate(
        """\
const event = new Event('pageshow', {
    bubbles: true,  // Whether the event bubbles up through the DOM or not
    cancelable: false  // Whether the event can be canceled
});
window.dispatchEvent(event);
"""
    )

# https://playwright.dev/python/docs/api/class-page#page-bring-to-front
def tab_focus(index: int):
    """
    Bring tab to front (activate tab).

    Examples:
        tab_focus(2)
    """
    global page  # set the focused page as the active page
    page = page.context.pages[index]
    page.bring_to_front()
    # trigger the callback that sets this page as active in browsergym
    page.evaluate(
        """\
const event = new Event('pageshow', {
    bubbles: true,  // Whether the event bubbles up through the DOM or not
    cancelable: false  // Whether the event can be canceled
});
window.dispatchEvent(event);
"""
    )

# https://playwright.dev/python/docs/input#upload-files
def upload_file(bid: str, file: str | list[str]):
    """
    Click an element and wait for a "filechooser" event, then select one
    or multiple input files for upload. Relative file paths are resolved
    relative to the current working directory. An empty list clears the
    selected files.

    Examples:
        upload_file("572", "my_receipt.pdf")
        upload_file("63", ["/home/bob/Documents/image.jpg", "/home/bob/Documents/file.zip"])
    """
    elem = get_elem_by_bid(page, bid, demo_mode != "off")
    add_demo_mode_effects(page, elem, bid, demo_mode=demo_mode, move_cursor=True)

    with page.expect_file_chooser() as fc_info:
        elem.click(timeout=500)


    Examples:
        tab_focus(2)
    """
    global page  # set the focused page as the active page
    page = page.context.pages[index]
    page.bring_to_front()
    # trigger the callback that sets this page as active in browsergym
    page.evaluate(
        """\
const event = new Event('pageshow', {
    bubbles: true,  // Whether the event bubbles up through the DOM or not
    cancelable: false  // Whether the event can be canceled
});
window.dispatchEvent(event);
"""
    )

# https://playwright.dev/python/docs/input#upload-files
def upload_file(bid: str, file: str | list[str]):
    """
    Click an element and wait for a "filechooser" event, then select one
    or multiple input files for upload. Relative file paths are resolved
    relative to the current working directory. An empty list clears the
    selected files.

    Examples:
        upload_file("572", "my_receipt.pdf")
        upload_file("63", ["/home/bob/Documents/image.jpg", "/home/bob/Documents/file.zip"])
    """
    elem = get_elem_by_bid(page, bid, demo_mode != "off")
    add_demo_mode_effects(page, elem, bid, demo_mode=demo_mode, move_cursor=True)

    with page.expect_file_chooser() as fc_info:
        elem.click(timeout=500)

    file_chooser = fc_info.value
    file_chooser.set_files(file)

# https://playwright.dev/python/docs/input#upload-files
def mouse_upload_file(x: float, y: float, file: str | list[str]):
    """
    Click a location and wait for a "filechooser" event, then select one
    or multiple input files for upload. Relative file paths are resolved
    relative to the current working directory. An empty list clears the
    selected files.

    Examples:
        mouse_upload_file(132.1, 547, "my_receipt.pdf")
        mouse_upload_file(328, 812, ["/home/bob/Documents/image.jpg", "/home/bob/Documents/file.zip"])
    """
    x, y = map_coordinates(page, x, y)  # map coordinates to page coordinates
    if demo_mode != "off":
        smooth_move_visual_cursor_to(page, x, y)
        highlight_by_box(page, {"x": x, "y": y, "width": 1, "height": 1})

    with page.expect_file_chooser() as fc_info:


    Click an element and wait for a "filechooser" event, then select one
    or multiple input files for upload. Relative file paths are resolved
    relative to the current working directory. An empty list clears the
    selected files.

    Examples:
        upload_file("572", "my_receipt.pdf")
        upload_file("63", ["/home/bob/Documents/image.jpg", "/home/bob/Documents/file.zip"])
    """
    elem = get_elem_by_bid(page, bid, demo_mode != "off")
    add_demo_mode_effects(page, elem, bid, demo_mode=demo_mode, move_cursor=True)

    with page.expect_file_chooser() as fc_info:
        elem.click(timeout=500)

    file_chooser = fc_info.value
    file_chooser.set_files(file)

# https://playwright.dev/python/docs/input#upload-files
def mouse_upload_file(x: float, y: float, file: str | list[str]):
    """
    Click a location and wait for a "filechooser" event, then select one
    or multiple input files for upload. Relative file paths are resolved
    relative to the current working directory. An empty list clears the
    selected files.

    Examples:
        mouse_upload_file(132.1, 547, "my_receipt.pdf")
        mouse_upload_file(328, 812, ["/home/bob/Documents/image.jpg", "/home/bob/Documents/file.zip"])
    """
    x, y = map_coordinates(page, x, y)  # map coordinates to page coordinates
    if demo_mode != "off":
        smooth_move_visual_cursor_to(page, x, y)
        highlight_by_box(page, {"x": x, "y": y, "width": 1, "height": 1})

    with page.expect_file_chooser() as fc_info:
        page.mouse.click(x, y)

    file_chooser = fc_info.value
    file_chooser.set_files(file)


do

    def do(force: bool):
        elem.click(button=button, modifiers=modifiers, force=force, timeout=500)

    call_fun(do, retry_with_force)

# https://playwright.dev/python/docs/api/class-locator#locator-hover
def hover(bid: str):
    """
    Hover over an element.

    Examples:
        hover('b8')
    """
    elem = get_elem_by_bid(page, bid, demo_mode != "off")
    add_demo_mode_effects(
        page, elem, bid, demo_mode=demo_mode, move_cursor=True, highlight_box=False
    )

    def do(force: bool):
        elem.hover(force=force, timeout=500)

    call_fun(do, retry_with_force)

# https://playwright.dev/python/docs/input#keys-and-shortcuts
def press(bid: str, key_comb: str):
    """
    Focus the matching element and press a combination of keys. It accepts
    the logical key names that are emitted in the keyboardEvent.key property
    of the keyboard events: Backquote, Minus, Equal, Backslash, Backspace,
    Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp,
    ArrowRight, ArrowUp, F1 - F12, Digit0 - Digit9, KeyA - KeyZ, etc. You can
    alternatively specify a single character you'd like to produce such as "a"
    or "#". Following modification shortcuts are also supported: Shift, Control,
    Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta resolves to Control on
    Windows and Linux and to Meta on macOS.

    Examples:
        press('88', 'Backspace')
        press('a26', 'ControlOrMeta+a')


import inspect
import random
import typing
from dataclasses import dataclass

from . import utils
from .base import AbstractActionSet
from .functions import (  # check,; uncheck,
    clear,
    click,
    dblclick,
    drag_and_drop,
    fill,
    focus,
    go_back,
    go_forward,
    goto,
    hover,
    keyboard_down,
    keyboard_insert_text,
    keyboard_press,
    keyboard_type,
    keyboard_up,
    mouse_click,
    mouse_dblclick,
    mouse_down,
    mouse_drag_and_drop,
    mouse_move,
    mouse_up,
    mouse_upload_file,
    new_tab,
    noop,
    press,
    report_infeasible,
    scroll,
    scroll_at,
    select_option,
    send_msg_to_user,
    tab_close,
    tab_focus,
    upload_file,
)
from .parsers import action_docstring_parser, highlevel_action_parser

ACTION_SUBSETS = {
    "chat": [send_msg_to_user],
    "infeas": [report_infeasible],
    "bid": [
        scroll,
        fill,
        # These are not really needed and might pollute the action space, doing more harm than good
        # check,
        # uncheck,
        select_option,
        click,
        dblclick,
        hover,
        press,
        focus,
        clear,
        drag_and_drop,
        upload_file,
    ],
    "coord": [
        scroll_at,
        mouse_move,
        mouse_up,
        mouse_down,
        mouse_click,
        mouse_dblclick,
        mouse_drag_and_drop,
        mouse_upload_file,
        keyboard_down,
        keyboard_up,
        keyboard_press,
        keyboard_type,
        keyboard_insert_text,
    ],
    "nav": [go_back, go_forward, goto],
    "tab": [
        tab_close,
        tab_focus,
        new_tab,
    ],
    # adapted from MiniWoB repo
    # https://github.com/Farama-Foundation/miniwob-plusplus/blob/1bab0dffe34e92cc1049fe9443542029bf7e44a9/miniwob/action.py#L122
    "miniwob_all": [
        mouse_move,  #     MOVE_COORDS
        mouse_click,  #    CLICK_COORDS
        mouse_dblclick,  # DBLCLICK_COORDS
        mouse_down,  #     MOUSEDOWN_COORDS
        mouse_up,  #       MOUSEUP_COORDS
        scroll,  #         SCROLL_UP_COORDS, SCROLL_DOWN_COORDS
        click,  #          CLICK_ELEMENT
        keyboard_press,  # PRESS_KEY
        keyboard_type,  #  TYPE_TEX (and substitute for TYPE_FIELD()
        fill,  #           FOCUS_ELEMENT_AND_TYPE_TEXT (and substitute for FOCUS_ELEMENT_AND_TYPE_FIELD)
    ],
    # adapted from MiniWoB repo
    # https://github.com/Farama-Foundation/miniwob-plusplus/blob/1bab0dffe34e92cc1049fe9443542029bf7e44a9/miniwob/action.py#L142
    "miniwob_shi17": [
        mouse_click,  #    CLICK_COORDS
        mouse_dblclick,  # DBLCLICK_COORDS
        mouse_down,  #     MOUSEDOWN_COORDS
        mouse_up,  #       MOUSEUP_COORDS
        scroll,  #         SCROLL_UP_COORDS, SCROLL_DOWN_COORDS
        keyboard_press,  # PRESS_KEY
    ],
    # adapted from MiniWoB repo
    # https://github.com/Farama-Foundation/miniwob-plusplus/blob/1bab0dffe34e92cc1049fe9443542029bf7e44a9/miniwob/action.py#L160
    "miniwob_liu18": [
        click,  # CLICK_ELEMENT
        fill,  #  substitute for FOCUS_ELEMENT_AND_TYPE_FIELD
    ],
    # adapted from MiniWoB repo
    # https://github.com/Farama-Foundation/miniwob-plusplus/blob/1bab0dffe34e92cc1049fe9443542029bf7e44a9/miniwob/action.py#L173
    "miniwob_humphreys22": [
        mouse_move,  #     MOVE_COORDS
        mouse_click,  #    CLICK_COORDS
        mouse_dblclick,  # DBLCLICK_COORDS
        mouse_down,  #     MOUSEDOWN_COORDS
        mouse_up,  #       MOUSEUP_COORDS
        scroll,  #         SCROLL_UP_COORDS, SCROLL_DOWN_COORDS
        keyboard_press,  # PRESS_KEY
        keyboard_type,  #  substitute for TYPE_FIELD
    ],
    # from the webarena paper
    # https://arxiv.org/abs/2307.13854
    # from the webarena source code
    # https://github.com/web-arena-x/webarena/blob/e31c190c9b43f63e5724322b847e00249300df40/browser_env/actions.py#L240
    # from the webarena default prompt
    # https://github.com/web-arena-x/webarena/blob/e31c190c9b43f63e5724322b847e00249300df40/agent/prompts/raw/p_cot_id_actree_2s.py#L13
    "webarena": [
        #                   #     code      |      paper       |      prompt
        scroll,  #            SCROLL        | scroll(dir)      | scroll [down|up]
        keyboard_press,  #    KEY_PRESS     | press(key_comb)  | press [key_comb]
        #                     MOUSE_CLICK   |                  |
        #                     KEYBOARD_TYPE |                  |
        #                     MOUSE_HOVER   |                  |
        click,  #             CLICK         | click(elem)      | click [id]
        fill,  #              TYPE          | type(elem, text) | type [id] [content]
        hover,  #             HOVER         | hover(elem)      | hover [id]
        tab_focus,  #         PAGE_FOCUS    | tab_focus(index) | tab_focus [tab_index]
        new_tab,  #           NEW_TAB       | new_tab()        | new_tab
        go_back,  #           GO_BACK       | go_back()        | go_back
        go_forward,  #        GO_FORWARD    | go_forward()     | go_forward
        goto,  #              GOTO_URL      | goto(url)        | goto [url]
        tab_close,  #         PAGE_CLOSE    | tab_close()      | close_tab
        #                     CHECK         |                  |
        select_option,  #     SELECT_OPTION |                  |
        send_msg_to_user,  #  STOP          | stop(answer)     | stop [answer]
        report_infeasible,  ## explicit unachievable action, equivalent STOP "N/A"
    ],
    # from the visualwebarena paper
    # https://arxiv.org/abs/2401.13649
    # from the visualwebarena source code
    # https://github.com/web-arena-x/visualwebarena/blob/15890922c97a8694e366fde2d7de8dbd1ff63fb5/browser_env/actions.py#L311-L343
    # from the visualwebarena default prompt
    # https://github.com/web-arena-x/visualwebarena/blob/15890922c97a8694e366fde2d7de8dbd1ff63fb5/agent/prompts/jsons/p_cot_id_actree_3s.json#L2
    "visualwebarena": [
        #                   #     code      |      paper       |      prompt
        scroll,  #            SCROLL        | scroll(dir)      | scroll [down|up]
        keyboard_press,  #    KEY_PRESS     | press(key_comb)  | press [key_comb]
        #                     MOUSE_CLICK   |                  |
        #                     KEYBOARD_TYPE |                  |
        #                     MOUSE_HOVER   |                  |
        click,  #             CLICK         | click(elem)      | click [id]
        fill,  #              TYPE          | type(elem, text) | type [id] [content]
        hover,  #             HOVER         | hover(elem)      | hover [id]
        tab_focus,  #         PAGE_FOCUS    | tab_focus(index) | tab_focus [tab_index]
        new_tab,  #           NEW_TAB       | new_tab()        | new_tab
        go_back,  #           GO_BACK       | go_back()        | go_back
        go_forward,  #        GO_FORWARD    | go_forward()     | go_forward
        goto,  #              GOTO_URL      | goto(url)        | goto [url]
        tab_close,  #         PAGE_CLOSE    | tab_close()      | close_tab
        #                     CHECK         |                  |
        select_option,  #     SELECT_OPTION |                  |
        send_msg_to_user,  #  STOP          | stop(answer)     | stop [answer]
        #                     CLEAR         |                  |
        upload_file,  #       UPLOAD        |                  |
        report_infeasible,  ## explicit unachievable action, equivalent STOP "N/A"
    ],
    # from workarena paper
    # https://arxiv.org/abs/2403.07718
    "workarena": [
        scroll,
        fill,
        select_option,
        click,
        dblclick,
        hover,
        press,
        focus,
        clear,
        drag_and_drop,
        send_msg_to_user,
    ],
    # from workarena++ paper
    # https://arxiv.org/abs/2407.05291
    "workarena++": [
        scroll,
        fill,
        select_option,
        click,
        dblclick,
        hover,
        press,
        focus,
        clear,
        drag_and_drop,
        tab_focus,
        new_tab,
        tab_close,
        go_back,
        go_forward,
        goto,
        send_msg_to_user,
        report_infeasible,
    ],
    # from weblinx_browsergym
    # https://github.com/McGill-NLP/agentlab-weblinx-mvp/blob/a91b6d19870c5187d252e70a2e2013511cc6f1d2/weblinx_browsergym/__init__.py#L274-L286
    "weblinx": [
        send_msg_to_user,  # say(speaker="assistant", utterance=[str]) -> send_msg_to_user(text=[str])
        click,  # click(uid=[element id]) -> click(bid=[element id])
        hover,  # hover(uid=[element id]) -> hover(bid=[element id])
        fill,  # textinput(uid=[element id], value=[str]) -> fill(bid=[element id], value=[str])
        # change(uid=[element], value=[str]) -> ❌
        goto,  # load(url=[link]) -> goto(url=[link])
        # submit(uid=[element]) -> click(bid=[element id])
        scroll,  # scroll(x=[int x],y=[int y]) -> scroll(delta_x=[int x], delta_y=[int y])
        # copy(uid=[element],text=[str]) -> ❌
        # paste(uid=[element],text=[str]) -> ❌
        new_tab,  # tabcreate() -> new_tab()
        tab_close,  # tabremove(target=[tabId]) -> tab_close()
        tab_focus,  # tabswitch(origin=[origin tabId],target=[target tabId]) -> tab_focus(index=[target tabid])
    ],
    # from assistantbench paper
    # https://arxiv.org/abs/2407.15711
    "assistantbench": [
        scroll,  # SCROLL
        fill,  # TYPE
        select_option,  # SELECT
        click,  # CLICK
        press,  # PRESS ENTER
        go_back,  # GOBACK
        goto,  # GOTO, SEARCH
        send_msg_to_user,  # TERMINATE
    ],
}

@dataclass
class HighLevelAction:
    # entrypoint: callable
    signature: str
    description: str
    examples: list[str]

class HighLevelActionSet(AbstractActionSet):

    # static class variables
    ActionSubset = typing.Literal[
        "chat",
        "infeas",
        "bid",
        "coord",
        "nav",
        "tab",
        "miniwob_all",
        "miniwob_shi17",
        "miniwob_liu18",
        "miniwob_humphreys22",
        "webarena",
        "visualwebarena",
        "workarena",
        "workarena++",
        "weblinx",
        "assistantbench",
        "custom",
    ]
    DemoMode = typing.Literal["off", "default", "all_blue", "only_visible_elements"]

    def __init__(
        self,
        subsets: typing.Optional[ActionSubset | list[ActionSubset]] = [
            "chat",
            "infeas",
            "bid",
            "nav",
            "tab",
        ],
        custom_actions: typing.Optional[list[callable]] = None,
        multiaction: bool = True,
        demo_mode: typing.Optional[DemoMode] = None,
        strict: bool = False,
        retry_with_force: bool = False,
    ):
        super().__init__(strict)
        self.multiaction = multiaction
        self.demo_mode = demo_mode
        self.retry_with_force = retry_with_force

        if not subsets:
            raise ValueError(f"'action_subsets' is empty.")

        if isinstance(subsets, str):
            subsets = [subsets]

        allowed_actions = [noop]  # the noop action is always allowed

        # add actions from specified action sets
        if subsets:
            for subset in subsets:
                if subset in ACTION_SUBSETS:
                    allowed_actions.extend(ACTION_SUBSETS[subset])
                elif subset == "custom":
                    if not custom_actions:
                        raise ValueError(
                            "'custom' is in 'action_subsets' but 'custom_actions' is empty."
                        )
                    allowed_actions.extend(custom_actions)
                else:
                    raise ValueError(f"Unknown high-level action subspace: {subset}")

        # like set() but preserves order
        # https://stackoverflow.com/questions/1653970/does-python-have-an-ordered-set
        allowed_actions = list(dict.fromkeys(allowed_actions).keys())

        # parse the actions and build the action space
        self.action_set: dict[str, HighLevelAction] = {}
        self.python_includes = ""

        # include playwright imports
        self.python_includes += f"""\
import playwright.sync_api
from typing import Literal

"""
        # set demo_mode and retry_with_force flags
        self.python_includes += f"""\
demo_mode={repr(demo_mode)}
retry_with_force={repr(retry_with_force)}

if demo_mode is None:
    demo_mode = "default" if DEMO_MODE else "off"

"""

        # include utility functions
        for _, func in inspect.getmembers(utils, inspect.isfunction):
            self.python_includes += f"""\
{inspect.getsource(func)}

"""

        # parse and include action functions
        for func in allowed_actions:

            # include action function definition in the code
            self.python_includes += f"""\
{inspect.getsource(func)}

"""

            # extract action signature
            signature = f"{func.__name__}{inspect.signature(func)}"

            # parse docstring
            description, examples = action_docstring_parser.parse_string(func.__doc__)

            # reconstruct action description
            description = " ".join(description)

            # reconstruct action examples
            examples = [
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")"
                for function_name, function_args in examples
            ]

            if func.__name__ in self.action_set:
                raise ValueError(f"Duplicated action '{func.__name__}'")

            self.action_set[func.__name__] = HighLevelAction(
                # entrypoint=func,
                signature=signature,
                description=description,
                examples=examples,
            )

    def example_action(self, abstract: bool, max_examples: int = 3) -> str:
        """
        Returns an example action as a string.
        """
        if abstract:
            if self.multiaction:
                return """\
One or several actions, separated by new lines."""
            else:
                return """\
One single action to be executed. You can only use one action at a time."""
        else:
            picked_examples = []

            # use fill and click examples if action is present
            for action_name in ["fill", "click", "mouse_click", "keyboard_type"]:
                if action_name in self.action_set:
                    picked_examples.extend(self.action_set[action_name].examples)

            # last resort, use all action examples
            if not picked_examples:
                for _, action in self.action_set.items():
                    picked_examples += action.examples

            # shuffle examples
            rng = random.Random(1)
            rng.shuffle(picked_examples)

            if self.multiaction:
                return "\n".join(picked_examples[:max_examples])
            else:
                return picked_examples[0]

    def describe(self, with_long_description: bool = True, with_examples: bool = True):
        """
        Returns a textual description of this action space.
        """
        description = f"""
{len(self.action_set)} different types of actions are available.

"""
        for _, action in self.action_set.items():
            description += f"""\
{action.signature}
"""

            if with_long_description:
                description += f"""\
    Description: {action.description}
"""
            if with_examples and action.examples:
                description += f"""\
    Examples:
"""
                for example in action.examples:
                    description += f"""\
        {example}

"""

        if self.multiaction:
            description += f"""\
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior."""
        else:
            description += f"""\
Only a single action can be provided at once."""

        example_action = self.example_action(abstract=False)
        if example_action:
            description += f""" Example:
{example_action}
"""
        else:
            description += f"""\

"""

        return description

    def to_python_code(self, action):
        """
        Converts the given high-level action string to browsergym-compatible python code.

        Args:
            action: the high-level action to parse.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """
        highlevel_code = action

        # do the actual parsing and convert each high-level action to
        # the corresponding python function call
        if self.strict:
            function_calls = highlevel_action_parser.parse_string(highlevel_code, parse_all=True)
            function_calls = function_calls.as_list()
        else:
            function_calls = highlevel_action_parser.search_string(
                highlevel_code
            )  # allow for multiple matches, skip anything in-between
            function_calls = sum(function_calls.as_list(), [])  # unpack multiple matches

        if not function_calls:
            raise ValueError("Received an empty action.")
        elif len(function_calls) > 1 and not self.multiaction:
            raise ValueError("Received a multi-action, only single-actions are allowed.")

        python_code = ""

        # function definitions
        python_code += self.python_includes

        # function calls
        for function_name, function_args in function_calls:
            if function_name not in self.action_set:
                raise NameError(f"Invalid action type '{function_name}'.")
            python_code += (
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")\n"
            )

        # return the constructed python code
        return python_code

    def to_tool_description(self, api="openai", add_examples=True) -> list[dict]:
        """
        Translates actions to tool descriptions following the OpenAI API format.

        Returns:
            A list of tool descriptors.
        """
        schema_keys = {
            "openai": "parameters",
            "anthropic": "input_schema",
        }
        schema = schema_keys.get(api, "parameters")
        tools = []
        for tool_name, action in self.action_set.items():
            # Parse the signature to extract parameter names and types
            parameters = {"type": "object", "properties": {}, "required": []}
            signature = inspect.signature(globals()[tool_name])
            for param_name, param in signature.parameters.items():
                param_type = "string"  # Default to string if type is not specified
                if param.annotation != inspect.Parameter.empty:

                    type_map = {
                        str: "string",
                        float: "number",
                        int: "number",
                        bool: "boolean",
                        dict: "object",
                        list: "array",
                    }
                    param_type = type_map.get(param.annotation, "string")

                parameters["properties"][param_name] = {
                    "type": param_type,
                    # "description": f"Parameter {param_name} of type {param_type}",
                }
                if param.default == inspect.Parameter.empty:
                    parameters["required"].append(param_name)
                else:
                    parameters["properties"][param_name]["default"] = param.default

            # Construct the tool descriptor
            description = action.description
            if add_examples and action.examples:
                description += "\n\nExamples:\n"
                for example in action.examples:
                    d
# ... truncated ...


HighLevelAction

        new_tab,  # tabcreate() -> new_tab()
        tab_close,  # tabremove(target=[tabId]) -> tab_close()
        tab_focus,  # tabswitch(origin=[origin tabId],target=[target tabId]) -> tab_focus(index=[target tabid])
    ],
    # from assistantbench paper
    # https://arxiv.org/abs/2407.15711
    "assistantbench": [
        scroll,  # SCROLL
        fill,  # TYPE
        select_option,  # SELECT
        click,  # CLICK
        press,  # PRESS ENTER
        go_back,  # GOBACK
        goto,  # GOTO, SEARCH
        send_msg_to_user,  # TERMINATE
    ],
}

@dataclass
class HighLevelAction:
    # entrypoint: callable
    signature: str
    description: str
    examples: list[str]

class HighLevelActionSet(AbstractActionSet):

    # static class variables
    ActionSubset = typing.Literal[
        "chat",
        "infeas",
        "bid",
        "coord",
        "nav",
        "tab",
        "miniwob_all",
        "miniwob_shi17",
        "miniwob_liu18",
        "miniwob_humphreys22",
        "webarena",
        "visualwebarena",
        "workarena",
        "workarena++",


HighLevelActionSet

        scroll,  # SCROLL
        fill,  # TYPE
        select_option,  # SELECT
        click,  # CLICK
        press,  # PRESS ENTER
        go_back,  # GOBACK
        goto,  # GOTO, SEARCH
        send_msg_to_user,  # TERMINATE
    ],
}

@dataclass
class HighLevelAction:
    # entrypoint: callable
    signature: str
    description: str
    examples: list[str]

class HighLevelActionSet(AbstractActionSet):

    # static class variables
    ActionSubset = typing.Literal[
        "chat",
        "infeas",
        "bid",
        "coord",
        "nav",
        "tab",
        "miniwob_all",
        "miniwob_shi17",
        "miniwob_liu18",
        "miniwob_humphreys22",
        "webarena",
        "visualwebarena",
        "workarena",
        "workarena++",
        "weblinx",
        "assistantbench",
        "custom",
    ]
    DemoMode = typing.Literal["off", "default", "all_blue", "only_visible_elements"]

    def __init__(
        self,
        subsets: typing.Optional[ActionSubset | list[ActionSubset]] = [
            "chat",
            "infeas",
            "bid",
            "nav",
            "tab",
        ],
        custom_actions: typing.Optional[list[callable]] = None,
        multiaction: bool = True,
        demo_mode: typing.Optional[DemoMode] = None,
        strict: bool = False,
        retry_with_force: bool = False,
    ):
        super().__init__(strict)
        self.multiaction = multiaction
        self.demo_mode = demo_mode
        self.retry_with_force = retry_with_force

        if not subsets:
            raise ValueError(f"'action_subsets' is empty.")

        if isinstance(subsets, str):
            subsets = [subsets]

        allowed_actions = [noop]  # the noop action is always allowed

        # add actions from specified action sets
        if subsets:
            for subset in subsets:
                if subset in ACTION_SUBSETS:
                    allowed_actions.extend(ACTION_SUBSETS[subset])
                elif subset == "custom":
                    if not custom_actions:
                        raise ValueError(
                            "'custom' is in 'action_subsets' but 'custom_actions' is empty."
                        )
                    allowed_actions.extend(custom_actions)
                else:
                    raise ValueError(f"Unknown high-level action subspace: {subset}")

        # like set() but preserves order
        # https://stackoverflow.com/questions/1653970/does-python-have-an-ordered-set
        allowed_actions = list(dict.fromkeys(allowed_actions).keys())

        # parse the actions and build the action space
        self.action_set: dict[str, HighLevelAction] = {}
        self.python_includes = ""

        # include playwright imports
        self.python_includes += f"""\
import playwright.sync_api
from typing import Literal

"""
        # set demo_mode and retry_with_force flags
        self.python_includes += f"""\
demo_mode={repr(demo_mode)}
retry_with_force={repr(retry_with_force)}

if demo_mode is None:
    demo_mode = "default" if DEMO_MODE else "off"

"""

        # include utility functions
        for _, func in inspect.getmembers(utils, inspect.isfunction):
            self.python_includes += f"""\
{inspect.getsource(func)}

"""

        # parse and include action functions
        for func in allowed_actions:

            # include action function definition in the code
            self.python_includes += f"""\
{inspect.getsource(func)}

"""

            # extract action signature
            signature = f"{func.__name__}{inspect.signature(func)}"

            # parse docstring
            description, examples = action_docstring_parser.parse_string(func.__doc__)

            # reconstruct action description
            description = " ".join(description)

            # reconstruct action examples
            examples = [
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")"
                for function_name, function_args in examples
            ]

            if func.__name__ in self.action_set:
                raise ValueError(f"Duplicated action '{func.__name__}'")

            self.action_set[func.__name__] = HighLevelAction(
                # entrypoint=func,
                signature=signature,
                description=description,
                examples=examples,
            )

    def example_action(self, abstract: bool, max_examples: int = 3) -> str:
        """
        Returns an example action as a string.
        """
        if abstract:
            if self.multiaction:
                return """\
One or several actions, separated by new lines."""
            else:
                return """\
One single action to be executed. You can only use one action at a time."""
        else:
            picked_examples = []

            # use fill and click examples if action is present
            for action_name in ["fill", "click", "mouse_click", "keyboard_type"]:
                if action_name in self.action_set:
                    picked_examples.extend(self.action_set[action_name].examples)

            # last resort, use all action examples
            if not picked_examples:
                for _, action in self.action_set.items():
                    picked_examples += action.examples

            # shuffle examples
            rng = random.Random(1)
            rng.shuffle(picked_examples)

            if self.multiaction:
                return "\n".join(picked_examples[:max_examples])
            else:
                return picked_examples[0]

    def describe(self, with_long_description: bool = True, with_examples: bool = True):
        """
        Returns a textual description of this action space.
        """
        description = f"""
{len(self.action_set)} different types of actions are available.

"""
        for _, action in self.action_set.items():
            description += f"""\
{action.signature}
"""

            if with_long_description:
                description += f"""\
    Description: {action.description}
"""
            if with_examples and action.examples:
                description += f"""\
    Examples:
"""
                for example in action.examples:
                    description += f"""\
        {example}

"""

        if self.multiaction:
            description += f"""\
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior."""
        else:
            description += f"""\
Only a single action can be provided at once."""

        example_action = self.example_action(abstract=False)
        if example_action:
            description += f""" Example:
{example_action}
"""
        else:
            description += f"""\

"""

        return description

    def to_python_code(self, action):
        """
        Converts the given high-level action string to browsergym-compatible python code.

        Args:
            action: the high-level action to parse.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """
        highlevel_code = action

        # do the actual parsing and convert each high-level action to
        # the corresponding python function call
        if self.strict:
            function_calls = highlevel_action_parser.parse_string(highlevel_code, parse_all=True)
            function_calls = function_calls.as_list()
        else:
            function_calls = highlevel_action_parser.search_string(
                highlevel_code
            )  # allow for multiple matches, skip anything in-between
            function_calls = sum(function_calls.as_list(), [])  # unpack multiple matches

        if not function_calls:
            raise ValueError("Received an empty action.")
        elif len(function_calls) > 1 and not self.multiaction:
            raise ValueError("Received a multi-action, only single-actions are allowed.")

        python_code = ""

        # function definitions
        python_code += self.python_includes

        # function calls
        for function_name, function_args in function_calls:
            if function_name not in self.action_set:
                raise NameError(f"Invalid action type '{function_name}'.")
            python_code += (
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")\n"
            )

        # return the constructed python code
        return python_code

    def to_tool_description(self, api="openai", add_examples=True) -> list[dict]:
        """
        Translates actions to tool descriptions following the OpenAI API format.

        Returns:
            A list of tool descriptors.
        """
        schema_keys = {
            "openai": "parameters",
            "anthropic": "input_schema",
        }
        schema = schema_keys.get(api, "parameters")
        tools = []
        for tool_name, action in self.action_set.items():
            # Parse the signature to extract parameter names and types
            parameters = {"type": "object", "properties": {}, "required": []}
            signature = inspect.signature(globals()[tool_name])
            for param_name, param in signature.parameters.items():
                param_type = "string"  # Default to string if type is not specified
                if param.annotation != inspect.Parameter.empty:

                    type_map = {
                        str: "string",
                        float: "number",
                        int: "number",
                        bool: "boolean",
                        dict: "object",
                        list: "array",
                    }
                    param_type = type_map.get(param.annotation, "string")

                parameters["properties"][param_name] = {
                    "type": param_type,
                    # "description": f"Parameter {param_name} of type {param_type}",
                }
                if param.default == inspect.Parameter.empty:
                    parameters["required"].append(param_name)
                else:
                    parameters["properties"][param_name]["default"] = param.default

            # Construct the tool descriptor
            description = action.description
            if add_examples and action.examples:
                description += "\n\nExamples:\n"
                for example in action.examples:
                    description += f"- {example}\n"

            tool = {
                "name": tool_name,
                "description": description,
                schema: parameters,
            }
            if api == "openai":
                tool["type"] = "function"
            tools.append(tool)

        return tools

# consistency checks
assert "custom" not in ACTION_SUBSETS
assert set(typing.get_args(HighLevelActionSet.ActionSubset)) == set(
    list(ACTION_SUBSETS.keys()) + ["custom"]
)


        "chat",
        "infeas",
        "bid",
        "coord",
        "nav",
        "tab",
        "miniwob_all",
        "miniwob_shi17",
        "miniwob_liu18",
        "miniwob_humphreys22",
        "webarena",
        "visualwebarena",
        "workarena",
        "workarena++",
        "weblinx",
        "assistantbench",
        "custom",
    ]
    DemoMode = typing.Literal["off", "default", "all_blue", "only_visible_elements"]

    def __init__(
        self,
        subsets: typing.Optional[ActionSubset | list[ActionSubset]] = [
            "chat",
            "infeas",
            "bid",
            "nav",
            "tab",
        ],
        custom_actions: typing.Optional[list[callable]] = None,
        multiaction: bool = True,
        demo_mode: typing.Optional[DemoMode] = None,
        strict: bool = False,
        retry_with_force: bool = False,
    ):
        super().__init__(strict)
        self.multiaction = multiaction
        self.demo_mode = demo_mode
        self.retry_with_force = retry_with_force

        if not subsets:
            raise ValueError(f"'action_subsets' is empty.")

        if isinstance(subsets, str):
            subsets = [subsets]

        allowed_actions = [noop]  # the noop action is always allowed

        # add actions from specified action sets
        if subsets:
            for subset in subsets:
                if subset in ACTION_SUBSETS:
                    allowed_actions.extend(ACTION_SUBSETS[subset])
                elif subset == "custom":
                    if not custom_actions:
                        raise ValueError(
                            "'custom' is in 'action_subsets' but 'custom_actions' is empty."
                        )
                    allowed_actions.extend(custom_actions)
                else:
                    raise ValueError(f"Unknown high-level action subspace: {subset}")

        # like set() but preserves order
        # https://stackoverflow.com/questions/1653970/does-python-have-an-ordered-set
        allowed_actions = list(dict.fromkeys(allowed_actions).keys())

        # parse the actions and build the action space
        self.action_set: dict[str, HighLevelAction] = {}
        self.python_includes = ""

        # include playwright imports
        self.python_includes += f"""\
import playwright.sync_api
from typing import Literal

"""
        # set demo_mode and retry_with_force flags
        self.python_includes += f"""\
demo_mode={repr(demo_mode)}
retry_with_force={repr(retry_with_force)}

if demo_mode is None:
    demo_mode = "default" if DEMO_MODE else "off"

"""

        # include utility functions
        for _, func in inspect.getmembers(utils, inspect.isfunction):
            self.python_includes += f"""\
{inspect.getsource(func)}

"""

        # parse and include action functions
        for func in allowed_actions:

            # include action function definition in the code
            self.python_includes += f"""\
{inspect.getsource(func)}

"""

            # extract action signature
            signature = f"{func.__name__}{inspect.signature(func)}"

            # parse docstring
            description, examples = action_docstring_parser.parse_string(func.__doc__)

            # reconstruct action description
            description = " ".join(description)

            # reconstruct action examples
            examples = [
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")"
                for function_name, function_args in examples
            ]

            if func.__name__ in self.action_set:
                raise ValueError(f"Duplicated action '{func.__name__}'")

            self.action_set[func.__name__] = HighLevelAction(
                # entrypoint=func,
                signature=signature,
                description=description,
                examples=examples,
            )

    def example_action(self, abstract: bool, max_examples: int = 3) -> str:
        """
        Returns an example action as a string.
        """
        if abstract:
            if self.multiaction:
                return """\
One or several actions, separated by new lines."""
            else:
                return """\
One single action to be executed. You can only use one action at a time."""
        else:
            picked_examples = []

            # use fill and click examples if action is present
            for action_name in ["fill", "click", "mouse_click", "keyboard_type"]:
                if action_name in self.action_set:
                    picked_examples.extend(self.action_set[action_name].examples)


            # reconstruct action description
            description = " ".join(description)

            # reconstruct action examples
            examples = [
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")"
                for function_name, function_args in examples
            ]

            if func.__name__ in self.action_set:
                raise ValueError(f"Duplicated action '{func.__name__}'")

            self.action_set[func.__name__] = HighLevelAction(
                # entrypoint=func,
                signature=signature,
                description=description,
                examples=examples,
            )

    def example_action(self, abstract: bool, max_examples: int = 3) -> str:
        """
        Returns an example action as a string.
        """
        if abstract:
            if self.multiaction:
                return """\
One or several actions, separated by new lines."""
            else:
                return """\
One single action to be executed. You can only use one action at a time."""
        else:
            picked_examples = []

            # use fill and click examples if action is present
            for action_name in ["fill", "click", "mouse_click", "keyboard_type"]:
                if action_name in self.action_set:
                    picked_examples.extend(self.action_set[action_name].examples)

            # last resort, use all action examples
            if not picked_examples:
                for _, action in self.action_set.items():
                    picked_examples += action.examples

            # shuffle examples
            rng = random.Random(1)
            rng.shuffle(picked_examples)

            if self.multiaction:
                return "\n".join(picked_examples[:max_examples])
            else:
                return picked_examples[0]

    def describe(self, with_long_description: bool = True, with_examples: bool = True):
        """
        Returns a textual description of this action space.
        """
        description = f"""
{len(self.action_set)} different types of actions are available.

"""
        for _, action in self.action_set.items():
            description += f"""\
{action.signature}
"""

            if with_long_description:
                description += f"""\
    Description: {action.description}
"""
            if with_examples and action.examples:
                description += f"""\


describe

            # use fill and click examples if action is present
            for action_name in ["fill", "click", "mouse_click", "keyboard_type"]:
                if action_name in self.action_set:
                    picked_examples.extend(self.action_set[action_name].examples)

            # last resort, use all action examples
            if not picked_examples:
                for _, action in self.action_set.items():
                    picked_examples += action.examples

            # shuffle examples
            rng = random.Random(1)
            rng.shuffle(picked_examples)

            if self.multiaction:
                return "\n".join(picked_examples[:max_examples])
            else:
                return picked_examples[0]

    def describe(self, with_long_description: bool = True, with_examples: bool = True):
        """
        Returns a textual description of this action space.
        """
        description = f"""
{len(self.action_set)} different types of actions are available.

"""
        for _, action in self.action_set.items():
            description += f"""\
{action.signature}
"""

            if with_long_description:
                description += f"""\
    Description: {action.description}
"""
            if with_examples and action.examples:
                description += f"""\
    Examples:
"""
                for example in action.examples:
                    description += f"""\
        {example}

"""

        if self.multiaction:
            description += f"""\
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior."""
        else:
            description += f"""\
Only a single action can be provided at once."""

        example_action = self.example_action(abstract=False)
        if example_action:
            description += f""" Example:
{example_action}
"""
        else:
            description += f"""\

"""

        return description

    def to_python_code(self, action):
        """
        Converts the given high-level action string to browsergym-compatible python code.

        Args:
            action: the high-level action to parse.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """
        highlevel_code = action

        # do the actual parsing and convert each high-level action to
        # the corresponding python function call
        if self.strict:
            function_calls = highlevel_action_parser.parse_string(highlevel_code, parse_all=True)
            function_calls = function_calls.as_list()
        else:
            function_calls = highlevel_action_parser.search_string(


        if self.multiaction:
            description += f"""\
Multiple actions can be provided at once, but will be executed sequentially without any feedback from the page.
More than 2-3 actions usually leads to failure or unexpected behavior."""
        else:
            description += f"""\
Only a single action can be provided at once."""

        example_action = self.example_action(abstract=False)
        if example_action:
            description += f""" Example:
{example_action}
"""
        else:
            description += f"""\

"""

        return description

    def to_python_code(self, action):
        """
        Converts the given high-level action string to browsergym-compatible python code.

        Args:
            action: the high-level action to parse.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """
        highlevel_code = action

        # do the actual parsing and convert each high-level action to
        # the corresponding python function call
        if self.strict:
            function_calls = highlevel_action_parser.parse_string(highlevel_code, parse_all=True)
            function_calls = function_calls.as_list()
        else:
            function_calls = highlevel_action_parser.search_string(
                highlevel_code
            )  # allow for multiple matches, skip anything in-between
            function_calls = sum(function_calls.as_list(), [])  # unpack multiple matches

        if not function_calls:
            raise ValueError("Received an empty action.")
        elif len(function_calls) > 1 and not self.multiaction:
            raise ValueError("Received a multi-action, only single-actions are allowed.")

        python_code = ""

        # function definitions
        python_code += self.python_includes

        # function calls
        for function_name, function_args in function_calls:
            if function_name not in self.action_set:
                raise NameError(f"Invalid action type '{function_name}'.")
            python_code += (
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")\n"
            )

        # return the constructed python code
        return python_code

    def to_tool_description(self, api="openai", add_examples=True) -> list[dict]:
        """
        Translates actions to tool descriptions following the OpenAI API format.

        Returns:
            A list of tool descriptors.
        """
        schema_keys = {
            "openai": "parameters",
            "anthropic": "input_schema",
        }
        schema = schema_keys.get(api, "parameters")
        tools = []
        for tool_name, action in self.action_set.items():
            # Parse the signature to extract parameter names and types
            parameters = {"type": "object", "properties": {}, "required": []}
            signature = inspect.signature(globals()[tool_name])
            for param_name, param in signature.parameters.items():
                param_type = "string"  # Default to string if type is not specified


            raise ValueError("Received an empty action.")
        elif len(function_calls) > 1 and not self.multiaction:
            raise ValueError("Received a multi-action, only single-actions are allowed.")

        python_code = ""

        # function definitions
        python_code += self.python_includes

        # function calls
        for function_name, function_args in function_calls:
            if function_name not in self.action_set:
                raise NameError(f"Invalid action type '{function_name}'.")
            python_code += (
                function_name + "(" + ", ".join([repr(arg) for arg in function_args]) + ")\n"
            )

        # return the constructed python code
        return python_code

    def to_tool_description(self, api="openai", add_examples=True) -> list[dict]:
        """
        Translates actions to tool descriptions following the OpenAI API format.

        Returns:
            A list of tool descriptors.
        """
        schema_keys = {
            "openai": "parameters",
            "anthropic": "input_schema",
        }
        schema = schema_keys.get(api, "parameters")
        tools = []
        for tool_name, action in self.action_set.items():
            # Parse the signature to extract parameter names and types
            parameters = {"type": "object", "properties": {}, "required": []}
            signature = inspect.signature(globals()[tool_name])
            for param_name, param in signature.parameters.items():
                param_type = "string"  # Default to string if type is not specified
                if param.annotation != inspect.Parameter.empty:

                    type_map = {
                        str: "string",
                        float: "number",
                        int: "number",
                        bool: "boolean",
                        dict: "object",
                        list: "array",
                    }
                    param_type = type_map.get(param.annotation, "string")

                parameters["properties"][param_name] = {
                    "type": param_type,
                    # "description": f"Parameter {param_name} of type {param_type}",
                }
                if param.default == inspect.Parameter.empty:
                    parameters["required"].append(param_name)
                else:
                    parameters["properties"][param_name]["default"] = param.default

            # Construct the tool descriptor
            description = action.description
            if add_examples and action.examples:
                description += "\n\nExamples:\n"
                for example in action.examples:
                    description += f"- {example}\n"

            tool = {
                "name": tool_name,
                "description": description,
                schema: parameters,
            }
            if api == "openai":
                tool["type"] = "function"
            tools.append(tool)

        return tools

# consistency checks
assert "custom" not in ACTION_SUBSETS
assert set(typing.get_args(HighLevelActionSet.ActionSubset)) == set(
    list(ACTION_SUBSETS.keys()) + ["custom"]
)


from abc import ABC, abstractmethod
from typing import Any

import playwright.sync_api

from . import get_global_demo_mode

class AbstractActionSet(ABC):
    def __init__(self, strict: bool = False):
        self.strict = strict

    @abstractmethod
    def describe(self, with_long_description: bool = True, with_examples: bool = True) -> str:
        """
        Returns a textual description of this action space.
        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:
        """
        Converts the action set to a tool descriptor.

        Returns:
            A list of dictionaries describing the actions in the action set.
        """
        pass

def execute_python_code(
    code: str,
    page: playwright.sync_api.Page,
    send_message_to_user: callable,
    report_infeasible_instructions: callable,
):
    """
    Executes Python code in a new context, except for a playwright `page` object and a `send_message_to_user` function.

    WARNING: this is not safe!
    https://stackoverflow.com/questions/77655440/can-you-protect-a-python-variable-with-exec

    Args:
        code: the Python code to execute, as a string.
        page: the playwright page that will be made accessible to the code.
        send_message_to_user: utility function that will be made accessible to the code. It should take one text argument.
        report_infeasible_instructions: utility function that will be made accessible to the code. It should take one text argument.
    """

    globals = {
        "page": page,
        "send_message_to_user": send_message_to_user,
        "report_infeasible_instructions": report_infeasible_instructions,
        "DEMO_MODE": get_global_demo_mode(),
    }

    exec(code, globals)


AbstractActionSet

from abc import ABC, abstractmethod
from typing import Any

import playwright.sync_api

from . import get_global_demo_mode

class AbstractActionSet(ABC):
    def __init__(self, strict: bool = False):
        self.strict = strict

    @abstractmethod
    def describe(self, with_long_description: bool = True, with_examples: bool = True) -> str:
        """
        Returns a textual description of this action space.
        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:
        """
        Converts the action set to a tool descriptor.

        Returns:
            A list of dictionaries describing the actions in the action set.
        """
        pass

def execute_python_code(
    code: str,
    page: playwright.sync_api.Page,
    send_message_to_user: callable,
    report_infeasible_instructions: callable,
):
    """
    Executes Python code in a new context, except for a playwright `page` object and a `send_message_to_user` function.

    WARNING: this is not safe!
    https://stackoverflow.com/questions/77655440/can-you-protect-a-python-variable-with-exec

    Args:
        code: the Python code to execute, as a string.
        page: the playwright page that will be made accessible to the code.
        send_message_to_user: utility function that will be made accessible to the code. It should take one text argument.
        report_infeasible_instructions: utility function that will be made accessible to the code. It should take one text argument.
    """


        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:
        """
        Converts the action set to a tool descriptor.

        Returns:
            A list of dictionaries describing the actions in the action set.
        """
        pass

def execute_python_code(
    code: str,
    page: playwright.sync_api.Page,
    send_message_to_user: callable,
    report_infeasible_instructions: callable,
):
    """
    Executes Python code in a new context, except for a playwright `page` object and a `send_message_to_user` function.

    WARNING: this is not safe!
    https://stackoverflow.com/questions/77655440/can-you-protect-a-python-variable-with-exec

    Args:
        code: the Python code to execute, as a string.
        page: the playwright page that will be made accessible to the code.
        send_message_to_user: utility function that will be made accessible to the code. It should take one text argument.
        report_infeasible_instructions: utility function that will be made accessible to the code. It should take one text argument.
    """

    globals = {
        "page": page,
        "send_message_to_user": send_message_to_user,
        "report_infeasible_instructions": report_infeasible_instructions,
        "DEMO_MODE": get_global_demo_mode(),
    }

    exec(code, globals)


from abc import ABC, abstractmethod
from typing import Any

import playwright.sync_api

from . import get_global_demo_mode

class AbstractActionSet(ABC):
    def __init__(self, strict: bool = False):
        self.strict = strict

    @abstractmethod
    def describe(self, with_long_description: bool = True, with_examples: bool = True) -> str:
        """
        Returns a textual description of this action space.
        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.


describe

from abc import ABC, abstractmethod
from typing import Any

import playwright.sync_api

from . import get_global_demo_mode

class AbstractActionSet(ABC):
    def __init__(self, strict: bool = False):
        self.strict = strict

    @abstractmethod
    def describe(self, with_long_description: bool = True, with_examples: bool = True) -> str:
        """
        Returns a textual description of this action space.
        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:


from abc import ABC, abstractmethod
from typing import Any

import playwright.sync_api

from . import get_global_demo_mode

class AbstractActionSet(ABC):
    def __init__(self, strict: bool = False):
        self.strict = strict

    @abstractmethod
    def describe(self, with_long_description: bool = True, with_examples: bool = True) -> str:
        """
        Returns a textual description of this action space.
        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:
        """
        Converts the action set to a tool descriptor.

        Returns:
            A list of dictionaries describing the actions in the action set.
        """


from . import get_global_demo_mode

class AbstractActionSet(ABC):
    def __init__(self, strict: bool = False):
        self.strict = strict

    @abstractmethod
    def describe(self, with_long_description: bool = True, with_examples: bool = True) -> str:
        """
        Returns a textual description of this action space.
        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:
        """
        Converts the action set to a tool descriptor.

        Returns:
            A list of dictionaries describing the actions in the action set.
        """
        pass

def execute_python_code(
    code: str,
    page: playwright.sync_api.Page,
    send_message_to_user: callable,
    report_infeasible_instructions: callable,
):
    """
    Executes Python code in a new context, except for a playwright `page` object and a `send_message_to_user` function.


        """

    @abstractmethod
    def example_action(self, abstract: bool) -> str:
        """
        Returns an example action as a string.
        """

    @abstractmethod
    def to_python_code(self, action) -> str:
        """
        Converts the given action to browsergym-compatible python code.

        Args:
            action: the action to convert.

        Returns:
            Executable python code that performs the action in a browsergym environment.
        """

    def to_tool_descriptor(self) -> list[Any]:
        """
        Converts the action set to a tool descriptor.

        Returns:
            A list of dictionaries describing the actions in the action set.
        """
        pass

def execute_python_code(
    code: str,
    page: playwright.sync_api.Page,
    send_message_to_user: callable,
    report_infeasible_instructions: callable,
):
    """
    Executes Python code in a new context, except for a playwright `page` object and a `send_message_to_user` function.

    WARNING: this is not safe!
    https://stackoverflow.com/questions/77655440/can-you-protect-a-python-variable-with-exec

    Args:
        code: the Python code to execute, as a string.
        page: the playwright page that will be made accessible to the code.
        send_message_to_user: utility function that will be made accessible to the code. It should take one text argument.
        report_infeasible_instructions: utility function that will be made accessible to the code. It should take one text argument.
    """


from typing import Literal

import playwright.sync_api

def get_elem_by_bid(
    page: playwright.sync_api.Page, bid: str, scroll_into_view: bool = False
) -> playwright.sync_api.Locator:
    """
    Parse the given bid to sequentially locate every nested frame leading to the bid, then
    locate the bid element. Bids are expected to take the form "abDb123", which means
    the element abDb123 is located inside frame abDAb, which is located inside frame abDA,
    which is located inside frame a, which is located inside the page's main frame.

    Args:
        bid: the browsergym id (playwright testid) of the page element.
        scroll_into_view: try to scroll element into view, unless it is completely visible.

    Returns:
        Playwright element.
        Bounding box of the element.
    """
    if not isinstance(bid, str):
        raise ValueError(f"expected a string, got {repr(bid)}")

    current_frame = page

    # dive into each nested frame, to the frame where the element is located
    i = 0
    while bid[i:] and not bid[i:].isnumeric():
        i += 1
        # allow multi-character frame ids such as aA, bCD etc.
        while bid[i:] and bid[i].isalpha() and bid[i].isupper():
            i += 1
        frame_bid = bid[:i]  # bid of the next frame to select
        frame_elem = current_frame.get_by_test_id(frame_bid)
        if not frame_elem.count():
            raise ValueError(f'Could not find element with bid "{bid}"')
        if scroll_into_view:
            frame_elem.scroll_into_view_if_needed(timeout=500)
        current_frame = frame_elem.frame_locator(":scope")

    # finally, we should have selected the frame where the target element is
    elem = current_frame.get_by_test_id(bid)
    if not elem.count():
        raise ValueError(f'Could not find element with bid "{bid}"')
    if scroll_into_view:
        elem.scroll_into_view_if_needed(timeout=500)
    return elem

def highlight_by_box(
    page: playwright.sync_api.Page, box: dict, color: Literal["blue", "red"] = "blue"
):
    """Highlights the target element based on its bounding box attributes."""

    assert color in ("blue", "red")

    if box:
        left, top, width, height = box["x"], box["y"], box["width"], box["height"]
        page.evaluate(
            f"""\
const overlay = document.createElement('div');
document.body.appendChild(overlay);
overlay.setAttribute('style', `
    all: initial;
    position: fixed;
    border: 2px solid transparent;  /* Start with transparent border */
    borderRadius: 10px;  /* Add rounded corners */
    boxShadow: 0 0 0px {color};  /* Initial boxShadow with 0px spread */
    left: {left - 2}px;  /* Adjust left position to accommodate initial shadow spread */
    top: {top - 2}px;  /* Adjust top position likewise */
    width: {width}px;
    height: {height}px;
    z-index: 2147483646; /* Maximum value - 1 */
    pointerEvents: none; /* Ensure the overlay does not interfere with user interaction */
`);

// Animate the boxShadow to create a "wave" effect
let spread = 0;  // Initial spread radius of the boxShadow
const waveInterval = setInterval(() => {{
    spread += 10;  // Increase the spread radius to simulate the wave moving outward
    overlay.style.boxShadow = `0 0 40px ${{spread}}px {color}`;  // Update boxShadow to new spread radius
    overlay.style.opacity = 1 - spread / 38;  // Gradually decrease opacity to fade out the wave
    if (spread >= 38) {{  // Assuming 76px ~ 2cm spread radius
        clearInterval(waveInterval);  // Stop the animation once the spread radius reaches 2cm
        document.body.removeChild(overlay);  // Remove the overlay from the document
    }}
}}, 200);  // Adjust the interval as needed to control the speed of the wave animation
"""
        )
        # Wait a bit to let users see the highlight
        page.wait_for_timeout(1000)  # Adjust delay as needed

def smooth_move_visual_cursor_to(
    page: playwright.sync_api.Page, x: float, y: float, speed: float = 400
):
    """
    Smoothly moves the visual cursor to a specific point, with constant
    movement speed.

    Args:
        x: target location X coordinate (in viewport pixels)
        y: target location Y coordinate (in viewport pixels)
        speed: cursor speed (in pixels per second)
    """
    movement_time = page.evaluate(
        """\
    ([targetX, targetY, speed]) => {

        // create cursor if needed
        if (!("browsergym_visual_cursor" in window)) {
            if (window.trustedTypes && window.trustedTypes.createPolicy) {
                window.trustedTypes.createPolicy('default', {
                    createHTML: (string, sink) => string
                });
            }
            let cursor = document.createElement('div');
            cursor.setAttribute('id', 'browsergym-visual-cursor');
            cursor.innerHTML = `
                <svg width="50px" height="50px" viewBox="213 106 713 706" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M213.333 106.667L426.667 853.333 512 512 853.333 426.667 213.333 106.667z" fill="blue"/>
                </svg>
`;
            cursor.setAttribute('style', `
                all: initial;
                position: fixed;
                opacity: 0.7; /* Slightly transparent */
                z-index: 2147483647; /* Maximum value */
                pointer-events: none; /* Ensures the SVG doesn't interfere with page interactions */
            `);

            // Calculate center position within the viewport
            const centerX = window.innerWidth / 2;
            const centerY = window.innerHeight / 2;

            cursor.style.left = `${centerX}px`;
            cursor.style.top = `${centerY}px`;

            // save cursor element
            window.browsergym_visual_cursor = cursor;
            window.browsergym_visual_cursor_n_owners = 0;
        }

        // recover cursor
        let cursor = window.browsergym_visual_cursor;

        // attach cursor to document
        document.body.appendChild(cursor);
        window.browsergym_visual_cursor_n_owners += 1;

        x = parseFloat(cursor.style.left);
        y = parseFloat(cursor.style.top);

        dx = targetX - x;
        dy = targetY - y;
        dist = Math.hypot(dx, dy);
        movement_time = (dist / speed) * 1000;  // seconds to milliseconds
        still_wait_time = 1000;

        // Adjust steps based on distance to keep movement speed consistent
        // 1 step per 10 pixels of distance, adjust as needed
        steps = Math.max(1, Math.trunc(dist / 10));

        step_dx = dx / steps;
        step_dy = dy / steps;
        step_dist = dist / steps;
        step_wait_time = Math.max(10, movement_time / steps);

        let step = 0;
        let time_still = 0;
        const cursorInterval = setInterval(() => {
            // move cursor
            if (step < steps) {
                x += step_dx;
                y += step_dy;
                cursor.style.left = `${x}px`;
                cursor.style.top = `${y}px`;
            }
            // still cursor (wait a bit)
            else if (time_still < still_wait_time) {
                time_still += step_wait_time;
            }
            // stop and detach cursor
            else {
                clearInterval(cursorInterval);
                window.browsergym_visual_cursor_n_owners -= 1;
                if (window.browsergym_visual_cursor_n_owners <= 0) {
                    document.body.removeChild(cursor);

                }
            }
            step += 1;
        }, step_wait_time);

        return movement_time;
    }""",
        [x, y, speed],
    )
    page.wait_for_timeout(movement_time)

def check_for_overlay(
    page: playwright.sync_api.Page, bid: str, element: playwright.sync_api.ElementHandle, box: dict
):
    if not element:
        return False

    visibility = element.get_attribute("browsergym_visibility_ratio")
    if visibility is not None:
        return float(visibility) >= 0.5

    """Checks if a given element is the topmost element at its center position by default.
    If check_corners is True, it checks if any of the corners is visible."""
    if box:
        # corners
        points_to_check = [
            (box["x"], box["y"]),
            (box["x"] + box["width"], box["y"]),
            (box["x"], box["y"] + box["height"]),
            (box["x"] + box["width"], box["y"] + box["height"]),
        ]

        for x, y in points_to_check:
            # Execute JavaScript to find the topmost element at the point.
            top_element = page.evaluate(
                f"""() => {{
                const el = document.elementFromPoint({x}, {y});
                return el ? el.outerHTML : '';
            }}"""
            )

            # Check if the topmost element is the element we're interested in.
            if top_element and bid in top_element:
                return True

    return False

def add_demo_mode_effects(
    page: playwright.sync_api.Page,
    elem: playwright.sync_api.ElementHandle,
    bid: str,
    demo_mode: Literal["off", "default", "all_blue", "only_visible_elements"],
    move_cursor: bool = True,
    highlight_box: bool = True,
):
    if demo_mode == "off":
        return

    """Adds visual effects to the target element"""
    box = elem.bounding_box()
    # box = extract_bounds_cdp(page, bid)
    if box:
        center_x, center_y = box["x"] + box["width"] / 2, box["y"] + box["height"] / 2
        is_top_element = check_for_overlay(page, bid, elem, box)

        if demo_mode == "only_visible_elements":
            if not is_top_element:
                return
            else:
                color = "blue"

        elif demo_mode == "default":
            if is_top_element:
                color = "blue"
            else:
                color = "red"

        elif demo_mode == "all_blue":
            color = "blue"

        if move_cursor:
            smooth_move_visual_cursor_to(page, center_x, center_y)

        if highlight_box:
            highlight_by_box(page, box, color=color)

def call_fun(fun: callable, retry_with_force: bool):
    try:
        fun(force=False)
    except playwright.sync_api.TimeoutError as e:
        if retry_with_force:
            fun(force=True)
        else:
            raise e

def map_coordinates(page, x, y):
    """
    Maps screenshot coordinates back to page coordinates based on bgym_scale_factor.

    When bgym_scale_factor > 1.0, the screenshot is captured at higher resolution,
    so VLM coordinates need to be scaled down to match the actual page coordinates.

    Args:
        page: Playwright page object (should have _bgym_scale_factor attribute)
        x: X coordinate from VLM (based on scaled screenshot)
        y: Y coordinate from VLM (based on scaled screenshot)

    Returns:
        tuple: (mapped_x, mapped_y) coordinates for use with Playwright

    Examples:
        # If bgym_scale_factor = 1.5 and VLM returns coordinates (300, 450)
        # from the scaled screenshot, map them back to page coordinates:
        page_x, page_y = map_coordinates(page, 300, 450)  # Returns (200.0, 300.0)
    """
    scale_factor = getattr(page, "_bgym_scale_factor", 1.0)

    # Scale down coordinates to match actual page coordinate system
    mapped_x = x / scale_factor
    mapped_y = y / scale_factor

    return mapped_x, mapped_y


from typing import Literal

import playwright.sync_api

def get_elem_by_bid(
    page: playwright.sync_api.Page, bid: str, scroll_into_view: bool = False
) -> playwright.sync_api.Locator:
    """
    Parse the given bid to sequentially locate every nested frame leading to the bid, then
    locate the bid element. Bids are expected to take the form "abDb123", which means
    the element abDb123 is located inside frame abDAb, which is located inside frame abDA,
    which is located inside frame a, which is located inside the page's main frame.

    Args:
        bid: the browsergym id (playwright testid) of the page element.
        scroll_into_view: try to scroll element into view, unless it is completely visible.

    Returns:
        Playwright element.
        Bounding box of the element.
    """
    if not isinstance(bid, str):
        raise ValueError(f"expected a string, got {repr(bid)}")

    current_frame = page

    # dive into each nested frame, to the frame where the element is located
    i = 0
    while bid[i:] and not bid[i:].isnumeric():
        i += 1
        # allow multi-character frame ids such as aA, bCD etc.
        while bid[i:] and bid[i].isalpha() and bid[i].isupper():
            i += 1
        frame_bid = bid[:i]  # bid of the next frame to select
        frame_elem = current_frame.get_by_test_id(frame_bid)
        if not frame_elem.count():
            raise ValueError(f'Could not find element with bid "{bid}"')
        if scroll_into_view:
            frame_elem.scroll_into_view_if_needed(timeout=500)
        current_frame = frame_elem.frame_locator(":scope")

    # finally, we should have selected the frame where the target element is
    elem = current_frame.get_by_test_id(bid)
    if not elem.count():
        raise ValueError(f'Could not find element with bid "{bid}"')
    if scroll_into_view:
        elem.scroll_into_view_if_needed(timeout=500)
    return elem

def highlight_by_box(
    page: playwright.sync_api.Page, box: dict, color: Literal["blue", "red"] = "blue"
):
    """Highlights the target element based on its bounding box attributes."""

    assert color in ("blue", "red")

    if box:
        left, top, width, height = box["x"], box["y"], box["width"], box["height"]
        page.evaluate(
            f"""\
const overlay = document.createElement('div');
document.body.appendChild(overlay);
overlay.setAttribute('style', `
    all: initial;
    position: fixed;
    border: 2px solid transparent;  /* Start with transparent border */
    borderRadius: 10px;  /* Add rounded corners */


        # allow multi-character frame ids such as aA, bCD etc.
        while bid[i:] and bid[i].isalpha() and bid[i].isupper():
            i += 1
        frame_bid = bid[:i]  # bid of the next frame to select
        frame_elem = current_frame.get_by_test_id(frame_bid)
        if not frame_elem.count():
            raise ValueError(f'Could not find element with bid "{bid}"')
        if scroll_into_view:
            frame_elem.scroll_into_view_if_needed(timeout=500)
        current_frame = frame_elem.frame_locator(":scope")

    # finally, we should have selected the frame where the target element is
    elem = current_frame.get_by_test_id(bid)
    if not elem.count():
        raise ValueError(f'Could not find element with bid "{bid}"')
    if scroll_into_view:
        elem.scroll_into_view_if_needed(timeout=500)
    return elem

def highlight_by_box(
    page: playwright.sync_api.Page, box: dict, color: Literal["blue", "red"] = "blue"
):
    """Highlights the target element based on its bounding box attributes."""

    assert color in ("blue", "red")

    if box:
        left, top, width, height = box["x"], box["y"], box["width"], box["height"]
        page.evaluate(
            f"""\
const overlay = document.createElement('div');
document.body.appendChild(overlay);
overlay.setAttribute('style', `
    all: initial;
    position: fixed;
    border: 2px solid transparent;  /* Start with transparent border */
    borderRadius: 10px;  /* Add rounded corners */
    boxShadow: 0 0 0px {color};  /* Initial boxShadow with 0px spread */
    left: {left - 2}px;  /* Adjust left position to accommodate initial shadow spread */
    top: {top - 2}px;  /* Adjust top position likewise */
    width: {width}px;
    height: {height}px;
    z-index: 2147483646; /* Maximum value - 1 */
    pointerEvents: none; /* Ensure the overlay does not interfere with user interaction */
`);

// Animate the boxShadow to create a "wave" effect
let spread = 0;  // Initial spread radius of the boxShadow
const waveInterval = setInterval(() => {{
    spread += 10;  // Increase the spread radius to simulate the wave moving outward
    overlay.style.boxShadow = `0 0 40px ${{spread}}px {color}`;  // Update boxShadow to new spread radius
    overlay.style.opacity = 1 - spread / 38;  // Gradually decrease opacity to fade out the wave
    if (spread >= 38) {{  // Assuming 76px ~ 2cm spread radius
        clearInterval(waveInterval);  // Stop the animation once the spread radius reaches 2cm
        document.body.removeChild(overlay);  // Remove the overlay from the document
    }}
}}, 200);  // Adjust the interval as needed to control the speed of the wave animation
"""
        )
        # Wait a bit to let users see the highlight
        page.wait_for_timeout(1000)  # Adjust delay as needed

def smooth_move_visual_cursor_to(
    page: playwright.sync_api.Page, x: float, y: float, speed: float = 400
):
    """
    Smoothly moves the visual cursor to a specific point, with constant
    movement speed.

    Args:
        x: target location X coordinate (in viewport pixels)
        y: target location Y coordinate (in viewport pixels)
        speed: cursor speed (in pixels per second)
    """
    movement_time = page.evaluate(
        """\
    ([targetX, targetY, speed]) => {

        // create cursor if needed
        if (!("browsergym_visual_cursor" in window)) {


    pointerEvents: none; /* Ensure the overlay does not interfere with user interaction */
`);

// Animate the boxShadow to create a "wave" effect
let spread = 0;  // Initial spread radius of the boxShadow
const waveInterval = setInterval(() => {{
    spread += 10;  // Increase the spread radius to simulate the wave moving outward
    overlay.style.boxShadow = `0 0 40px ${{spread}}px {color}`;  // Update boxShadow to new spread radius
    overlay.style.opacity = 1 - spread / 38;  // Gradually decrease opacity to fade out the wave
    if (spread >= 38) {{  // Assuming 76px ~ 2cm spread radius
        clearInterval(waveInterval);  // Stop the animation once the spread radius reaches 2cm
        document.body.removeChild(overlay);  // Remove the overlay from the document
    }}
}}, 200);  // Adjust the interval as needed to control the speed of the wave animation
"""
        )
        # Wait a bit to let users see the highlight
        page.wait_for_timeout(1000)  # Adjust delay as needed

def smooth_move_visual_cursor_to(
    page: playwright.sync_api.Page, x: float, y: float, speed: float = 400
):
    """
    Smoothly moves the visual cursor to a specific point, with constant
    movement speed.

    Args:
        x: target location X coordinate (in viewport pixels)
        y: target location Y coordinate (in viewport pixels)
        speed: cursor speed (in pixels per second)
    """
    movement_time = page.evaluate(
        """\
    ([targetX, targetY, speed]) => {

        // create cursor if needed
        if (!("browsergym_visual_cursor" in window)) {
            if (window.trustedTypes && window.trustedTypes.createPolicy) {
                window.trustedTypes.createPolicy('default', {
                    createHTML: (string, sink) => string
                });
            }
            let cursor = document.createElement('div');
            cursor.setAttribute('id', 'browsergym-visual-cursor');
            cursor.innerHTML = `
                <svg width="50px" height="50px" viewBox="213 106 713 706" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M213.333 106.667L426.667 853.333 512 512 853.333 426.667 213.333 106.667z" fill="blue"/>
                </svg>
`;
            cursor.setAttribute('style', `
                all: initial;
                position: fixed;
                opacity: 0.7; /* Slightly transparent */
                z-index: 2147483647; /* Maximum value */
                pointer-events: none; /* Ensures the SVG doesn't interfere with page interactions */
            `);

            // Calculate center position within the viewport
            const centerX = window.innerWidth / 2;
            const centerY = window.innerHeight / 2;

            cursor.style.left = `${centerX}px`;
            cursor.style.top = `${centerY}px`;

            // save cursor element
            window.browsergym_visual_cursor = cursor;
            window.browsergym_visual_cursor_n_owners = 0;
        }

        // recover cursor
        let cursor = window.browsergym_visual_cursor;

        // attach cursor to document
        document.body.appendChild(cursor);
        window.browsergym_visual_cursor_n_owners += 1;

        x = parseFloat(cursor.style.left);
        y = parseFloat(cursor.style.top);

        dx = targetX - x;
        dy = targetY - y;
        dist = Math.hypot(dx, dy);
        movement_time = (dist / speed) * 1000;  // seconds to milliseconds
        still_wait_time = 1000;

        // Adjust steps based on distance to keep movement speed consistent
        // 1 step per 10 pixels of distance, adjust as needed
        steps = Math.max(1, Math.trunc(dist / 10));

        step_dx = dx / steps;
        step_dy = dy / steps;
        step_dist = dist / steps;
        step_wait_time = Math.max(10, movement_time / steps);

        let step = 0;
        let time_still = 0;
        const cursorInterval = setInterval(() => {
            // move cursor
            if (step < steps) {
                x += step_dx;
                y += step_dy;
                cursor.style.left = `${x}px`;
                cursor.style.top = `${y}px`;
            }
            // still cursor (wait a bit)
            else if (time_still < still_wait_time) {
                time_still += step_wait_time;
            }
            // stop and detach cursor
            else {
                clearInterval(cursorInterval);
                window.browsergym_visual_cursor_n_owners -= 1;
                if (window.browsergym_visual_cursor_n_owners <= 0) {
                    document.body.removeChild(cursor);

                }
            }
            step += 1;
        }, step_wait_time);

        return movement_time;
    }""",
        [x, y, speed],
    )
    page.wait_for_timeout(movement_time)

def check_for_overlay(
    page: playwright.sync_api.Page, bid: str, element: playwright.sync_api.ElementHandle, box: dict
):
    if not element:
        return False

    visibility = element.get_attribute("browsergym_visibility_ratio")
    if visibility is not None:
        return float(visibility) >= 0.5

    """Checks if a given element is the topmost element at its center position by default.
    If check_corners is True, it checks if any of the corners is visible."""
    if box:
        # corners
        points_to_check = [
            (box["x"], box["y"]),
            (box["x"] + box["width"], box["y"]),
            (box["x"], box["y"] + box["height"]),


            }
            // stop and detach cursor
            else {
                clearInterval(cursorInterval);
                window.browsergym_visual_cursor_n_owners -= 1;
                if (window.browsergym_visual_cursor_n_owners <= 0) {
                    document.body.removeChild(cursor);

                }
            }
            step += 1;
        }, step_wait_time);

        return movement_time;
    }""",
        [x, y, speed],
    )
    page.wait_for_timeout(movement_time)

def check_for_overlay(
    page: playwright.sync_api.Page, bid: str, element: playwright.sync_api.ElementHandle, box: dict
):
    if not element:
        return False

    visibility = element.get_attribute("browsergym_visibility_ratio")
    if visibility is not None:
        return float(visibility) >= 0.5

    """Checks if a given element is the topmost element at its center position by default.
    If check_corners is True, it checks if any of the corners is visible."""
    if box:
        # corners
        points_to_check = [
            (box["x"], box["y"]),
            (box["x"] + box["width"], box["y"]),
            (box["x"], box["y"] + box["height"]),
            (box["x"] + box["width"], box["y"] + box["height"]),
        ]

        for x, y in points_to_check:
            # Execute JavaScript to find the topmost element at the point.
            top_element = page.evaluate(
                f"""() => {{
                const el = document.elementFromPoint({x}, {y});
                return el ? el.outerHTML : '';
            }}"""
            )

            # Check if the topmost element is the element we're interested in.
            if top_element and bid in top_element:
                return True

    return False

def add_demo_mode_effects(
    page: playwright.sync_api.Page,
    elem: playwright.sync_api.ElementHandle,
    bid: str,
    demo_mode: Literal["off", "default", "all_blue", "only_visible_elements"],
    move_cursor: bool = True,
    highlight_box: bool = True,
):
    if demo_mode == "off":
        return

    """Adds visual effects to the target element"""
    box = elem.bounding_box()
    # box = extract_bounds_cdp(page, bid)
    if box:
        center_x, center_y = box["x"] + box["width"] / 2, box["y"] + box["height"] / 2
        is_top_element = check_for_overlay(page, bid, elem, box)


            (box["x"], box["y"] + box["height"]),
            (box["x"] + box["width"], box["y"] + box["height"]),
        ]

        for x, y in points_to_check:
            # Execute JavaScript to find the topmost element at the point.
            top_element = page.evaluate(
                f"""() => {{
                const el = document.elementFromPoint({x}, {y});
                return el ? el.outerHTML : '';
            }}"""
            )

            # Check if the topmost element is the element we're interested in.
            if top_element and bid in top_element:
                return True

    return False

def add_demo_mode_effects(
    page: playwright.sync_api.Page,
    elem: playwright.sync_api.ElementHandle,
    bid: str,
    demo_mode: Literal["off", "default", "all_blue", "only_visible_elements"],
    move_cursor: bool = True,
    highlight_box: bool = True,
):
    if demo_mode == "off":
        return

    """Adds visual effects to the target element"""
    box = elem.bounding_box()
    # box = extract_bounds_cdp(page, bid)
    if box:
        center_x, center_y = box["x"] + box["width"] / 2, box["y"] + box["height"] / 2
        is_top_element = check_for_overlay(page, bid, elem, box)

        if demo_mode == "only_visible_elements":
            if not is_top_element:
                return
            else:
                color = "blue"

        elif demo_mode == "default":
            if is_top_element:
                color = "blue"
            else:
                color = "red"

        elif demo_mode == "all_blue":
            color = "blue"

        if move_cursor:
            smooth_move_visual_cursor_to(page, center_x, center_y)

        if highlight_box:
            highlight_by_box(page, box, color=color)

def call_fun(fun: callable, retry_with_force: bool):
    try:
        fun(force=False)
    except playwright.sync_api.TimeoutError as e:
        if retry_with_force:
            fun(force=True)
        else:
            raise e

def map_coordinates(page, x, y):
    """
    Maps screenshot coordinates back to page coordinates based on bgym_scale_factor.

    When bgym_scale_factor > 1.0, the screenshot is captured at higher resolution,
    so VLM coordinates need to be scaled down to match the actual page coordinates.

    Args:


                return
            else:
                color = "blue"

        elif demo_mode == "default":
            if is_top_element:
                color = "blue"
            else:
                color = "red"

        elif demo_mode == "all_blue":
            color = "blue"

        if move_cursor:
            smooth_move_visual_cursor_to(page, center_x, center_y)

        if highlight_box:
            highlight_by_box(page, box, color=color)

def call_fun(fun: callable, retry_with_force: bool):
    try:
        fun(force=False)
    except playwright.sync_api.TimeoutError as e:
        if retry_with_force:
            fun(force=True)
        else:
            raise e

def map_coordinates(page, x, y):
    """
    Maps screenshot coordinates back to page coordinates based on bgym_scale_factor.

    When bgym_scale_factor > 1.0, the screenshot is captured at higher resolution,
    so VLM coordinates need to be scaled down to match the actual page coordinates.

    Args:
        page: Playwright page object (should have _bgym_scale_factor attribute)
        x: X coordinate from VLM (based on scaled screenshot)
        y: Y coordinate from VLM (based on scaled screenshot)

    Returns:
        tuple: (mapped_x, mapped_y) coordinates for use with Playwright

    Examples:
        # If bgym_scale_factor = 1.5 and VLM returns coordinates (300, 450)
        # from the scaled screenshot, map them back to page coordinates:


        elif demo_mode == "all_blue":
            color = "blue"

        if move_cursor:
            smooth_move_visual_cursor_to(page, center_x, center_y)

        if highlight_box:
            highlight_by_box(page, box, color=color)

def call_fun(fun: callable, retry_with_force: bool):
    try:
        fun(force=False)
    except playwright.sync_api.TimeoutError as e:
        if retry_with_force:
            fun(force=True)
        else:
            raise e

def map_coordinates(page, x, y):
    """
    Maps screenshot coordinates back to page coordinates based on bgym_scale_factor.

    When bgym_scale_factor > 1.0, the screenshot is captured at higher resolution,
    so VLM coordinates need to be scaled down to match the actual page coordinates.

    Args:
        page: Playwright page object (should have _bgym_scale_factor attribute)
        x: X coordinate from VLM (based on scaled screenshot)
        y: Y coordinate from VLM (based on scaled screenshot)

    Returns:
        tuple: (mapped_x, mapped_y) coordinates for use with Playwright

    Examples:
        # If bgym_scale_factor = 1.5 and VLM returns coordinates (300, 450)
        # from the scaled screenshot, map them back to page coordinates:
        page_x, page_y = map_coordinates(page, 300, 450)  # Returns (200.0, 300.0)
    """
    scale_factor = getattr(page, "_bgym_scale_factor", 1.0)

    # Scale down coordinates to match actual page coordinate system
    mapped_x = x / scale_factor
    mapped_y = y / scale_factor

    return mapped_x, mapped_y