From 1b387013650b2ff2287e509dcffab9b5f2c4570c Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 10 Apr 2024 13:16:15 -0700 Subject: [PATCH 1/5] Add custom js --- examples/reference/chat/ChatInterface.ipynb | 42 ++++++++++++++++++++- panel/chat/interface.py | 36 +++++++++++++++--- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/examples/reference/chat/ChatInterface.ipynb b/examples/reference/chat/ChatInterface.ipynb index 01f7698cd6..0faaae06a8 100644 --- a/examples/reference/chat/ChatInterface.ipynb +++ b/examples/reference/chat/ChatInterface.ipynb @@ -40,7 +40,21 @@ "* **`avatar`** (`str | bytes | BytesIO | pn.pane.Image`): The avatar to use for the user. Can be a single character text, an emoji, or anything supported by `pn.pane.Image`. If not set, uses the first character of the name.\n", "* **`reset_on_send`** (`bool`): Whether to reset the widget's value after sending a message; has no effect for `TextInput`.\n", "* **`auto_send_types`** (`tuple`): The widget types to automatically send when the user presses enter or clicks away from the widget. If not provided, defaults to `[TextInput]`.\n", - "* **`button_properties`** (`Dict[Dict[str, Any]]`): Allows addition of functionality or customization of buttons by supplying a mapping from the button name to a dictionary containing the `icon`, `callback`, and/or `post_callback` keys. If the button names correspond to default buttons (send, rerun, undo, clear), the default icon can be updated and if a `callback` key value pair is provided, the specified callback functionality runs before the existing one. For button names that don't match existing ones, new buttons are created and must include a `callback` or `post_callback` key. The provided callbacks should have a signature that accepts two positional arguments: instance (the ChatInterface instance) and event (the button click event).\n", + "* **`button_properties`** (`Dict[Dict[str, Any]]`): Allows addition of functionality or customization of buttons by supplying a mapping from the button name to a dictionary containing the `icon`, `callback`, `post_callback`, and/or `js_on_click` keys. \n", + " * If the button names correspond to default buttons\n", + "(send, rerun, undo, clear), the default icon can be\n", + "updated and if a `callback` key value pair is provided,\n", + "the specified callback functionality runs before the existing one.\n", + " * For button names that don't match existing ones,\n", + "new buttons are created and must include a\n", + "`callback`, `post_callback`, and/or `js_on_click` key.\n", + " * The provided callbacks should have a signature that accepts\n", + "two positional arguments: instance (the ChatInterface instance)\n", + "and event (the button click event).\n", + " * The `js_on_click` key should be a string of JavaScript code\n", + "to execute when the button is clicked. The `js_args` key\n", + "should be a dictionary of arguments to pass to the JavaScript\n", + "code.\n", "\n", "##### Styling\n", "\n", @@ -476,6 +490,32 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You may also use custom Javascript code with `js_on_click` for the buttons, and also set the `button_properties` after definition.\n", + "\n", + "Try typing something in the chat input, and then click the new `Help` button on the bottom right." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chat_interface = pn.chat.ChatInterface()\n", + "chat_interface.button_properties={\n", + " \"help\": {\n", + " \"icon\": \"help\",\n", + " \"js_on_click\": \"alert(`Typed: '${chat_input.value}'`)\",\n", + " \"js_args\": {\"chat_input\": chat_interface.active_widget},\n", + " },\n", + "}\n", + "chat_interface" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 8a162e7003..43cea27b73 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -45,6 +45,12 @@ class _ChatButtonData: The objects to display. buttons : List The buttons to display. + callback : Callable + The callback to execute when the button is clicked. + js_on_click : str | None + The JavaScript to execute when the button is clicked. + js_args : Dict[str, Any] | None + The JavaScript arguments to pass when the button is clicked. """ index: int @@ -53,6 +59,8 @@ class _ChatButtonData: objects: List buttons: List callback: Callable + js_on_click: str | None = None + js_args: Dict[str, Any] | None = None class ChatInterface(ChatFeed): @@ -114,16 +122,25 @@ class ChatInterface(ChatFeed): button_properties = param.Dict(default={}, doc=""" Allows addition of functionality or customization of buttons by supplying a mapping from the button name to a dictionary - containing the `icon`, `callback`, and/or `post_callback` keys. + containing the `icon`, `callback`, `post_callback`, and/or `js_on_click` keys. + If the button names correspond to default buttons (send, rerun, undo, clear), the default icon can be updated and if a `callback` key value pair is provided, the specified callback functionality runs before the existing one. + For button names that don't match existing ones, - new buttons are created and must include a `callback` or `post_callback` key. + new buttons are created and must include a + `callback`, `post_callback`, and/or `js_on_click` key. + The provided callbacks should have a signature that accepts two positional arguments: instance (the ChatInterface instance) and event (the button click event). + + The `js_on_click` key should be a string of JavaScript code + to execute when the button is clicked. The `js_args` key + should be a dictionary of arguments to pass to the JavaScript + code. """) _widgets = param.Dict(default={}, allow_refs=False, doc=""" @@ -207,6 +224,7 @@ def _init_widgets(self): name = name.lower() callback = properties.get("callback") post_callback = properties.get("post_callback") + js_on_click = properties.get("js_on_click") default_properties = default_button_properties.get(name) or {} if default_properties: default_callback = default_properties["_default_callback"] @@ -222,7 +240,7 @@ def _init_widgets(self): callback = self._wrap_callbacks(post_callback=post_callback)(callback) elif callback is None and post_callback is not None: callback = post_callback - elif callback is None and post_callback is None: + elif callback is None and post_callback is None and not js_on_click: raise ValueError(f"A 'callback' key is required for the {name!r} button") icon = properties.get("icon") or default_properties.get("icon") self._button_data[name] = _ChatButtonData( @@ -232,6 +250,8 @@ def _init_widgets(self): objects=[], buttons=[], callback=callback, + js_on_click=js_on_click, + js_args=properties.get("js_args"), ) widgets = self.widgets @@ -300,8 +320,14 @@ def _init_widgets(self): ) if action != "stop": self._link_disabled_loading(button) - callback = partial(button_data.callback, self) - button.on_click(callback) + if button_data.callback: + callback = partial(button_data.callback, self) + button.on_click(callback) + if button_data.js_on_click: + button.js_on_click( + args=(button_data.js_args or {}), + code=button_data.js_on_click + ) self._buttons[action] = button button_data.buttons.append(button) From 1127b7d00dbc3b139a2ca2fa8bcb7859e5e100fa Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 10 Apr 2024 13:26:59 -0700 Subject: [PATCH 2/5] Add test --- panel/tests/ui/chat/test_chat_interface_ui.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/panel/tests/ui/chat/test_chat_interface_ui.py b/panel/tests/ui/chat/test_chat_interface_ui.py index 401a6e4037..4b2658c4d7 100644 --- a/panel/tests/ui/chat/test_chat_interface_ui.py +++ b/panel/tests/ui/chat/test_chat_interface_ui.py @@ -16,3 +16,24 @@ def test_chat_interface_help(page): message = page.locator("p") message_text = message.inner_text() assert message_text == "This is a test help text" + + +def test_chat_interface_custom_js(page): + chat_interface = ChatInterface() + chat_interface.button_properties={ + "help": { + "icon": "help", + "js_on_click": "console.log(`Typed: '${chat_input.value}'`)", + "js_args": {"chat_input": chat_interface.active_widget}, + }, + } + serve_component(page, chat_interface) + + chat_input = page.locator(".bk-input") + chat_input.fill("Hello") + + with page.expect_console_message() as msg_info: + page.locator("button", has_text="help").click() + msg = msg_info.value + + assert msg.args[0].json_value() == "Typed: 'Hello'" From c04ec2e5720ae184690836f77b1fa8894b8d8566 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 17 Apr 2024 11:29:56 -0700 Subject: [PATCH 3/5] Use dict instead --- examples/reference/chat/ChatInterface.ipynb | 17 +++++----- panel/chat/interface.py | 31 ++++++++++++------- panel/tests/chat/test_interface.py | 22 +++++++++++++ panel/tests/ui/chat/test_chat_interface_ui.py | 6 ++-- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/examples/reference/chat/ChatInterface.ipynb b/examples/reference/chat/ChatInterface.ipynb index 0faaae06a8..0df4e2cdf8 100644 --- a/examples/reference/chat/ChatInterface.ipynb +++ b/examples/reference/chat/ChatInterface.ipynb @@ -51,9 +51,10 @@ " * The provided callbacks should have a signature that accepts\n", "two positional arguments: instance (the ChatInterface instance)\n", "and event (the button click event).\n", - " * The `js_on_click` key should be a string of JavaScript code\n", - "to execute when the button is clicked. The `js_args` key\n", - "should be a dictionary of arguments to pass to the JavaScript\n", + " * The `js_on_click` key should be a dict, with a\n", + "`code` key, containing the JavaScript code\n", + "to execute when the button is clicked, and `args` key,\n", + "containing dictionary of arguments to pass to the JavaScript\n", "code.\n", "\n", "##### Styling\n", @@ -494,7 +495,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You may also use custom Javascript code with `js_on_click` for the buttons, and also set the `button_properties` after definition.\n", + "You may also use custom Javascript code with `js_on_click` containing `code` and `args` keys for the buttons, and also set the `button_properties` after definition.\n", "\n", "Try typing something in the chat input, and then click the new `Help` button on the bottom right." ] @@ -506,11 +507,13 @@ "outputs": [], "source": [ "chat_interface = pn.chat.ChatInterface()\n", - "chat_interface.button_properties={\n", + "chat_interface.button_properties = {\n", " \"help\": {\n", " \"icon\": \"help\",\n", - " \"js_on_click\": \"alert(`Typed: '${chat_input.value}'`)\",\n", - " \"js_args\": {\"chat_input\": chat_interface.active_widget},\n", + " \"js_on_click\": {\n", + " \"code\": \"alert(`Typed: '${chat_input.value}'`)\",\n", + " \"args\": {\"chat_input\": chat_interface.active_widget},\n", + " },\n", " },\n", "}\n", "chat_interface" diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 43cea27b73..f1a6cc284c 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -47,10 +47,8 @@ class _ChatButtonData: The buttons to display. callback : Callable The callback to execute when the button is clicked. - js_on_click : str | None - The JavaScript to execute when the button is clicked. - js_args : Dict[str, Any] | None - The JavaScript arguments to pass when the button is clicked. + js_on_click : dict | None + The JavaScript `code` and `args` to execute when the button is clicked. """ index: int @@ -59,8 +57,7 @@ class _ChatButtonData: objects: List buttons: List callback: Callable - js_on_click: str | None = None - js_args: Dict[str, Any] | None = None + js_on_click: dict | None = None class ChatInterface(ChatFeed): @@ -137,9 +134,10 @@ class ChatInterface(ChatFeed): two positional arguments: instance (the ChatInterface instance) and event (the button click event). - The `js_on_click` key should be a string of JavaScript code - to execute when the button is clicked. The `js_args` key - should be a dictionary of arguments to pass to the JavaScript + The `js_on_click` key should be a dict, with a + `code` key, containing the JavaScript code + to execute when the button is clicked, and `args` key, + containing dictionary of arguments to pass to the JavaScript code. """) @@ -251,7 +249,6 @@ def _init_widgets(self): buttons=[], callback=callback, js_on_click=js_on_click, - js_args=properties.get("js_args"), ) widgets = self.widgets @@ -324,9 +321,19 @@ def _init_widgets(self): callback = partial(button_data.callback, self) button.on_click(callback) if button_data.js_on_click: + if not isinstance(button_data.js_on_click, dict): + raise ValueError( + f"The 'js_on_click' key for the {action!r} button must be a dict " + f"containing 'code' and 'args' keys, but got {button_data.js_on_click!r}" + ) + elif "code" not in button_data.js_on_click: + raise ValueError( + f"A 'code' key is required for the {action!r} button's " + "'js_on_click' key" + ) button.js_on_click( - args=(button_data.js_args or {}), - code=button_data.js_on_click + args=(button_data.js_on_click.get("args") or {}), + code=button_data.js_on_click["code"], ) self._buttons[action] = button button_data.buttons.append(button) diff --git a/panel/tests/chat/test_interface.py b/panel/tests/chat/test_interface.py index 9715c0c1ae..4179aa2969 100644 --- a/panel/tests/chat/test_interface.py +++ b/panel/tests/chat/test_interface.py @@ -382,6 +382,28 @@ def post_callback(instance, event): assert chat_interface.objects[1].object == "2" assert chat_interface.objects[2].object == "3" + def test_custom_js_no_code(self): + chat_interface = ChatInterface() + with pytest.raises(ValueError, match="A 'code' key is required for"): + chat_interface.button_properties={ + "help": { + "icon": "help", + "js_on_click": { + "args": {"chat_input": chat_interface.active_widget}, + }, + }, + } + + def test_custom_js_not_dict(self): + chat_interface = ChatInterface() + with pytest.raises(ValueError, match="must be a dict"): + chat_interface.button_properties={ + "help": { + "icon": "help", + "js_on_click": "alert('Hello')", + }, + } + def test_manual_user(self): chat_interface = ChatInterface(user="New User") assert chat_interface.user == "New User" diff --git a/panel/tests/ui/chat/test_chat_interface_ui.py b/panel/tests/ui/chat/test_chat_interface_ui.py index 4b2658c4d7..356d62637d 100644 --- a/panel/tests/ui/chat/test_chat_interface_ui.py +++ b/panel/tests/ui/chat/test_chat_interface_ui.py @@ -23,8 +23,10 @@ def test_chat_interface_custom_js(page): chat_interface.button_properties={ "help": { "icon": "help", - "js_on_click": "console.log(`Typed: '${chat_input.value}'`)", - "js_args": {"chat_input": chat_interface.active_widget}, + "js_on_click": { + "code": "console.log(`Typed: '${chat_input.value}'`)", + "args": {"chat_input": chat_interface.active_widget}, + }, }, } serve_component(page, chat_interface) From 30570c76a2095c97b181cb580ed8bf8909d8f353 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 17 Apr 2024 12:34:13 -0700 Subject: [PATCH 4/5] Allow str --- examples/reference/chat/ChatInterface.ipynb | 5 +-- panel/chat/interface.py | 34 +++++++++---------- panel/tests/ui/chat/test_chat_interface_ui.py | 20 +++++++++++ 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/examples/reference/chat/ChatInterface.ipynb b/examples/reference/chat/ChatInterface.ipynb index 0df4e2cdf8..91f18ce5ae 100644 --- a/examples/reference/chat/ChatInterface.ipynb +++ b/examples/reference/chat/ChatInterface.ipynb @@ -51,9 +51,10 @@ " * The provided callbacks should have a signature that accepts\n", "two positional arguments: instance (the ChatInterface instance)\n", "and event (the button click event).\n", - " * The `js_on_click` key should be a dict, with a\n", + " * The `js_on_click` key should be a str or dict. If str,\n", + "provide the JavaScript code; else if dict, it must have a\n", "`code` key, containing the JavaScript code\n", - "to execute when the button is clicked, and `args` key,\n", + "to execute when the button is clicked, and optionally an `args` key,\n", "containing dictionary of arguments to pass to the JavaScript\n", "code.\n", "\n", diff --git a/panel/chat/interface.py b/panel/chat/interface.py index f1a6cc284c..0136599082 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -47,7 +47,7 @@ class _ChatButtonData: The buttons to display. callback : Callable The callback to execute when the button is clicked. - js_on_click : dict | None + js_on_click : dict | str | None The JavaScript `code` and `args` to execute when the button is clicked. """ @@ -57,7 +57,7 @@ class _ChatButtonData: objects: List buttons: List callback: Callable - js_on_click: dict | None = None + js_on_click: dict | str | None = None class ChatInterface(ChatFeed): @@ -134,9 +134,10 @@ class ChatInterface(ChatFeed): two positional arguments: instance (the ChatInterface instance) and event (the button click event). - The `js_on_click` key should be a dict, with a + The `js_on_click` key should be a str or dict. If str, + provide the JavaScript code; else if dict, it must have a `code` key, containing the JavaScript code - to execute when the button is clicked, and `args` key, + to execute when the button is clicked, and optionally an `args` key, containing dictionary of arguments to pass to the JavaScript code. """) @@ -321,20 +322,19 @@ def _init_widgets(self): callback = partial(button_data.callback, self) button.on_click(callback) if button_data.js_on_click: - if not isinstance(button_data.js_on_click, dict): - raise ValueError( - f"The 'js_on_click' key for the {action!r} button must be a dict " - f"containing 'code' and 'args' keys, but got {button_data.js_on_click!r}" + js_on_click = button_data.js_on_click + if isinstance(js_on_click, dict): + if "code" not in js_on_click: + raise ValueError( + f"A 'code' key is required for the {action!r} button's " + "'js_on_click' key" + ) + button.js_on_click( + args=js_on_click.get("args", {}), + code=js_on_click["code"], ) - elif "code" not in button_data.js_on_click: - raise ValueError( - f"A 'code' key is required for the {action!r} button's " - "'js_on_click' key" - ) - button.js_on_click( - args=(button_data.js_on_click.get("args") or {}), - code=button_data.js_on_click["code"], - ) + elif isinstance(js_on_click, str): + button.js_on_click(code=js_on_click) self._buttons[action] = button button_data.buttons.append(button) diff --git a/panel/tests/ui/chat/test_chat_interface_ui.py b/panel/tests/ui/chat/test_chat_interface_ui.py index 356d62637d..b156f5d9af 100644 --- a/panel/tests/ui/chat/test_chat_interface_ui.py +++ b/panel/tests/ui/chat/test_chat_interface_ui.py @@ -39,3 +39,23 @@ def test_chat_interface_custom_js(page): msg = msg_info.value assert msg.args[0].json_value() == "Typed: 'Hello'" + + +def test_chat_interface_custom_js_string(page): + chat_interface = ChatInterface() + chat_interface.button_properties={ + "help": { + "icon": "help", + "js_on_click": "console.log(`Clicked`)", + }, + } + serve_component(page, chat_interface) + + chat_input = page.locator(".bk-input") + chat_input.fill("Hello") + + with page.expect_console_message() as msg_info: + page.locator("button", has_text="help").click() + msg = msg_info.value + + assert msg.args[0].json_value() == "Clicked" From f28269590b642fcd5f679c91af1c3eaeeb9442e0 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Thu, 18 Apr 2024 08:34:49 -0700 Subject: [PATCH 5/5] Update panel/tests/chat/test_interface.py --- panel/tests/chat/test_interface.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/panel/tests/chat/test_interface.py b/panel/tests/chat/test_interface.py index 4179aa2969..9829e8b9ec 100644 --- a/panel/tests/chat/test_interface.py +++ b/panel/tests/chat/test_interface.py @@ -394,16 +394,6 @@ def test_custom_js_no_code(self): }, } - def test_custom_js_not_dict(self): - chat_interface = ChatInterface() - with pytest.raises(ValueError, match="must be a dict"): - chat_interface.button_properties={ - "help": { - "icon": "help", - "js_on_click": "alert('Hello')", - }, - } - def test_manual_user(self): chat_interface = ChatInterface(user="New User") assert chat_interface.user == "New User"