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..b38163877c 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,20 @@ 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} + + # 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: + 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..877d76ba04 100644 --- a/panel/tests/chat/test_feed.py +++ b/panel/tests/chat/test_feed.py @@ -94,6 +94,42 @@ async def test_send_with_user_avatar(self, chat_feed): assert message.user == user assert message.avatar == avatar + async def test_user_messages_styles(self, chat_feed): + chat_feed.user_messages_styles = { + "Bob": {"background": "red", "color": "white"}, + "Alice": {"background": "blue", "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" + + async def test_user_messages_stylesheets(self, chat_feed): + chat_feed.user_messages_stylesheets = { + "Bob": ["bob.css"], + "Alice": ["alice.css", "common.css"], + } + 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"] + + 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 + + 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") + 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": "👨"}) assert len(chat_feed.objects) == 1