From 12844326a8b41796ac48df73c21e1d4af65d0b19 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 17 Dec 2024 14:22:22 -0800 Subject: [PATCH 1/8] Add edit functionality --- examples/reference/chat/ChatFeed.ipynb | 34 +++++++++ examples/reference/chat/ChatMessage.ipynb | 2 + panel/chat/feed.py | 35 +++++++++- panel/chat/input.py | 5 ++ panel/chat/interface.py | 2 + panel/chat/message.py | 70 +++++++++++++++++-- panel/dist/css/chat_message.css | 15 ++++ panel/tests/chat/test_feed.py | 19 +++++ panel/tests/chat/test_interface.py | 19 +++++ panel/tests/ui/chat/test_chat_interface_ui.py | 28 ++++++++ 10 files changed, 223 insertions(+), 6 deletions(-) diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb index d754d02248..5ff0e6028e 100644 --- a/examples/reference/chat/ChatFeed.ipynb +++ b/examples/reference/chat/ChatFeed.ipynb @@ -50,6 +50,7 @@ "* **`header`** (Any): The header of the chat feed; commonly used for the title. Can be a string, pane, or widget.\n", "* **`callback_user`** (str): The default user name to use for the message provided by the callback.\n", "* **`callback_avatar`** (str, BytesIO, bytes, ImageBase): The default avatar to use for the entry provided by the callback. Takes precedence over `ChatMessage.default_avatars` if set; else, if None, defaults to the avatar set in `ChatMessage.default_avatars` if matching key exists. Otherwise defaults to the first character of the `callback_user`.\n", + "* **`edit_callback`** (callable): Callback to execute when a user edits a message. The signature must include the previous message value `contents`, the previous `user` name, and the component `instance`.\n", "* **`help_text`** (str): If provided, initializes a chat message in the chat log using the provided help text as the message object and `help` as the user. This is useful for providing instructions, and will not be included in the `serialize` method by default.\n", "* **`placeholder_text`** (str): The text to display next to the placeholder icon.\n", "* **`placeholder_params`** (dict) Defaults to `{\"user\": \" \", \"reaction_icons\": {}, \"show_copy_icon\": False, \"show_timestamp\": False}` Params to pass to the placeholder `ChatMessage`, like `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, `show_timestamp`.\n", @@ -651,6 +652,39 @@ "message = chat_feed.send(\"Hello bots!\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Edit Callbacks\n", + "\n", + "An `edit_callback` can be attached to the `ChatFeed` to handle message edits.\n", + "\n", + "The signature must include the latest available message value `contents`, the index of the edited message, and the chat `instance`.\n", + "\n", + "Here, when the user edits the first message, the downstream message is updated to match the edited message." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def echo_callback(content, index, instance):\n", + " return content\n", + "\n", + "\n", + "def edit_callback(content, index, instance):\n", + " instance.objects[index + 1].object = content\n", + "\n", + "\n", + "chat_feed = pn.chat.ChatFeed(\n", + " edit_callback=edit_callback, callback=echo_callback, callback_user=\"Echo Guy\"\n", + ")\n", + "chat_feed.send(\"Edit this\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/examples/reference/chat/ChatMessage.ipynb b/examples/reference/chat/ChatMessage.ipynb index 9315925a0e..60d9b6c7a1 100644 --- a/examples/reference/chat/ChatMessage.ipynb +++ b/examples/reference/chat/ChatMessage.ipynb @@ -45,6 +45,7 @@ "* **`user`** (str): Name of the user who sent the message.\n", "* **`avatar`** (str | BinaryIO): 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", "* **`default_avatars`** (Dict[str, str | BinaryIO]): A default mapping of user names to their corresponding avatars to use when the user is set but the avatar is not. You can modify, but not replace the dictionary. Note, the keys are *only* alphanumeric sensitive, meaning spaces, special characters, and case sensitivity is disregarded, e.g. `\"Chat-GPT3.5\"`, `\"chatgpt 3.5\"` and `\"Chat GPT 3.5\"` all map to the same value.\n", + "* **`edited`** (bool): An event that is triggered when the message is edited\n", "* **`footer_objects`** (List): A list of objects to display in the column of the footer of the message.\n", "* **`header_objects`** (List): A list of objects to display in the row of the header of the message.\n", "* **`avatar_lookup`** (Callable): A function that can lookup an `avatar` from a user name. The function signature should be `(user: str) -> Avatar`. If this is set, `default_avatars` is disregarded.\n", @@ -61,6 +62,7 @@ "* **`show_timestamp`** (bool): Whether to display the timestamp of the message.\n", "* **`show_reaction_icons`** (bool): Whether to display the reaction icons.\n", "* **`show_copy_icon`** (bool): Whether to show the copy icon.\n", + "* **`show_edit_icon`** (bool): Whether to display the edit icon.\n", "* **`show_activity_dot`** (bool): Whether to show the activity dot.\n", "* **`name`** (str): The title or name of the chat message widget, if any.\n", "\n", diff --git a/panel/chat/feed.py b/panel/chat/feed.py index c299b948db..9470729fe8 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -118,6 +118,11 @@ class ChatFeed(ListPanel): defaults to the avatar set in `ChatMessage.default_avatars` if matching key exists. Otherwise defaults to the first character of the `callback_user`.""") + edit_callback = param.Callable(allow_refs=False, doc=""" + Callback to execute when a user edits a message. + The signature must include the new message value `contents`, + the `message_index`, and the `instance`.""") + card_params = param.Dict(default={}, doc=""" Params to pass to Card, like `header`, `header_background`, `header_color`, etc.""") @@ -232,7 +237,16 @@ def __init__(self, *objects, **params): super().__init__(*objects, **params) if self.help_text: - self.objects = [ChatMessage(self.help_text, user="Help", **message_params), *self.objects] + self.objects = [ + ChatMessage( + self.help_text, + user="Help", + show_edit_icon=False, + show_copy_icon=False, + show_reaction_icons=False, + **message_params + ), *self.objects + ] # instantiate the card's column linked_params = dict( @@ -368,6 +382,17 @@ def _replace_placeholder(self, message: ChatMessage | None = None) -> None: except ValueError: pass + async def _on_edit_message(self, event): + if self.edit_callback is None: + return + message = event.obj + contents = message.serialize() + index = self._chat_log.index(message) + if iscoroutinefunction(self.edit_callback): + await self.edit_callback(contents, index, self) + else: + self.edit_callback(contents, index, self) + def _build_message( self, value: dict, @@ -396,7 +421,15 @@ def _build_message( message_params["width"] = int(self.width - 80) message_params.update(input_message_params) + if "show_edit_icon" not in message_params: + user = message_params.get("user", "") + message_params["show_edit_icon"] = ( + bool(self.edit_callback) and + user.lower() not in (self.callback_user.lower(), "help") + ) + message = ChatMessage(**message_params) + message.param.watch(self._on_edit_message, "edited") return message def _upsert_message( diff --git a/panel/chat/input.py b/panel/chat/input.py index 5f885cc839..41d9abe582 100644 --- a/panel/chat/input.py +++ b/panel/chat/input.py @@ -71,10 +71,14 @@ class ChatAreaInput(_PnTextAreaInput): Can only be set during initialization.""", ) + enter_pressed = param.Event(doc=""" + Event when the enter key has been pressed.""") + _widget_type: ClassVar[type[Model]] = _bkChatAreaInput _rename: ClassVar[Mapping[str, str | None]] = { "value": None, + "enter_pressed": None, **_PnTextAreaInput._rename, } @@ -99,5 +103,6 @@ def _process_event(self, event: ChatMessageEvent) -> None: Clear value on shift enter key down. """ self.value = event.value + self.enter_pressed = True with param.discard_events(self): self.value = "" diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 8c8caf15c5..11f3e431a9 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -693,6 +693,7 @@ def send( user = self.user if avatar is None: avatar = self.avatar + message_params["show_edit_icon"] = message_params.get("show_edit_icon", user == self.user) return super().send(value, user=user, avatar=avatar, respond=respond, **message_params) def stream( @@ -739,4 +740,5 @@ def stream( # so only set to the default when not a ChatMessage user = user or self.user avatar = avatar or self.avatar + message_params["show_edit_icon"] = message_params.get("show_edit_icon", user == self.user) return super().stream(value, user=user, avatar=avatar, message=message, replace=replace, **message_params) diff --git a/panel/chat/message.py b/panel/chat/message.py index 395a70c81a..c9804a7f6d 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -30,10 +30,13 @@ HTML, DataFrame, HTMLBasePane, Markdown, ) from ..pane.media import Audio, Video +from ..pane.placeholder import Placeholder from ..param import ParamFunction from ..viewable import ServableMixin, Viewable from ..widgets.base import Widget +from ..widgets.icon import ToggleIcon from .icon import ChatCopyIcon, ChatReactionIcons +from .input import ChatAreaInput from .utils import ( avatar_lookup, build_avatar_pane, serialize_recursively, stream_to, ) @@ -215,6 +218,9 @@ class ChatMessage(Pane): show_avatar = param.Boolean(default=True, doc=""" Whether to display the avatar of the user.""") + show_edit_icon = param.Boolean(default=True, doc=""" + Whether to display the edit icon.""") + show_user = param.Boolean(default=True, doc=""" Whether to display the name of the user.""") @@ -241,6 +247,9 @@ class ChatMessage(Pane): user = param.Parameter(default="User", doc=""" Name of the user who sent the message.""") + edited = param.Event(doc=""" + An event that is triggered when the message is edited.""") + _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/chat_message.css"] # Declares whether Pane supports updates to the Bokeh model @@ -248,9 +257,6 @@ class ChatMessage(Pane): def __init__(self, object=None, **params): self._exit_stack = ExitStack() - self.chat_copy_icon = ChatCopyIcon( - visible=False, width=15, height=15, css_classes=["copy-icon"] - ) if params.get("timestamp") is None: tz = params.get("timestamp_tz") if tz is not None: @@ -264,8 +270,12 @@ def __init__(self, object=None, **params): params["reaction_icons"] = ChatReactionIcons(options=reaction_icons, default_layout=Row, sizing_mode=None) self._internal = True super().__init__(object=object, **params) + self.edit_icon = ToggleIcon( + icon="edit", active_icon="x", width=15, height=15, + stylesheets=self._stylesheets + self.param.stylesheets.rx(), css_classes=["edit-icon"], + ) self.chat_copy_icon = ChatCopyIcon( - visible=False, width=15, height=15, css_classes=["copy-icon"], + visible=False, width=15, height=15, css_classes=["edit-icon"], stylesheets=self._stylesheets + self.param.stylesheets.rx(), ) if not self.avatar: @@ -285,15 +295,30 @@ def _build_layout(self): self.param.watch(self._update_avatar_pane, "avatar") self._object_panel = self._create_panel(self.object) + self._placeholder = Placeholder( + object=self._object_panel, + css_classes=["placeholder"], + stylesheets=self._stylesheets + self.param.stylesheets.rx(), + sizing_mode=None, + ) + self._edit_area = ChatAreaInput( + value=self.object, + css_classes=["edit-area"], + stylesheets=self._stylesheets + self.param.stylesheets.rx() + ) + self._update_chat_copy_icon() + self._update_edit_widgets() self._center_row = Row( - self._object_panel, + self._placeholder, css_classes=["center"], stylesheets=self._stylesheets + self.param.stylesheets.rx(), sizing_mode=None ) self.param.watch(self._update_object_pane, "object") self.param.watch(self._update_reaction_icons, "reaction_icons") + self.edit_icon.param.watch(self._toggle_edit, "value") + self._edit_area.param.watch(self._submit_edit, "enter_pressed") self._user_html = HTML( self.param.user, height=20, @@ -338,6 +363,7 @@ def _build_layout(self): ) self._icons_row = Row( + self.edit_icon, self.chat_copy_icon, self._render_reaction_icons(), css_classes=["icons"], @@ -574,6 +600,7 @@ def _update_object_pane(self, event=None): if old is not new: self._center_row[0] = new self._update_chat_copy_icon() + self._update_edit_widgets() @param.depends("avatar_lookup", "user", watch=True) def _update_avatar(self): @@ -608,6 +635,39 @@ def _update_chat_copy_icon(self): self.chat_copy_icon.value = "" self.chat_copy_icon.visible = False + def _update_edit_widgets(self): + object_panel = self._object_panel + if isinstance(object_panel, HTMLBasePane): + object_panel = object_panel.object + elif isinstance(object_panel, Widget): + object_panel = object_panel.value + if isinstance(object_panel, str) and self.show_edit_icon: + self.edit_icon.visible = True + else: + self.edit_icon.visible = False + + def _toggle_edit(self, event): + if event.new: + with param.discard_events(self): + if isinstance(self._object_panel, HTMLBasePane): + self._edit_area.value = self._object_panel.object + elif isinstance(self._object_panel, Widget): + self._edit_area.value = self._object_panel.value + self._placeholder.update(object=self._edit_area) + else: + self._placeholder.update(object=self._object_panel) + + def _submit_edit(self, event): + if isinstance(self.object, HTMLBasePane): + self.object.object = self._edit_area.value + elif isinstance(self.object, Widget): + self.object.value = self._edit_area.value + else: + self.object = self._edit_area.value + self.param.trigger("object") + self.edit_icon.value = False + self.edited = True + def _cleanup(self, root=None) -> None: """ Cleanup the exit stack. diff --git a/panel/dist/css/chat_message.css b/panel/dist/css/chat_message.css index 084785ec3a..0b7339fbe3 100644 --- a/panel/dist/css/chat_message.css +++ b/panel/dist/css/chat_message.css @@ -147,3 +147,18 @@ font-size: 1.25em; line-height: 0.9em; } + +.edit-icon { + margin-top: 4px; + margin-inline: 3px; +} + +.placeholder { + margin: 0; + width: calc(100% - 15px); +} + +.edit-area { + /* for that smooth transition on a one line message */ + height: 51.5px; +} diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index ce3b20a217..921def4af4 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -1598,3 +1598,22 @@ def append_callback(message, instance): chat_feed.send("AB") wait_until(lambda: chat_feed.objects[-1].object == "Echo: AB") assert logs == ["AB", "Echo: ", "Echo: AB"] + + +@pytest.mark.xdist_group("chat") +class TestChatFeedEditCallback: + + @pytest.mark.parametrize("edit_callback", [None, lambda content, index, instance: ""]) + def test_show_edit_icon_callback(self, chat_feed, edit_callback): + chat_feed.edit_callback = edit_callback + chat_feed.send("Hello") + assert chat_feed[0].show_edit_icon is bool(edit_callback) + + @pytest.mark.parametrize("user", ["User", "Assistant", "Help"]) + def test_show_edit_icon_user(self, chat_feed, user): + chat_feed.edit_callback = lambda content, index, instance: "" + chat_feed.send("Hello", user=user) + if user == "User": + assert chat_feed[0].show_edit_icon + else: + assert not chat_feed[0].show_edit_icon diff --git a/panel/tests/chat/test_interface.py b/panel/tests/chat/test_interface.py index 86e2809b51..60132e6db9 100644 --- a/panel/tests/chat/test_interface.py +++ b/panel/tests/chat/test_interface.py @@ -521,3 +521,22 @@ def test_scale_height(self): assert chat_interface._chat_log.sizing_mode == "scale_height" assert chat_interface._input_layout.sizing_mode == "stretch_width" assert chat_interface._input_layout[0].sizing_mode == "stretch_width" + + +@pytest.mark.xdist_group("chat") +class TestChatInterfaceEditCallback: + + @pytest.fixture + def chat_interface(self): + return ChatInterface() + + def test_show_edit_icon_user(self, chat_interface): + chat_interface.edit_callback = lambda content, index, instance: "" + chat_interface.send("Hello") + assert chat_interface[0].show_edit_icon + + @pytest.mark.parametrize("user", ["admin", "Assistant", "Help"]) + def test_not_show_edit_icon_user(self, chat_interface, user): + chat_interface.edit_callback = lambda content, index, instance: "" + chat_interface.send("Hello", user=user) + assert not chat_interface[0].show_edit_icon diff --git a/panel/tests/ui/chat/test_chat_interface_ui.py b/panel/tests/ui/chat/test_chat_interface_ui.py index a694aead87..b601323be5 100644 --- a/panel/tests/ui/chat/test_chat_interface_ui.py +++ b/panel/tests/ui/chat/test_chat_interface_ui.py @@ -71,3 +71,31 @@ def test_chat_interface_show_button_tooltips(page): help_button.hover() expect(page.locator(".bk-Tooltip")).to_be_visible() + + +def test_chat_interface_edit_message(page): + def echo_callback(content, index, instance): + return content + + def edit_callback(content, index, instance): + instance.objects[index + 1].object = content + + chat_interface = ChatInterface(edit_callback=edit_callback, callback=echo_callback) + chat_interface.send("Edit this") + + serve_component(page, chat_interface) + + # find the edit icon and click .ti.ti-edit + # trict mode violation: locator(".ti-edit") resolved to 2 elements + page.locator(".ti-edit").first.click() + + # find the input field and type new message + chat_input = page.locator(".bk-input").first + chat_input.fill("Edited") + + # click enter + chat_input.press("Enter") + + expect(page.locator(".message").first).to_have_text("Edited") + for object in chat_interface.objects: + assert object.object == "Edited" From 96810d8c0df88c91808eb5b46ee0b4dc13c6ef47 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 17 Dec 2024 14:27:29 -0800 Subject: [PATCH 2/8] update enter pressed docs --- examples/reference/chat/ChatAreaInput.ipynb | 1 + panel/chat/input.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/reference/chat/ChatAreaInput.ipynb b/examples/reference/chat/ChatAreaInput.ipynb index ca80644a21..474c3af301 100644 --- a/examples/reference/chat/ChatAreaInput.ipynb +++ b/examples/reference/chat/ChatAreaInput.ipynb @@ -40,6 +40,7 @@ "* **``enter_sends``** (bool): If True, pressing the Enter key sends the message, if False it is sent by pressing the Ctrl-Enter. Defaults to True.\n", "* **``value``** (str): The value when the \"Enter\" or \"Ctrl-Enter\" key is pressed. Only to be used with `watch` or `bind` because the `value` resets to `\"\"` after the message is sent; use `value_input` instead to access what's currently available in the text input box.\n", "* **``value_input``** (str): The current value updated on every key press.\n", + "* **`enter_pressed`** (bool): Event when the Enter/Ctrl+Enter key has been pressed.\n", "\n", "##### Display\n", "\n", diff --git a/panel/chat/input.py b/panel/chat/input.py index 41d9abe582..b7654aea77 100644 --- a/panel/chat/input.py +++ b/panel/chat/input.py @@ -72,7 +72,7 @@ class ChatAreaInput(_PnTextAreaInput): ) enter_pressed = param.Event(doc=""" - Event when the enter key has been pressed.""") + Event when the Enter/Ctrl+Enter key has been pressed.""") _widget_type: ClassVar[type[Model]] = _bkChatAreaInput From f7173524722aeaab923e90b40e2bb91eae3ff914 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 17 Dec 2024 14:33:23 -0800 Subject: [PATCH 3/8] update change log and release --- CHANGELOG.md | 6 ++++++ doc/about/releases.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17acdb8283..db2b008a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Releases +## Version 1.6.0 + +### Features + +- Introduces `ChatMessage` and `ChatFeed` edit functionality ([#7559](https://github.com/holoviz/panel/pull/7559)) + ## Version 1.5.5 This release fixes a regression causing .node_modules to be bundled into our released wheel and introduces a number of bug fixes and enhancements. Many thanks to @mayonnaisecolouredbenz7, @pmeier, @Italirz, @Coderambling and our maintainer team @MarcSkovMadsen, @hoxbro, @ahuang11, @thuydotm, @maximlt and @philippjfr. diff --git a/doc/about/releases.md b/doc/about/releases.md index 610549bae8..3d743ac030 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -2,6 +2,12 @@ See [the HoloViz blog](https://blog.holoviz.org/#category=panel) for a visual summary of the major features added in each release. +## Version 1.6.0 + +### Features + +- Introduces `ChatMessage` and `ChatFeed` edit functionality ([#7559](https://github.com/holoviz/panel/pull/7559)) + ## Version 1.5.5 This release fixes a regression causing .node_modules to be bundled into our released wheel and introduces a number of bug fixes and enhancements. Many thanks to @mayonnaisecolouredbenz7, @pmeier, @Italirz, @Coderambling and our maintainer team @MarcSkovMadsen, @hoxbro, @ahuang11, @thuydotm, @maximlt and @philippjfr. From bfde3e52a7257a69bb1e23fcfb70f5d3cd5e13f0 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 17 Dec 2024 14:43:56 -0800 Subject: [PATCH 4/8] fix test --- panel/chat/message.py | 1 - 1 file changed, 1 deletion(-) diff --git a/panel/chat/message.py b/panel/chat/message.py index c9804a7f6d..72e24da08c 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -302,7 +302,6 @@ def _build_layout(self): sizing_mode=None, ) self._edit_area = ChatAreaInput( - value=self.object, css_classes=["edit-area"], stylesheets=self._stylesheets + self.param.stylesheets.rx() ) From 3d7082a3281fd2b5a5f8687281f49b4a3ae24276 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 17 Dec 2024 15:04:29 -0800 Subject: [PATCH 5/8] catch edge case --- panel/chat/message.py | 2 +- panel/tests/chat/test_message.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/panel/chat/message.py b/panel/chat/message.py index 72e24da08c..d2a5388469 100644 --- a/panel/chat/message.py +++ b/panel/chat/message.py @@ -597,7 +597,7 @@ def _update_object_pane(self, event=None): old = self._object_panel self._object_panel = new = self._create_panel(self.object, old=old) if old is not new: - self._center_row[0] = new + self._placeholder.update(new) self._update_chat_copy_icon() self._update_edit_widgets() diff --git a/panel/tests/chat/test_message.py b/panel/tests/chat/test_message.py index 4d56c81e3f..fe9ed98cee 100644 --- a/panel/tests/chat/test_message.py +++ b/panel/tests/chat/test_message.py @@ -51,10 +51,10 @@ def test_layout(self): assert isinstance(center_row, Row) object_pane = center_row[0] - assert isinstance(object_pane, Markdown) - assert object_pane.object == "ABC" + assert isinstance(object_pane.object, Markdown) + assert object_pane.object.object == "ABC" - icons = columns[1][4][1] + icons = columns[1][4][2] assert isinstance(icons, ChatReactionIcons) footer_col = columns[1][3] @@ -155,19 +155,19 @@ def test_update_user(self): def test_update_object(self): message = ChatMessage(object="Test") columns = message._composite.objects - object_pane = columns[1][2][0] + object_pane = columns[1][2][0].object assert isinstance(object_pane, Markdown) assert object_pane.object == "Test" message.object = TextInput(value="Also testing...") - object_pane = columns[1][2][0] + object_pane = columns[1][2][0].object assert isinstance(object_pane, TextInput) assert object_pane.value == "Also testing..." message.object = _FileInputMessage( contents=b"I am a file", file_name="test.txt", mime_type="text/plain" ) - object_pane = columns[1][2][0] + object_pane = columns[1][2][0].object assert isinstance(object_pane, Markdown) assert object_pane.object == "I am a file" From 4810064638bff976d13b78fb690d26b643e4a4fa Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 18 Dec 2024 08:25:07 -0800 Subject: [PATCH 6/8] do not edit placeholder --- panel/chat/feed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index 9470729fe8..cca02a519b 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -425,7 +425,7 @@ def _build_message( user = message_params.get("user", "") message_params["show_edit_icon"] = ( bool(self.edit_callback) and - user.lower() not in (self.callback_user.lower(), "help") + user.lower() not in (self.callback_user.lower(), "help", " ") ) message = ChatMessage(**message_params) From c6816c72de54dec827e0d8aac4151b126b3cb5d4 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 18 Dec 2024 09:38:42 -0800 Subject: [PATCH 7/8] Fix placeholder --- panel/chat/feed.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index cca02a519b..9384599ded 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -164,7 +164,11 @@ class ChatFeed(ListPanel): The text to display next to the placeholder icon.""") placeholder_params = param.Dict(default={ - "user": " ", "reaction_icons": {}, "show_copy_icon": False, "show_timestamp": False + "user": " ", + "reaction_icons": {}, + "show_copy_icon": False, + "show_timestamp": False, + "show_edit_icon": False }, doc=""" Params to pass to the placeholder ChatMessage, like `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, `show_timestamp`. @@ -425,7 +429,7 @@ def _build_message( user = message_params.get("user", "") message_params["show_edit_icon"] = ( bool(self.edit_callback) and - user.lower() not in (self.callback_user.lower(), "help", " ") + user.lower() not in (self.callback_user.lower(), "help") ) message = ChatMessage(**message_params) From eabaf0e6d115b63d36c2e943e3d90ede240c7d30 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:23:26 -0800 Subject: [PATCH 8/8] Update panel/chat/interface.py --- panel/chat/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/chat/interface.py b/panel/chat/interface.py index 11f3e431a9..0acbe44188 100644 --- a/panel/chat/interface.py +++ b/panel/chat/interface.py @@ -740,5 +740,5 @@ def stream( # so only set to the default when not a ChatMessage user = user or self.user avatar = avatar or self.avatar - message_params["show_edit_icon"] = message_params.get("show_edit_icon", user == self.user) + message_params["show_edit_icon"] = message_params.get("show_edit_icon", user == self.user and message_params.get("edit_callback")) return super().stream(value, user=user, avatar=avatar, message=message, replace=replace, **message_params)