From fe38c2e702a495c2aaae85795693d5a6cf6b49fe Mon Sep 17 00:00:00 2001 From: J01024 Date: Thu, 22 May 2025 16:18:50 +0100 Subject: [PATCH 1/5] usr messages align v1 --- examples/reference/chat/ChatFeed.ipynb | 60 ++++++++++++++++++++++++++ panel/chat/feed.py | 20 +++++++++ panel/tests/chat/test_feed.py | 39 ++++++++++++++++- 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb index 2457e40c5f..c79621d6db 100644 --- a/examples/reference/chat/ChatFeed.ipynb +++ b/examples/reference/chat/ChatFeed.ipynb @@ -62,6 +62,8 @@ "* **`load_buffer`** (int): The number of objects loaded on each side of the visible objects. When scrolled halfway into the buffer, the feed will automatically load additional objects while unloading objects on the opposite side.\n", "* **`show_activity_dot`** (bool): Whether to show an activity dot on the ChatMessage while streaming the callback response.\n", "* **`view_latest`** (bool): Whether to scroll to the latest object on init. If not enabled the view will be on the first object. Defaults to True.\n", + "* **`user_messages_styles`** (dict): A dictionary mapping the user name to styles to pass to their message objects. \n", + "* **`user_messages_stylesheets`** (dict): A dictionary mapping the user name to stylesheets to pass to their message objects.\n", "\n", "#### Methods\n", "\n", @@ -1315,6 +1317,64 @@ "chat_feed" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Customisation per user: Per-user styles and stylesheets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can now easily customise the appearance of messages for each user individually using the `user_messages_styles` and `user_messages_stylesheets` parameters.\n", + "`user_messages_styles` lets you provide a dictionary mapping user names to a dictionary of CSS style properties.\n", + "`user_messages_stylesheets` lets you provide a dictionary mapping user names to a list of CSS stylesheets that will be applied only to that user's messages.\n", + "This gives you fine-grained control over the look and feel of each user's messages, such as background colour, text colour, font, or even custom CSS rules.\n", + "\n", + "**Example: Per-user styles**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chat_feed = pn.chat.ChatFeed()\n", + "chat_feed.user_messages_styles = {\n", + " 'Alice': {'background': 'lavender', 'color': 'navy'},\n", + " 'Bob': {'background': 'mintcream', 'color': 'darkgreen', 'fontWeight': 'bold'},\n", + "}\n", + "chat_feed.send('Hello from Alice!', user='Alice')\n", + "chat_feed.send('Hi from Bob!', user='Bob')\n", + "chat_feed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Example: Per-user stylesheets**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chat_feed = pn.chat.ChatFeed()\n", + "chat_feed.user_messages_stylesheets = {\n", + " 'Alice': [\".message { border: 2px solid purple; border-radius: 8px; }\"],\n", + " 'Bob': [\".message { box-shadow: 0 0 8px #0a0; }\"],\n", + "}\n", + "chat_feed.send('Alice with a border', user='Alice')\n", + "chat_feed.send('Bob with a shadow', user='Bob')\n", + "chat_feed" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/panel/chat/feed.py b/panel/chat/feed.py index 49f2e41723..c11c74bc8b 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -215,6 +215,16 @@ class ChatFeed(ListPanel): Whether to scroll to the latest object on init. If not enabled the view will be on the first object.""") + user_messages_styles = param.Dict(default={}, doc=""" + A dictionary mapping user names to a dict of CSS style properties. + E.g. {"User": {"background": "lightblue"}, "Assistant": {"background": "lightgrey"}} + """) + + user_messages_stylesheets = param.Dict(default={}, doc=""" + A dictionary mapping user names to a list of CSS stylesheets to apply to messages by that user only. + E.g. {"User": [".message.user"], "Assistant": [".message {background: lightgrey;}"]} + """) + _placeholder = param.ClassSelector(class_=ChatMessage, allow_refs=False, doc=""" The placeholder wrapped in a ChatMessage object; primarily to prevent recursion error in _update_placeholder.""") @@ -437,6 +447,16 @@ def _build_message( message_params["width"] = int(self.width - 80) message_params.update(input_message_params) + # Apply per-user styles + user_styles = self.user_messages_styles.get(user, {}) + if user_styles: + message_params["styles"] = {**message_params.get("styles", {}), **user_styles} + + # Apply per-user stylesheets + user_stylesheets = self.user_messages_stylesheets.get(user, []) + if user_stylesheets: + message_params["stylesheets"].extend(user_stylesheets) + if "show_edit_icon" not in message_params: user = message_params.get("user", "") message_params["show_edit_icon"] = ( diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index dd6fee4026..171b2b8453 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -94,6 +94,43 @@ async def test_send_with_user_avatar(self, chat_feed): assert message.user == user assert message.avatar == avatar + def test_user_messages_styles(self, chat_feed): + chat_feed.user_messages_styles = { + "Bob": {"background": "red", "color": "white"}, + "Alice": {"background": "blue", "color": "yellow"}, + } + msg_bob = chat_feed.send("Hi", user="Bob") + msg_alice = chat_feed.send("Hello", user="Alice") + assert msg_bob.styles["background"] == "red" + assert msg_bob.styles["color"] == "white" + assert msg_alice.styles["background"] == "blue" + assert msg_alice.styles["color"] == "yellow" + + def test_user_messages_stylesheets(self, chat_feed): + chat_feed.user_messages_stylesheets = { + "Bob": ["bob.css"], + "Alice": ["alice.css", "common.css"], + } + msg_bob = chat_feed.send("Hi", user="Bob") + msg_alice = chat_feed.send("Hello", user="Alice") + assert "bob.css" in msg_bob.stylesheets + assert "alice.css" in msg_alice.stylesheets + assert "common.css" in msg_alice.stylesheets + + def test_user_messages_styles_and_stylesheets_absent(self, chat_feed): + chat_feed.user_messages_styles = {"Bob": {"background": "red"}} + chat_feed.user_messages_stylesheets = {"Bob": ["bob.css"]} + msg = chat_feed.send("Hi", user="Charlie") + assert "background" not in msg.styles + assert not msg.stylesheets + + def test_user_messages_styles_and_stylesheets_together(self, chat_feed): + chat_feed.user_messages_styles = {"Bob": {"background": "red"}} + chat_feed.user_messages_stylesheets = {"Bob": ["bob.css"]} + msg = chat_feed.send("Hi", user="Bob") + assert msg.styles["background"] == "red" + assert "bob.css" in msg.stylesheets + async def test_send_dict(self, chat_feed): message = chat_feed.send({"object": "Message", "user": "Bob", "avatar": "👨"}) assert len(chat_feed.objects) == 1 @@ -1603,7 +1640,7 @@ async def callback(contents, user, instance): message += char yield message - async def append_callback(message, instance): + def append_callback(message, instance): logs.append(message.object) logs = [] From 891d87cb5c1c9d4cc6482e0ea982784add469f12 Mon Sep 17 00:00:00 2001 From: J01024 Date: Thu, 22 May 2025 16:30:21 +0100 Subject: [PATCH 2/5] revert test_feed.py accidental change --- panel/tests/chat/test_feed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 171b2b8453..a9dfb4303f 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -1640,7 +1640,7 @@ async def callback(contents, user, instance): message += char yield message - def append_callback(message, instance): + async def append_callback(message, instance): logs.append(message.object) logs = [] From bff9f84e1c462f4046651d509fd7e06f049c06c7 Mon Sep 17 00:00:00 2001 From: J01024 Date: Thu, 22 May 2025 16:37:36 +0100 Subject: [PATCH 3/5] ensure stylesheets exists in message_params first. --- panel/chat/feed.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/panel/chat/feed.py b/panel/chat/feed.py index c11c74bc8b..b38163877c 100644 --- a/panel/chat/feed.py +++ b/panel/chat/feed.py @@ -452,6 +452,10 @@ def _build_message( if user_styles: message_params["styles"] = {**message_params.get("styles", {}), **user_styles} + # Ensure stylesheets key exists and is a list + if "stylesheets" not in message_params or not isinstance(message_params["stylesheets"], list): + message_params["stylesheets"] = [] + # Apply per-user stylesheets user_stylesheets = self.user_messages_stylesheets.get(user, []) if user_stylesheets: From cf69ac6a1e1b9a3c53bfcf44d08cee66ddeb32ce Mon Sep 17 00:00:00 2001 From: J01024 Date: Thu, 22 May 2025 16:44:26 +0100 Subject: [PATCH 4/5] tests unclosed socket --- panel/tests/chat/test_feed.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index a9dfb4303f..8f4d9f824c 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -99,37 +99,36 @@ def test_user_messages_styles(self, chat_feed): "Bob": {"background": "red", "color": "white"}, "Alice": {"background": "blue", "color": "yellow"}, } - msg_bob = chat_feed.send("Hi", user="Bob") - msg_alice = chat_feed.send("Hello", user="Alice") - assert msg_bob.styles["background"] == "red" - assert msg_bob.styles["color"] == "white" - assert msg_alice.styles["background"] == "blue" - assert msg_alice.styles["color"] == "yellow" + chat_feed.send("Hi", user="Bob") + chat_feed.send("Hello", user="Alice") + assert chat_feed.objects[0].styles["background"] == "red" + assert chat_feed.objects[0].styles["color"] == "white" + assert chat_feed.objects[1].styles["background"] == "blue" + assert chat_feed.objects[1].styles["color"] == "yellow" def test_user_messages_stylesheets(self, chat_feed): chat_feed.user_messages_stylesheets = { "Bob": ["bob.css"], "Alice": ["alice.css", "common.css"], } - msg_bob = chat_feed.send("Hi", user="Bob") - msg_alice = chat_feed.send("Hello", user="Alice") - assert "bob.css" in msg_bob.stylesheets - assert "alice.css" in msg_alice.stylesheets - assert "common.css" in msg_alice.stylesheets + chat_feed.send("Hi", user="Bob") + chat_feed.send("Hello", user="Alice") + assert chat_feed.objects[0].stylesheets == ["bob.css"] + assert chat_feed.objects[1].stylesheets == ["alice.css", "common.css"] def test_user_messages_styles_and_stylesheets_absent(self, chat_feed): chat_feed.user_messages_styles = {"Bob": {"background": "red"}} chat_feed.user_messages_stylesheets = {"Bob": ["bob.css"]} - msg = chat_feed.send("Hi", user="Charlie") - assert "background" not in msg.styles - assert not msg.stylesheets + chat_feed.send("Hi", user="Charlie") + assert "background" not in chat_feed.objects[0].styles + assert not chat_feed.objects[0].stylesheets def test_user_messages_styles_and_stylesheets_together(self, chat_feed): chat_feed.user_messages_styles = {"Bob": {"background": "red"}} chat_feed.user_messages_stylesheets = {"Bob": ["bob.css"]} - msg = chat_feed.send("Hi", user="Bob") - assert msg.styles["background"] == "red" - assert "bob.css" in msg.stylesheets + chat_feed.send("Hi", user="Bob") + assert chat_feed.objects[0].styles["background"] == "red" + assert chat_feed.objects[0].stylesheets == ["bob.css"] async def test_send_dict(self, chat_feed): message = chat_feed.send({"object": "Message", "user": "Bob", "avatar": "👨"}) From 1889f673a6b0f4f67fda67b0154b369236c0baf3 Mon Sep 17 00:00:00 2001 From: J01024 Date: Thu, 22 May 2025 17:03:50 +0100 Subject: [PATCH 5/5] make tests async --- panel/tests/chat/test_feed.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/panel/tests/chat/test_feed.py b/panel/tests/chat/test_feed.py index 8f4d9f824c..877d76ba04 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -94,7 +94,7 @@ async def test_send_with_user_avatar(self, chat_feed): assert message.user == user assert message.avatar == avatar - def test_user_messages_styles(self, chat_feed): + async def test_user_messages_styles(self, chat_feed): chat_feed.user_messages_styles = { "Bob": {"background": "red", "color": "white"}, "Alice": {"background": "blue", "color": "yellow"}, @@ -106,7 +106,7 @@ def test_user_messages_styles(self, chat_feed): assert chat_feed.objects[1].styles["background"] == "blue" assert chat_feed.objects[1].styles["color"] == "yellow" - def test_user_messages_stylesheets(self, chat_feed): + async def test_user_messages_stylesheets(self, chat_feed): chat_feed.user_messages_stylesheets = { "Bob": ["bob.css"], "Alice": ["alice.css", "common.css"], @@ -116,14 +116,14 @@ def test_user_messages_stylesheets(self, chat_feed): assert chat_feed.objects[0].stylesheets == ["bob.css"] assert chat_feed.objects[1].stylesheets == ["alice.css", "common.css"] - def test_user_messages_styles_and_stylesheets_absent(self, chat_feed): + async def test_user_messages_styles_and_stylesheets_absent(self, chat_feed): chat_feed.user_messages_styles = {"Bob": {"background": "red"}} chat_feed.user_messages_stylesheets = {"Bob": ["bob.css"]} chat_feed.send("Hi", user="Charlie") assert "background" not in chat_feed.objects[0].styles assert not chat_feed.objects[0].stylesheets - def test_user_messages_styles_and_stylesheets_together(self, chat_feed): + async def test_user_messages_styles_and_stylesheets_together(self, chat_feed): chat_feed.user_messages_styles = {"Bob": {"background": "red"}} chat_feed.user_messages_stylesheets = {"Bob": ["bob.css"]} chat_feed.send("Hi", user="Bob")