From 32a6dadc7e1b796f7d8d1effff5cb98130ceb3b2 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 5 Aug 2025 16:59:56 -0500 Subject: [PATCH 1/9] Quick and dirty 1st pass at custom icons in R --- pkg-r/R/chat.R | 34 ++++++++++++++++++++++++++++---- pkg-r/man/chat_append.Rd | 5 +++++ pkg-r/man/chat_append_message.Rd | 5 +++++ pkg-r/man/chat_ui.Rd | 7 ++++++- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/pkg-r/R/chat.R b/pkg-r/R/chat.R index 8237e8a9..36c70b76 100644 --- a/pkg-r/R/chat.R +++ b/pkg-r/R/chat.R @@ -54,6 +54,9 @@ chat_deps <- function() { #' @param fill Whether the chat element should try to vertically fill its #' container, if the container is #' [fillable](https://rstudio.github.io/bslib/articles/filling/index.html) +#' @param icon_assistant The icon to use for the assistant chat messages. +#' Can be HTML or a tag in the form of [htmltools::HTML()] or +#' [htmltools::tags()]. If `None`, a default robot icon is used. #' @returns A Shiny tag object, suitable for inclusion in a Shiny UI #' #' @examplesIf interactive() @@ -90,7 +93,8 @@ chat_ui <- function( placeholder = "Enter a message...", width = "min(680px, 100%)", height = "auto", - fill = TRUE + fill = TRUE, + icon_assistant = NULL ) { attrs <- rlang::list2(...) if (!all(nzchar(rlang::names2(attrs)))) { @@ -123,6 +127,7 @@ chat_ui <- function( tag_name, rlang::list2( content = ui[["html"]], + icon = if (!is.null(icon_assistant)) as.character(icon_assistant), ui[["dependencies"]], ) ) @@ -138,13 +143,19 @@ chat_ui <- function( ), placeholder = placeholder, fill = if (isTRUE(fill)) NA else NULL, + # Also include icon on the parent so that when messages are dynamically added, + # we know the default icon has changed + icon_assistant = if (!is.null(icon_assistant)) { + as.character(icon_assistant) + }, ..., tag("shiny-chat-messages", message_tags), tag( "shiny-chat-input", list(id = paste0(id, "_user_input"), placeholder = placeholder) ), - chat_deps() + chat_deps(), + htmltools::findDependencies(icon_assistant) ) ) @@ -195,6 +206,9 @@ chat_ui <- function( #' #' @param role The role of the message (either "assistant" or "user"). Defaults #' to "assistant". +#' @param icon An optional icon to display next to the message, currently only +#' used for assistant messages. The icon can be any HTML element (e.g., an +#' [htmltools::img()] tag) or a string of HTML. #' @param session The Shiny session object #' #' @returns Returns a promise that resolves to the contents of the stream, or an @@ -246,13 +260,14 @@ chat_append <- function( id, response, role = c("assistant", "user"), + icon = NULL, session = getDefaultReactiveDomain() ) { check_active_session(session) role <- match.arg(role) stream <- as_generator(response) - chat_append_stream(id, stream, role = role, session = session) + chat_append_stream(id, stream, role = role, icon = icon, session = session) } #' Low-level function to append a message to a chat control @@ -274,6 +289,9 @@ chat_append <- function( #' then the new content is appended to the existing message content. If #' `"replace"`, then the existing message content is replaced by the new #' content. Ignored if `chunk` is `FALSE`. +#' @param icon An optional icon to display next to the message, currently only +#' used for assistant messages. The icon can be any HTML element (e.g., +#' [htmltools::img()] tag) or a string of HTML. #' @param session The Shiny session object #' #' @returns Returns nothing (\code{invisible(NULL)}). @@ -329,6 +347,7 @@ chat_append_message <- function( msg, chunk = TRUE, operation = c("append", "replace"), + icon = NULL, session = getDefaultReactiveDomain() ) { check_active_session(session) @@ -389,6 +408,10 @@ chat_append_message <- function( operation = operation ) + if (!is.null(icon)) { + msg$icon <- as.character(icon) + } + session$sendCustomMessage( "shinyChatMessage", list( @@ -405,9 +428,10 @@ chat_append_stream <- function( id, stream, role = "assistant", + icon = NULL, session = getDefaultReactiveDomain() ) { - result <- chat_append_stream_impl(id, stream, role, session) + result <- chat_append_stream_impl(id, stream, role, icon, session) result <- chat_update_bookmark(id, result, session = session) # Handle erroneous result... result <- promises::catch(result, function(reason) { @@ -453,12 +477,14 @@ rlang::on_load( id, stream, role = "assistant", + icon = NULL, session = shiny::getDefaultReactiveDomain() ) { chat_append_message( id, list(role = role, content = ""), chunk = "start", + icon = icon, session = session ) diff --git a/pkg-r/man/chat_append.Rd b/pkg-r/man/chat_append.Rd index 1c92eb56..0c51067e 100644 --- a/pkg-r/man/chat_append.Rd +++ b/pkg-r/man/chat_append.Rd @@ -8,6 +8,7 @@ chat_append( id, response, role = c("assistant", "user"), + icon = NULL, session = getDefaultReactiveDomain() ) } @@ -34,6 +35,10 @@ interpreted as markdown as long as they're not inside HTML. \item{role}{The role of the message (either "assistant" or "user"). Defaults to "assistant".} +\item{icon}{An optional icon to display next to the message, currently only +used for assistant messages. The icon can be any HTML element (e.g., an +\code{\link[htmltools:builder]{htmltools::img()}} tag) or a string of HTML.} + \item{session}{The Shiny session object} } \value{ diff --git a/pkg-r/man/chat_append_message.Rd b/pkg-r/man/chat_append_message.Rd index 27ebabd1..e55bd859 100644 --- a/pkg-r/man/chat_append_message.Rd +++ b/pkg-r/man/chat_append_message.Rd @@ -9,6 +9,7 @@ chat_append_message( msg, chunk = TRUE, operation = c("append", "replace"), + icon = NULL, session = getDefaultReactiveDomain() ) } @@ -31,6 +32,10 @@ then the new content is appended to the existing message content. If \code{"replace"}, then the existing message content is replaced by the new content. Ignored if \code{chunk} is \code{FALSE}.} +\item{icon}{An optional icon to display next to the message, currently only +used for assistant messages. The icon can be any HTML element (e.g., +\code{\link[htmltools:builder]{htmltools::img()}} tag) or a string of HTML.} + \item{session}{The Shiny session object} } \value{ diff --git a/pkg-r/man/chat_ui.Rd b/pkg-r/man/chat_ui.Rd index 0c28ef49..2ccf633f 100644 --- a/pkg-r/man/chat_ui.Rd +++ b/pkg-r/man/chat_ui.Rd @@ -11,7 +11,8 @@ chat_ui( placeholder = "Enter a message...", width = "min(680px, 100\%)", height = "auto", - fill = TRUE + fill = TRUE, + icon_assistant = NULL ) } \arguments{ @@ -47,6 +48,10 @@ as described above, and the \code{role} can be "assistant" or "user". \item{fill}{Whether the chat element should try to vertically fill its container, if the container is \href{https://rstudio.github.io/bslib/articles/filling/index.html}{fillable}} + +\item{icon_assistant}{The icon to use for the assistant chat messages. +Can be HTML or a tag in the form of \code{\link[htmltools:HTML]{htmltools::HTML()}} or +\code{\link[htmltools:builder]{htmltools::tags()}}. If \code{None}, a default robot icon is used.} } \value{ A Shiny tag object, suitable for inclusion in a Shiny UI From f05bca45a2170377ac857d6744ff721c6c4e6bdf Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 6 Aug 2025 09:44:03 -0500 Subject: [PATCH 2/9] Update news --- pkg-r/NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index 5a12d946..68cac713 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -6,6 +6,8 @@ * Added `update_chat_user_input()` for programmatically updating the user input of a chat UI element. (#78) +* Added `chat_append(icon=...)` and `chat_ui(icon_assistant=...)` for customizing the icon that appears next to assistant responses. (#88) + ## Improvements * `chat_app()` now correctly restores the chat client state when refreshing the app, e.g. by reloading the page. (#71) From 13ffce3aaa7035f75e5974254612226f3a2e4adf Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 22 Aug 2025 11:17:38 -0400 Subject: [PATCH 3/9] fix(chat_append_stream): pass `...` through so `icon` is sent --- pkg-r/R/chat.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg-r/R/chat.R b/pkg-r/R/chat.R index 85fa7fb4..37b54b29 100644 --- a/pkg-r/R/chat.R +++ b/pkg-r/R/chat.R @@ -502,7 +502,8 @@ rlang::on_load( msg = list(role = role, content = content), operation = "append", chunk = chunk, - session = session + session = session, + ... ) } From 89f86324634e6d002cdc1242b2f209c2866895aa Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 22 Aug 2025 11:17:56 -0400 Subject: [PATCH 4/9] tests: Add example test app --- pkg-r/tests/testthat/apps/icon/app.R | 165 ++++++++++++++++++ .../testthat/apps/icon/img/grace-hopper.jpg | Bin 0 -> 8946 bytes pkg-r/tests/testthat/apps/icon/img/shiny.png | Bin 0 -> 9672 bytes 3 files changed, 165 insertions(+) create mode 100644 pkg-r/tests/testthat/apps/icon/app.R create mode 100644 pkg-r/tests/testthat/apps/icon/img/grace-hopper.jpg create mode 100644 pkg-r/tests/testthat/apps/icon/img/shiny.png diff --git a/pkg-r/tests/testthat/apps/icon/app.R b/pkg-r/tests/testthat/apps/icon/app.R new file mode 100644 index 00000000..7ea07e30 --- /dev/null +++ b/pkg-r/tests/testthat/apps/icon/app.R @@ -0,0 +1,165 @@ +library(shiny) +library(bslib) +pkgload::load_all() +library(shinychat) +library(bsicons) + +ui <- page_fillable( + title = "Chat Icons", + + layout_columns( + # Default Bot ---- + div( + h2("Default Bot"), + chat_ui( + id = "chat_default", + messages = list( + list( + content = "Hello! I'm Default Bot. How can I help you today?", + role = "assistant" + ) + ), + icon_assistant = NULL # Uses default robot icon + ) + ), + + # Animal Bot ---- + div( + h2("Animal Bot"), + chat_ui( + id = "chat_animal", + messages = list("Hello! I'm Animal Bot. How can I help you today?"), + icon_assistant = fontawesome::fa("otter", ) + ), + selectInput( + "animal", + "Animal", + choices = c("Otter", "Hippo", "Frog", "Dove"), + selected = "Otter" + ) + ), + + # SVG Bot ---- + div( + h2("SVG Bot"), + chat_ui( + id = "chat_svg", + messages = list("Hello! I'm SVG Bot. How can I help you today?"), + icon_assistant = HTML( + ' + + + + ' + ) + ) + ), + + # Image Bot ---- + div( + h2("Image Bot"), + chat_ui( + id = "chat_image", + messages = list("Hello! I'm Image Bot. How can I help you today?"), + icon_assistant = img( + src = "img/grace-hopper.jpg", + class = "icon-image grace-hopper" + ) + ), + selectInput( + "image", + "Image", + choices = c("Grace Hopper", "Shiny"), + selected = "Grace Hopper" + ) + ) + ) +) + +server <- function(input, output, session) { + # Add resource path for images + addResourcePath("img", "img") + + # Default Bot ---- + observeEvent(input$chat_default_user_input, { + req(input$chat_default_user_input) + + # Simulate delay + Sys.sleep(1) + + chat_append( + "chat_default", + paste0("You said: ", input$chat_default_user_input) + ) + }) + + # Animal Bot ---- + observeEvent(input$chat_animal_user_input, { + req(input$chat_animal_user_input) + + # Simulate delay + Sys.sleep(1) + + animal <- tolower(input$animal) + + # Create icon based on selection + if (animal == "otter") { + # Use default icon (NULL) + icon <- NULL + } else { + icon_map <- list( + "hippo" = "hippo", + "frog" = "frog", + "dove" = "dove" + ) + + if (animal %in% names(icon_map)) { + icon <- fontawesome::fa( + icon_map[[animal]], + # fontawesome doesn't support `class` argument, so we use `title` + title = paste0("icon-", animal) + ) + } else { + icon <- NULL + } + } + + chat_append( + "chat_animal", + paste0(animal, " said: ", input$chat_animal_user_input), + icon = icon + ) + }) + + # SVG Bot ---- + observeEvent(input$chat_svg_user_input, { + req(input$chat_svg_user_input) + + chat_append( + "chat_svg", + paste0("You said: ", input$chat_svg_user_input) + ) + }) + + # Image Bot ---- + observeEvent(input$chat_image_user_input, { + req(input$chat_image_user_input) + + # Create icon based on selection + icon <- NULL + if (input$image == "Shiny") { + icon <- img( + src = "img/shiny.png", + class = "icon-image shiny" + ) + } + + chat_append( + "chat_image", + paste0("You said: ", input$chat_image_user_input), + icon = icon + ) + }) +} + +shinyApp(ui, server) diff --git a/pkg-r/tests/testthat/apps/icon/img/grace-hopper.jpg b/pkg-r/tests/testthat/apps/icon/img/grace-hopper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b09e239ad44111a92f750a883b2324fd93ddc57f GIT binary patch literal 8946 zcmbVxcUV)+w)dtZNRcWv6cLmXq$({aND&Z31q7tG7(%ELAP`0AAc6vlC0Gy@HPRIk zq$`N@77$2i(j);5fwUX-J)V1>@BDF}ZzX&0$;_7Rvmasp>#Iv6aiH;680)xY%w0r{)et}y4 z2sjw&8xB6Cr40s5kVZ&&I6N@gR{;qR4T~~DnkxP7X2dH0ss<}5{O%GBGgWePa8j^9 zLLJ;A9+5cl;{x<&}p1;1kZ(+Aw369>cLo#iAD;h| z^{4hfRhWSPn>i+b%{Q`$3=Duqo1G1Vg&|_YOu+w7%RgiKNBI95_&>Ay+Yr{Yn(>){ z|GJ17A9D=|bNbf^D(Gry1BZ{ux>0nS%&fO?&9MY2H4p+IM_KkxLEeGS`vSSHRm2K5yeACxkXR; z@+d`!>0Hixz^i<$qFLOzpSVxgFEV)-pM>OIDQT7cs%i%g>ggL89zJ4ZVR_uj+Q#<8 z=`&|tT;0yO`?FjrI0SkjDmvz3EHW}To zvXhk0S!0_Y87K~07Ou=`k9Bk5#5Yf+a%NoWTFLo1w$F#%-0UW$Zg-;!vGjXRNqeJJ z7MIxCq*2*|`7H4n#4mTeaCR2kg2ufg2&GoP)fa%_F|EcPYf4j75S`=2E2oM*Mv@QR z8@zK~Ghey-zMD{ER40<(1>@EKy~g7<>KQH$KE?z9VcQzCNZBTt&=dsm0cWa`IhjCm zSke%r`c=VGCJ=O?xT7u@Kh3vS0$e?Lp-B470bjVNqiltu;a1x_6NFg!s|Ah)J+`PB zOdDt%M93b2h@-@1pkDijP?|Atv#QdbD8Pd{Y~vhZ1lrGajdODgbG;yauJS3i2a#!1 zmeZ(0J29l+G`@Lu7c6UYmRgvLm(fQ*k!q<;6f!M=)ON2VRr@1M0__M3`c_=AQ z93NlUx!7-7+R?M}CXhE|yuYC6d*vay`eg2$`=w&ffhBK$S`;yz@(Dsyfgj28%xV0R zrN{)%?>;3Zt+NXd)7-c}9ZiI-V#ZtM^T}qz68UCt7&dSFvzivV)!d+aM1Z^?X~?jRQGD<$(_iIT<1=2;yF(9AwYU;}~#^CVfP=ws6jO7VqLQ=0qF zt<7l{QUgAgC|iHp2p6;gsiVqXnZ)8RR=uDUb&k|udmEuLMEa5)bT6xRkKhs0OMB%m zEqQWvyfaDSZ6rR3;au3jb% zdeI*?A$rGQc!xRNe4B5K^e4KM?MaVPW&)4)Tv0fjb$Izf`pTO3qAjVsQ+Ua1*ZU#q8TaiT zF(rgX*O^ahp4+hQncz&uGGyjGj{Gc+x(j7N{C1zf`@#eghqVj7&|$*w{NxBWfM+YO zJh;2U*=%S=^vaxX3!%w&zv_qJBJiBkkbqA8vK6QWspXt4_BYC;u z0RE1qL3e9yMp--Op-yT|twW>x<*w&XTP|PXIl%_*ieb zM)s^MyyE+*HgV1M0SE_bL63o2;={^`^C)GKvG+R?yf%*$SMwe7ykkA$LPhJsgu_By z-U7RMO1@(Ja#F&TjJ=l_vTm~ z8El@Ga{Ck!ESq;iFITL&V7PiKgx|U_Zy)AhyM0D@)TSC_Kk|`ze>GY#r2dS5XfsaO ze8#H0I;n;lOf<%SP3y=_ZPcXSj(P7aAm8gb^fD~$1^9|*Lwrp(f()Z8=T2q>*dc1R zUpy5tvAVNx5!->zHj^4b$;3&f(N1xkgBdyexbEeCmvH2fPyWGt?laokC%Mn0%1Q%2 z)~^`UX%Z4Zyt+UPe^71RzDu0>cG4X>;lO0)SOOotf&~UngHi&i2IyvU6MYiTEGSFE z1dIm>Od>D8ijgUkAYVxOI3hvNrlwL0c@@3fhz-Rw9uimS`}W$~T)6ViDA4#lH*V)D zgAq>o2L5|j)e8-&W}=}MvObBEvrm9Eb5I3_=mMc=VN#kdtP4p_B z>TZlf-Ce>f5^EXX)e3C|1!uCCHvD<+eSOL?$q-^ih#3hXkNg>kU^YsE)9m8hwL9nU*2TL|Jo)ypChK(O08Y`!T58j$(1EH$QJW|yXU#OV3h|);N&>zqxk+%X z;&DiZhD2tk@!Xin_MHFll=JHM_^QR1Fq9MJXbfqc>@-}x&UF)xXmKsXwNJkOIK7X& z5%*ZF%Hll_!oAzUz~1e$oe?=qq56sQd?biV{KL?eeAs? zc7M}82m(Y=MXU`o0nU2Lky)7IxLu3ElnNy#JS%c)fB!43YwsVoDp|i3xOws=A{Yj_ zt(c%6?h31Xuo;N`mPVB{o2Nr(bFcJ1J*s~l(B|ASF?-!7Qpi5iddVC#UqeKbDU{(^I{~2lZhuLSJX8m%HKIwZ7K%5wTk(CYEnvCVecnhZ2;pyf63SdjbCr?P4xm(ab!kU~J=~ zL*t>^p>!!Z1XKT+G2+8-#S%3U7wQei^Tkd_w|Lj`m+ElVQa$$4DkSgPzbl3&#hmyl zbXA&_T((6~4U+mL$aV6v?*_p%u*%n2lUQ=bvpDY{Se(g+ijFTB6PR?R?Pk8oBXdxJ zmvK{l{h)}8Lsp=6mNKR8+U&&6XgPFBcjzZVsM(ibm-eQ3*b-V3@$PVQ!R2iXNbCDy z`3B6>qyy+?lb@{jtWj@DsF~7IeQ!o}T_&vD?xj|ubdMDK#})GU_b1SOhuA;O<9H`z zwVee}(=o(Hoe%*N(I-euXJJm+eyj-@3RkMPm!8zi()uKBH_sQ14oiE>vOpkC zg$bnTPm8Y9Kj<61CBb>ar?*JTd7|;|Eb%3F+#3J=5n+A85s$u7-FvXlFsqt+C!x?d z?;7U7kbzG4RA^J+q056>+Q|0E9mi|FMAbG}^AV*{mpZ%HQ2K~(&E7>>j=aH3u%%pz zYG<-4xI0 zU*6?I2{u6R)4o#hkniHY=RNGW98goA?5fCzcRmouxJd!LBPcX=byp!&h`O-e|@WE9{=WxT49busXtXwM| zjg!HH(v$cp3*%rtkOb5<{`}8(vZ-}%(_37Zqw>C4Utq9iFsO+$X)H;a!b3o$H!2US z!mw%Xr8TL=F}x}59nZvHcAs0W^eo#Mtt;-g%(Gt5BP3ZAm}BY5G-J|UY11cF7w+4~ z3b1>8KyKhqQ>&G!W7~I3=4Lk3w~tUPNb<`o1W*n`G-RiGL|$%~7D2FsK2mY0c9egQ zWCBpdN^_|Bz6V}1_lfxfvuW}B>;?$$wFlpu9gj`@(fk1BdH^01f_f(Idho<0Lm_{UX<}0@ozs;;VI?Yq z48K=;_jEu46Cv@I3oyFnqw4fhc}~rZP!m1fFutzveX`*V!RwC*_x#(;BnOjBJYU`; zzr%Z^YyPAdSchkS6Peq7V7QwkJ9RqQab39^=%}BTL-Nc_XC(=HqwMHKCOWkdNDcjL z_na5k@9BD0SEJQW4{2)cLsxtDmN_Wtw+*UEs!jBL;DLWV*8`jGe*0b&` zE=vuSd-(#l{7G+Gc=~uiRS{I#wV|dkI{F%<5Lt>0aJpz~4tj%mjqP+y#d24@@c6bR zx{NEf6Rh#~ha;kpTU**Pcc3@V0Q#Q4lhN)j<+7r(;&)QOeDeddl68vY`13u3n%$2GOdsnt2uTFT-Y@bRTc$h?Wt1^5z2cU z34&Svp##XmM1EW8#(M<2Z(u1x@&^scZdh2J(xkc_8LC?c-hb3dXx@4jlWih53(j<@ zm6sy+)TO%cAJ23P9l(z)Sf;Dqe|W0Vy7=_HidP)k8L`@nI|SB}5R7itGKryz(u8a1 z)s07P?IYFJLB;FHmfjGu7AC%Hem8wz{Y5uMpDXVhx50}o>x-C5Rhp>s%>4A4+$+Y| zltI4&G4_&Xjmi*BdB*?s#FF1(Kl+c=&mU6)bLP+F)T$rxb4yYGQMM+|1i04e*?y#v z%TzOp9|0r|jlb7h5cg6e2Z0~kpTUo_!Xc+ z`o?KQz-@f;ZSZfp8i#X3WMZ`p6OFR@#_6|`#FxBGi1IH{(7%i5*fn`Q!dowC03=$|&jffU zr}kFXfCB4l5)+)>|LfAuF_Yx> z6T%L|A6&OhN*5E7Ur>%8>$qgf(a|L4c(p+)jwIJ~x3$}#PbH?j)eGgayBa_`=7!{BrDliQ#m_J;cx7f$75+sJXDZKsK@ z>)H1~^!rS}r&o+3jy>Dpkod5s;CV}|@W5qm21bo*!w@)3#?I@KPuG!pJH7^6Qv`^` z<6$Vrm&*n_61lrE%h`hh*Q-l}QMZ(>3gJ!K$*Su4K#9=E`qlf2^l~%fC8N}r#Ex$3 zBFDb(9r=SRMvvTk^ztT-ntkicX9W1FT29$)508Sh99d~u%S(Dg(_(8g6QGzP7*^OZ zxKz_j9-8Rr&GQXZwb*eU)*>Oj1ARR;`IS=aCV6UDZj-b4>ZMtJlh{xwz*rx^Y{vR!QXwMo8<$ zj8nzA3)d}FC-(YOe%;Q7(@NVS(j%X~>I69@4KV@sMs<`kC2R_ym#pg1ty@PWg>4au zPwe)loeUY%uPfMnC*bo5gM)^*20E`M)wI!!uh_HfMxjJT^gyD>SNx8GQ=b#0@0W-x(N>a;w z*pKpZk3`$GpLTB9hJ{n7&hg%~d9mA{s}qP+-!LP4%TSg(K0$)j6f+4P6Pjx0-Il!I zPwsGj+jLMm=T0vc=4#Z$i|APMH{3W}Fl0HMwgXyIhEk6paf=<(K$1Q(7r*c<8nYx2 zGXwWHQ~U0kA#w;r≫vj`=9l6K5N2MHLn z1Xf}?CN?7s$L5Bj6K)$h`15Yexy`j~KNI}+5&ryx!H7(e>WWZWlH$88%R!A9KR367 zCv3v`yXG^Rw>ZNOZ()1U6W(Oj@tO!&;2SHhp1qjK;?J=@YbR%AoR%}Na9CsR_)VSm zZ=Wmo&nyEyNp8y!;wf~NyTkhzIG&6(cqp$C%klj^9(e`{pEHkZ7?;b1yg$y5spr-; z2i1eBc`~Cl)!X_V%4BTyFBS$C3wJLh-fQ!Z`3Smb5ORXn3dY(bFSNnY3Ka@rl_p;(Y-o7`}@;pbB?8e4n804MDJ>pNb2{lF1l8Y z6+y}MLn=-_OrAJTGI&yonotpGP^+xsZn`fuAKc94KiTFmqDntczsEZFD})F(@}Z8h zPQ%ZZ_Yl1I^j?5#t%ngrJ>mN#gzZ?VtaawGr(SA%*l`!1I6CD1?f%*jlKfl44mL{lq%Hn_8#vDbaNZ1Dy)url;%d! zpTV$y#eMy{Bpp;GOLn?zdarcC!&ZE%R>@*1{LU!5pUM+7DL8G4xklT*ywiW}n)vEHUG&h$x*k-FlHT zAIn>=101s9hbVGX7w;ab9iv}fX~0a4>sE+S%0gbLE$_HWPW67j7LizB;7+top}>v1 z{@F?N5buriCa0NzS1qKg36m><{?v<{V04(W_NIssj#?f~#9jm>;*w&bD^2;T}ci3;CJZ^`qxv8koRqcpW24pWJz=XhjGv!8H%cmczVEF|{4mNx0B;}0T*i>4M<73n z8wr%3Oh9FSH=~IOkONDE)QDJ`=oT4H$skPc{VY)9u#@n??AdJqw**qVgB>!%*qLf7E#ZX<5}eU)Jx2c;qp_du7X zH`I6J_z#R)&OiM$>x`ebbwtwRk|#^D@&_ T2zvhdZuAnL>u@w1bNqh*S8JWyyyUyz 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 Date: Fri, 22 Aug 2025 11:18:37 -0400 Subject: [PATCH 5/9] chore: fix indentation --- pkg-r/R/chat.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg-r/R/chat.R b/pkg-r/R/chat.R index 37b54b29..937348e6 100644 --- a/pkg-r/R/chat.R +++ b/pkg-r/R/chat.R @@ -290,8 +290,8 @@ chat_append <- function( #' `"replace"`, then the existing message content is replaced by the new #' content. Ignored if `chunk` is `FALSE`. #' @param icon An optional icon to display next to the message, currently only -#' used for assistant messages. The icon can be any HTML element (e.g., -#' [htmltools::img()] tag) or a string of HTML. +#' used for assistant messages. The icon can be any HTML element (e.g., +#' [htmltools::img()] tag) or a string of HTML. #' @param session The Shiny session object #' #' @returns Returns nothing (\code{invisible(NULL)}). From 8ddc0cac7388e8301e4eaf8857e9f55fd7133c55 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 22 Aug 2025 11:19:37 -0400 Subject: [PATCH 6/9] fix: Add resource path for UI too --- pkg-r/tests/testthat/apps/icon/app.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg-r/tests/testthat/apps/icon/app.R b/pkg-r/tests/testthat/apps/icon/app.R index 7ea07e30..bd24451d 100644 --- a/pkg-r/tests/testthat/apps/icon/app.R +++ b/pkg-r/tests/testthat/apps/icon/app.R @@ -4,6 +4,9 @@ pkgload::load_all() library(shinychat) library(bsicons) +# Add resource path for images +addResourcePath("img", "img") + ui <- page_fillable( title = "Chat Icons", @@ -77,9 +80,6 @@ ui <- page_fillable( ) server <- function(input, output, session) { - # Add resource path for images - addResourcePath("img", "img") - # Default Bot ---- observeEvent(input$chat_default_user_input, { req(input$chat_default_user_input) From 283c34af9ea1029898c9b76c716b1ed638979549 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 22 Aug 2025 11:27:07 -0400 Subject: [PATCH 7/9] fix: use `title` attribute for default icon instead of `class` --- pkg-r/tests/testthat/apps/icon/app.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-r/tests/testthat/apps/icon/app.R b/pkg-r/tests/testthat/apps/icon/app.R index bd24451d..158a3a1d 100644 --- a/pkg-r/tests/testthat/apps/icon/app.R +++ b/pkg-r/tests/testthat/apps/icon/app.R @@ -32,7 +32,7 @@ ui <- page_fillable( chat_ui( id = "chat_animal", messages = list("Hello! I'm Animal Bot. How can I help you today?"), - icon_assistant = fontawesome::fa("otter", ) + icon_assistant = fontawesome::fa("otter", title = "icon-otter") ), selectInput( "animal", From 0d36654858017a31484c822179476458342111d3 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 22 Aug 2025 11:27:29 -0400 Subject: [PATCH 8/9] fix: test app imports --- pkg-r/tests/testthat/apps/icon/app.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg-r/tests/testthat/apps/icon/app.R b/pkg-r/tests/testthat/apps/icon/app.R index 158a3a1d..cdbea499 100644 --- a/pkg-r/tests/testthat/apps/icon/app.R +++ b/pkg-r/tests/testthat/apps/icon/app.R @@ -1,8 +1,7 @@ library(shiny) library(bslib) -pkgload::load_all() library(shinychat) -library(bsicons) +library(fontawesome) # Add resource path for images addResourcePath("img", "img") From 09f721e62e2910e9efeb84b9f6a3f863783358aa Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 22 Aug 2025 11:27:53 -0400 Subject: [PATCH 9/9] fix(chat_ui): Fix `icon-assistant` attribute htmltools in R doesn't turn `icon_assistant` into kebab case --- pkg-r/R/chat.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-r/R/chat.R b/pkg-r/R/chat.R index 937348e6..44398422 100644 --- a/pkg-r/R/chat.R +++ b/pkg-r/R/chat.R @@ -145,7 +145,7 @@ chat_ui <- function( fill = if (isTRUE(fill)) NA else NULL, # Also include icon on the parent so that when messages are dynamically added, # we know the default icon has changed - icon_assistant = if (!is.null(icon_assistant)) { + `icon-assistant` = if (!is.null(icon_assistant)) { as.character(icon_assistant) }, ...,