1717)
1818from weakref import WeakValueDictionary
1919
20- from htmltools import HTML , Tag , TagAttrValue , css
20+ from htmltools import HTML , Tag , TagAttrValue , TagList , css
2121
2222from .. import _utils , reactive
2323from .._deprecated import warn_deprecated
@@ -493,7 +493,12 @@ def messages(
493493
494494 return tuple (res )
495495
496- async def append_message (self , message : Any ) -> None :
496+ async def append_message (
497+ self ,
498+ message : Any ,
499+ * ,
500+ icon : HTML | Tag | TagList | None = None ,
501+ ):
497502 """
498503 Append a message to the chat.
499504
@@ -506,10 +511,14 @@ async def append_message(self, message: Any) -> None:
506511 Content strings are interpreted as markdown and rendered to HTML on the
507512 client. Content may also include specially formatted **input suggestion**
508513 links (see note below).
514+ icon
515+ An optional icon to display next to the message, currently only used for
516+ assistant messages. The icon can be any HTML element (e.g., an
517+ :func:`~shiny.ui.img` tag) or a string of HTML.
509518
510519 Note
511520 ----
512- `` {.callout-note title="Input suggestions"}
521+ ::: {.callout-note title="Input suggestions"}
513522 Input suggestions are special links that send text to the user input box when
514523 clicked (or accessed via keyboard). They can be created in the following ways:
515524
@@ -528,17 +537,22 @@ async def append_message(self, message: Any) -> None:
528537
529538 Note that a user may also opt-out of submitting a suggestion by holding the
530539 `Alt/Option` key while clicking the suggestion link.
531- ```
540+ :::
532541
533- ``` {.callout-note title="Streamed messages"}
542+ ::: {.callout-note title="Streamed messages"}
534543 Use `.append_message_stream()` instead of this method when `stream=True` (or
535544 similar) is specified in model's completion method.
536- ```
545+ :::
537546 """
538- await self ._append_message (message )
547+ await self ._append_message (message , icon = icon )
539548
540549 async def _append_message (
541- self , message : Any , * , chunk : ChunkOption = False , stream_id : str | None = None
550+ self ,
551+ message : Any ,
552+ * ,
553+ chunk : ChunkOption = False ,
554+ stream_id : str | None = None ,
555+ icon : HTML | Tag | TagList | None = None ,
542556 ) -> None :
543557 # If currently we're in a stream, handle other messages (outside the stream) later
544558 if not self ._can_append_message (stream_id ):
@@ -568,9 +582,18 @@ async def _append_message(
568582 if msg is None :
569583 return
570584 self ._store_message (msg , chunk = chunk )
571- await self ._send_append_message (msg , chunk = chunk )
585+ await self ._send_append_message (
586+ msg ,
587+ chunk = chunk ,
588+ icon = icon ,
589+ )
572590
573- async def append_message_stream (self , message : Iterable [Any ] | AsyncIterable [Any ]):
591+ async def append_message_stream (
592+ self ,
593+ message : Iterable [Any ] | AsyncIterable [Any ],
594+ * ,
595+ icon : HTML | Tag | None = None ,
596+ ):
574597 """
575598 Append a message as a stream of message chunks.
576599
@@ -583,6 +606,10 @@ async def append_message_stream(self, message: Iterable[Any] | AsyncIterable[Any
583606 OpenAI, Anthropic, Ollama, and others. Content strings are interpreted as
584607 markdown and rendered to HTML on the client. Content may also include
585608 specially formatted **input suggestion** links (see note below).
609+ icon
610+ An optional icon to display next to the message, currently only used for
611+ assistant messages. The icon can be any HTML element (e.g., an
612+ :func:`~shiny.ui.img` tag) or a string of HTML.
586613
587614 Note
588615 ----
@@ -625,7 +652,7 @@ async def append_message_stream(self, message: Iterable[Any] | AsyncIterable[Any
625652 # Run the stream in the background to get non-blocking behavior
626653 @reactive .extended_task
627654 async def _stream_task ():
628- return await self ._append_message_stream (message )
655+ return await self ._append_message_stream (message , icon = icon )
629656
630657 _stream_task ()
631658
@@ -669,11 +696,15 @@ def get_latest_stream_result(self) -> str | None:
669696 else :
670697 return stream .result ()
671698
672- async def _append_message_stream (self , message : AsyncIterable [Any ]):
699+ async def _append_message_stream (
700+ self ,
701+ message : AsyncIterable [Any ],
702+ icon : HTML | Tag | None = None ,
703+ ):
673704 id = _utils .private_random_id ()
674705
675706 empty = ChatMessage (content = "" , role = "assistant" )
676- await self ._append_message (empty , chunk = "start" , stream_id = id )
707+ await self ._append_message (empty , chunk = "start" , stream_id = id , icon = icon )
677708
678709 try :
679710 async for msg in message :
@@ -702,6 +733,7 @@ async def _send_append_message(
702733 self ,
703734 message : TransformedMessage ,
704735 chunk : ChunkOption = False ,
736+ icon : HTML | Tag | TagList | None = None ,
705737 ):
706738 if message ["role" ] == "system" :
707739 # System messages are not displayed in the UI
@@ -721,13 +753,17 @@ async def _send_append_message(
721753 content = message ["content_client" ]
722754 content_type = "html" if isinstance (content , HTML ) else "markdown"
723755
756+ # TODO: pass along dependencies for both content and icon (if any)
724757 msg = ClientMessage (
725758 content = str (content ),
726759 role = message ["role" ],
727760 content_type = content_type ,
728761 chunk_type = chunk_type ,
729762 )
730763
764+ if icon is not None :
765+ msg ["icon" ] = str (icon )
766+
731767 # print(msg)
732768
733769 await self ._send_custom_message (msg_type , msg )
@@ -1118,7 +1154,6 @@ async def _send_custom_message(self, handler: str, obj: ClientMessage | None):
11181154
11191155@add_example (ex_dir = "../templates/chat/starters/hello" )
11201156class ChatExpress (Chat ):
1121-
11221157 def ui (
11231158 self ,
11241159 * ,
@@ -1127,6 +1162,7 @@ def ui(
11271162 width : CssUnit = "min(680px, 100%)" ,
11281163 height : CssUnit = "auto" ,
11291164 fill : bool = True ,
1165+ icon_assistant : HTML | Tag | TagList | None = None ,
11301166 ** kwargs : TagAttrValue ,
11311167 ) -> Tag :
11321168 """
@@ -1148,6 +1184,10 @@ def ui(
11481184 fill
11491185 Whether the chat should vertically take available space inside a fillable
11501186 container.
1187+ icon_assistant
1188+ The icon to use for the assistant chat messages. Can be a HTML or a tag in
1189+ the form of :class:`~htmltools.HTML` or :class:`~htmltools.Tag`. If `None`,
1190+ a default robot icon is used.
11511191 kwargs
11521192 Additional attributes for the chat container element.
11531193 """
@@ -1158,6 +1198,7 @@ def ui(
11581198 width = width ,
11591199 height = height ,
11601200 fill = fill ,
1201+ icon_assistant = icon_assistant ,
11611202 ** kwargs ,
11621203 )
11631204
@@ -1171,6 +1212,7 @@ def chat_ui(
11711212 width : CssUnit = "min(680px, 100%)" ,
11721213 height : CssUnit = "auto" ,
11731214 fill : bool = True ,
1215+ icon_assistant : HTML | Tag | TagList | None = None ,
11741216 ** kwargs : TagAttrValue ,
11751217) -> Tag :
11761218 """
@@ -1199,6 +1241,10 @@ def chat_ui(
11991241 The height of the chat container.
12001242 fill
12011243 Whether the chat should vertically take available space inside a fillable container.
1244+ icon_assistant
1245+ The icon to use for the assistant chat messages. Can be a HTML or a tag in
1246+ the form of :class:`~htmltools.HTML` or :class:`~htmltools.Tag`. If `None`,
1247+ a default robot icon is used.
12021248 kwargs
12031249 Additional attributes for the chat container element.
12041250 """
@@ -1226,6 +1272,10 @@ def chat_ui(
12261272
12271273 message_tags .append (Tag (tag_name , content = msg ["content" ]))
12281274
1275+ html_deps = None
1276+ if isinstance (icon_assistant , (Tag , TagList )):
1277+ html_deps = icon_assistant .get_dependencies ()
1278+
12291279 res = Tag (
12301280 "shiny-chat-container" ,
12311281 Tag ("shiny-chat-messages" , * message_tags ),
@@ -1235,6 +1285,7 @@ def chat_ui(
12351285 placeholder = placeholder ,
12361286 ),
12371287 chat_deps (),
1288+ html_deps ,
12381289 {
12391290 "style" : css (
12401291 width = as_css_unit (width ),
@@ -1244,6 +1295,7 @@ def chat_ui(
12441295 id = id ,
12451296 placeholder = placeholder ,
12461297 fill = fill ,
1298+ icon_assistant = str (icon_assistant ) if icon_assistant is not None else None ,
12471299 ** kwargs ,
12481300 )
12491301
0 commit comments