Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow adding JS callbacks in ChatInterface.button_properties #6706

Merged
merged 5 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Loading