Skip to content

Commit

Permalink
Allow adding JS callbacks in ChatInterface.button_properties (#6706)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 committed Apr 19, 2024
1 parent b9ea9e2 commit 373b846
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 6 deletions.
46 changes: 45 additions & 1 deletion examples/reference/chat/ChatInterface.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,23 @@
"* **`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 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 optionally an `args` key,\n",
"containing dictionary of arguments to pass to the JavaScript\n",
"code.\n",
"\n",
"##### Styling\n",
"\n",
Expand Down Expand Up @@ -476,6 +492,34 @@
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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."
]
},
{
"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\": {\n",
" \"code\": \"alert(`Typed: '${chat_input.value}'`)\",\n",
" \"args\": {\"chat_input\": chat_interface.active_widget},\n",
" },\n",
" },\n",
"}\n",
"chat_interface"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
43 changes: 38 additions & 5 deletions panel/chat/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ 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 : dict | str | None
The JavaScript `code` and `args` to execute when the button is clicked.
"""

index: int
Expand All @@ -53,6 +57,7 @@ class _ChatButtonData:
objects: List
buttons: List
callback: Callable
js_on_click: dict | str | None = None


class ChatInterface(ChatFeed):
Expand Down Expand Up @@ -114,16 +119,27 @@ 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 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 optionally an `args` key,
containing dictionary of arguments to pass to the JavaScript
code.
""")

_widgets = param.Dict(default={}, allow_refs=False, doc="""
Expand Down Expand Up @@ -207,6 +223,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"]
Expand All @@ -222,7 +239,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(
Expand All @@ -232,6 +249,7 @@ def _init_widgets(self):
objects=[],
buttons=[],
callback=callback,
js_on_click=js_on_click,
)

widgets = self.widgets
Expand Down Expand Up @@ -300,8 +318,23 @@ 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:
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 isinstance(js_on_click, str):
button.js_on_click(code=js_on_click)
self._buttons[action] = button
button_data.buttons.append(button)

Expand Down
12 changes: 12 additions & 0 deletions panel/tests/chat/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,18 @@ 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_manual_user(self):
chat_interface = ChatInterface(user="New User")
assert chat_interface.user == "New User"
Expand Down
43 changes: 43 additions & 0 deletions panel/tests/ui/chat/test_chat_interface_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,46 @@ 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": {
"code": "console.log(`Typed: '${chat_input.value}'`)",
"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'"


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"

0 comments on commit 373b846

Please sign in to comment.