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 2 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
42 changes: 41 additions & 1 deletion examples/reference/chat/ChatInterface.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {},
Expand Down
36 changes: 31 additions & 5 deletions panel/chat/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
"""

index: int
Expand All @@ -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):
Expand Down Expand Up @@ -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="""
Expand Down Expand Up @@ -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"]
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down
21 changes: 21 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,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'"
Loading