44# trimming of the message history to fit within the context window; these
55# are left for the caller to handle in the R version.
66
7- # ' @importFrom htmltools tag css
8- # ' @importFrom coro async
9- NULL
10-
117chat_deps <- function () {
128 htmltools :: htmlDependency(
139 " shinychat" ,
@@ -20,36 +16,48 @@ chat_deps <- function() {
2016 list (src = " text-area/textarea-autoresize.js" , type = " module" )
2117 ),
2218 stylesheet = c(
23- " chat/chat.css" ,
19+ " chat/chat.css" ,
2420 " markdown-stream/markdown-stream.css" ,
2521 " text-area/textarea-autoresize.css"
2622 )
2723 )
2824}
2925
3026# ' Create a chat UI element
31- # '
27+ # '
3228# ' @description
3329# ' Inserts a chat UI element into a Shiny UI, which includes a scrollable
3430# ' section for displaying chat messages, and an input field for the user to
3531# ' enter new messages.
36- # '
32+ # '
3733# ' To respond to user input, listen for `input$ID_user_input` (for example, if
3834# ' `id="my_chat"`, user input will be at `input$my_chat_user_input`), and use
3935# ' [chat_append()] to append messages to the chat.
4036# '
4137# ' @param id The ID of the chat element
4238# ' @param ... Extra HTML attributes to include on the chat element
4339# ' @param messages A list of messages to prepopulate the chat with. Each
44- # ' message can be a string or a named list with `content` and `role` fields.
40+ # ' message can be one of the following:
41+ # '
42+ # ' * A string, which is interpreted as markdown and rendered to HTML on
43+ # ' the client.
44+ # ' * To prevent interpreting as markdown, mark the string as
45+ # ' [htmltools::HTML()].
46+ # ' * A UI element.
47+ # ' * This includes [htmltools::tagList()], which take UI elements
48+ # ' (including strings) as children. In this case, strings are still
49+ # ' interpreted as markdown as long as they're not inside HTML.
50+ # ' * A named list of `content` and `role`. The `content` can contain content
51+ # ' as described above, and the `role` can be "assistant" or "user".
52+ # '
4553# ' @param placeholder The placeholder text for the chat's user input field
4654# ' @param width The CSS width of the chat element
4755# ' @param height The CSS height of the chat element
4856# ' @param fill Whether the chat element should try to vertically fill its
4957# ' container, if the container is
5058# ' [fillable](https://rstudio.github.io/bslib/articles/filling/index.html)
5159# ' @returns A Shiny tag object, suitable for inclusion in a Shiny UI
52- # '
60+ # '
5361# ' @examplesIf interactive()
5462# ' library(shiny)
5563# ' library(bslib)
@@ -62,7 +70,7 @@ chat_deps <- function() {
6270# ' server <- function(input, output, session) {
6371# ' observeEvent(input$chat_user_input, {
6472# ' # In a real app, this would call out to a chat model or API,
65- # ' # perhaps using the 'elmer ' package.
73+ # ' # perhaps using the 'ellmer ' package.
6674# ' response <- paste0(
6775# ' "You said:\n\n",
6876# ' "<blockquote>",
@@ -91,26 +99,30 @@ chat_ui <- function(
9199 }
92100
93101 message_tags <- lapply(messages , function (x ) {
94- if (is.character(x )) {
95- x <- list (content = x , role = " assistant" )
96- } else if (is.list(x )) {
97- if (! (" content" %in% names(x ))) {
98- rlang :: abort(" Each message must have a 'content' key." )
99- }
100- if (! (" role" %in% names(x ))) {
101- rlang :: abort(" Each message must have a 'role' key." )
102- }
103- } else {
104- rlang :: abort(" Each message must be a string or a named list." )
102+ role <- " assistant"
103+ content <- x
104+ if (is.list(x ) && (" content" %in% names(x ))) {
105+ content <- x [[" content" ]]
106+ role <- x [[" role" ]] %|| % role
105107 }
106108
107- if (isTRUE(x [[ " role" ]] == " user" )) {
109+ if (isTRUE(role == " user" )) {
108110 tag_name <- " shiny-user-message"
109111 } else {
110112 tag_name <- " shiny-chat-message"
111113 }
112114
113- tag(tag_name , list (content = x [[" content" ]]))
115+ ui <- with_current_theme({
116+ htmltools :: renderTags(content )
117+ })
118+
119+ tag(
120+ tag_name ,
121+ rlang :: list2(
122+ content = ui [[" html" ]],
123+ ui [[" dependencies" ]],
124+ )
125+ )
114126 })
115127
116128 res <- tag(" shiny-chat-container" , rlang :: list2(
@@ -131,36 +143,47 @@ chat_ui <- function(
131143 res <- bslib :: as_fill_carrier(res )
132144 }
133145
134- res
146+ tag_require( res , version = 5 , caller = " chat_ui " )
135147}
136148
137149# ' Append an assistant response (or user message) to a chat control
138150# '
139151# ' @description
140- # ' The `chat_append` function appends a message to an existing chat control . The
152+ # ' The `chat_append` function appends a message to an existing [chat_ui()] . The
141153# ' `response` can be a string, string generator, string promise, or string
142- # ' promise generator (as returned by the 'elmer ' package's `chat`, `stream`,
154+ # ' promise generator (as returned by the 'ellmer ' package's `chat`, `stream`,
143155# ' `chat_async`, and `stream_async` methods, respectively).
144- # '
156+ # '
145157# ' This function should be called from a Shiny app's server. It is generally
146158# ' used to append the model's response to the chat, while user messages are
147159# ' added to the chat UI automatically by the front-end. You'd only need to use
148160# ' `chat_append(role="user")` if you are programmatically generating queries
149161# ' from the server and sending them on behalf of the user, and want them to be
150162# ' reflected in the UI.
151- # '
163+ # '
152164# ' # Error handling
153- # '
165+ # '
154166# ' If the `response` argument is a generator, promise, or promise generator, and
155167# ' an error occurs while producing the message (e.g., an iteration in
156168# ' `stream_async` fails), the promise returned by `chat_append` will reject with
157169# ' the error. If the `chat_append` call is the last expression in a Shiny
158170# ' observer, Shiny will see that the observer failed, and end the user session.
159171# ' If you prefer to handle the error gracefully, use [promises::catch()] on the
160172# ' promise returned by `chat_append`.
161- # '
173+ # '
162174# ' @param id The ID of the chat element
163- # ' @param response The message or message stream to append to the chat element
175+ # ' @param response The message or message stream to append to the chat element.
176+ # ' The actual message content can one of the following:
177+ # '
178+ # ' * A string, which is interpreted as markdown and rendered to HTML on
179+ # ' the client.
180+ # ' * To prevent interpreting as markdown, mark the string as
181+ # ' [htmltools::HTML()].
182+ # ' * A UI element.
183+ # ' * This includes [htmltools::tagList()], which take UI elements
184+ # ' (including strings) as children. In this case, strings are still
185+ # ' interpreted as markdown as long as they're not inside HTML.
186+ # '
164187# ' @param role The role of the message (either "assistant" or "user"). Defaults
165188# ' to "assistant".
166189# ' @param session The Shiny session object
@@ -175,7 +198,7 @@ chat_ui <- function(
175198# ' library(coro)
176199# ' library(bslib)
177200# ' library(shinychat)
178- # '
201+ # '
179202# ' # Dumbest chatbot in the world: ignores user input and chooses
180203# ' # a random, vague response.
181204# ' fake_chatbot <- async_generator(function(input) {
@@ -187,42 +210,31 @@ chat_ui <- function(
187210# ' "Can you elaborate on that?",
188211# ' "Interesting question! Let's examine thi... **See more**"
189212# ' )
190- # '
213+ # '
191214# ' await(async_sleep(1))
192215# ' for (chunk in strsplit(sample(responses, 1), "")[[1]]) {
193216# ' yield(chunk)
194217# ' await(async_sleep(0.02))
195218# ' }
196219# ' })
197- # '
220+ # '
198221# ' ui <- page_fillable(
199222# ' chat_ui("chat", fill = TRUE)
200223# ' )
201- # '
224+ # '
202225# ' server <- function(input, output, session) {
203226# ' observeEvent(input$chat_user_input, {
204227# ' response <- fake_chatbot(input$chat_user_input)
205228# ' chat_append("chat", response)
206229# ' })
207230# ' }
208- # '
231+ # '
209232# ' shinyApp(ui, server)
210- # '
233+ # '
211234# ' @export
212235chat_append <- function (id , response , role = c(" assistant" , " user" ), session = getDefaultReactiveDomain()) {
213236 role <- match.arg(role )
214- if (is.character(response )) {
215- # string => generator
216- stream <- coro :: gen(yield(response ))
217- } else if (promises :: is.promising(response )) {
218- # promise => async generator
219- stream <- coro :: gen(yield(response ))
220- } else if (inherits(response , " coro_generator_instance" )) {
221- # Already a generator (sync or async)
222- stream <- response
223- } else {
224- rlang :: abort(" Unexpected message type; chat_append() expects a string, a string generator, a string promise, or a string promise generator" )
225- }
237+ stream <- as_generator(response )
226238 chat_append_stream(id , stream , role = role , session = session )
227239}
228240
@@ -250,13 +262,13 @@ chat_append <- function(id, response, role = c("assistant", "user"), session = g
250262# ' @returns Returns nothing (\code{invisible(NULL)}).
251263# '
252264# ' @importFrom shiny getDefaultReactiveDomain
253- # '
265+ # '
254266# ' @examplesIf interactive()
255267# ' library(shiny)
256268# ' library(coro)
257269# ' library(bslib)
258270# ' library(shinychat)
259- # '
271+ # '
260272# ' # Dumbest chatbot in the world: ignores user input and chooses
261273# ' # a random, vague response.
262274# ' fake_chatbot <- async_generator(function(id, input) {
@@ -268,7 +280,7 @@ chat_append <- function(id, response, role = c("assistant", "user"), session = g
268280# ' "Can you elaborate on that?",
269281# ' "Interesting question! Let's examine thi... **See more**"
270282# ' )
271- # '
283+ # '
272284# ' # Use low-level chat_append_message() to temporarily set a progress message
273285# ' chat_append_message(id, list(role = "assistant", content = "_Thinking..._ "))
274286# ' await(async_sleep(1))
@@ -280,20 +292,20 @@ chat_append <- function(id, response, role = c("assistant", "user"), session = g
280292# ' await(async_sleep(0.02))
281293# ' }
282294# ' })
283- # '
295+ # '
284296# ' ui <- page_fillable(
285297# ' chat_ui("chat", fill = TRUE)
286298# ' )
287- # '
299+ # '
288300# ' server <- function(input, output, session) {
289301# ' observeEvent(input$chat_user_input, {
290302# ' response <- fake_chatbot("chat", input$chat_user_input)
291303# ' chat_append("chat", response)
292304# ' })
293305# ' }
294- # '
306+ # '
295307# ' shinyApp(ui, server)
296- # '
308+ # '
297309# ' @export
298310chat_append_message <- function (id , msg , chunk = TRUE , operation = c(" append" , " replace" ), session = getDefaultReactiveDomain()) {
299311 if (! is.list(msg )) {
@@ -320,21 +332,31 @@ chat_append_message <- function(id, msg, chunk = TRUE, operation = c("append", "
320332 chunk_type <- NULL
321333 }
322334
323- if (identical(class(msg [[" content" ]]), " character" )) {
324- content_type <- " markdown"
325- } else {
326- content_type <- " html"
327- }
335+ content <- msg [[" content" ]]
336+ is_html <- inherits(content , c(" shiny.tag" , " shiny.tag.list" , " html" , " htmlwidget" ))
337+ content_type <- if (is_html ) " html" else " markdown"
328338
329339 operation <- match.arg(operation )
330340 if (identical(operation , " replace" )) {
331341 operation <- NULL
332342 }
333343
344+ if (is.character(content )) {
345+ # content is most likely a string, so avoid overhead in that case
346+ ui <- list (html = content , deps = " []" )
347+ } else {
348+ # process_ui() does *not* render markdown->HTML, but it does:
349+ # 1. Extract and register HTMLdependency()s with the session.
350+ # 2. Returns a HTML string representation of the TagChild
351+ # (i.e., `div()` -> `"<div>"`).
352+ ui <- process_ui(content , session )
353+ }
354+
334355 msg <- list (
335- content = msg [[" content " ]],
356+ content = ui [[" html " ]],
336357 role = msg [[" role" ]],
337358 content_type = content_type ,
359+ html_deps = ui [[" deps" ]],
338360 chunk_type = chunk_type ,
339361 operation = operation
340362 )
@@ -390,26 +412,26 @@ rlang::on_load(chat_append_stream_impl <- coro::async(function(id, stream, role
390412
391413
392414# ' Clear all messages from a chat control
393- # '
415+ # '
394416# ' @param id The ID of the chat element
395417# ' @param session The Shiny session object
396- # '
397- # ' @export
418+ # '
419+ # ' @export
398420# ' @examplesIf interactive()
399- # '
421+ # '
400422# ' library(shiny)
401423# ' library(bslib)
402- # '
424+ # '
403425# ' ui <- page_fillable(
404426# ' chat_ui("chat", fill = TRUE),
405427# ' actionButton("clear", "Clear chat")
406428# ' )
407- # '
429+ # '
408430# ' server <- function(input, output, session) {
409431# ' observeEvent(input$clear, {
410432# ' chat_clear("chat")
411433# ' })
412- # '
434+ # '
413435# ' observeEvent(input$chat_user_input, {
414436# ' response <- paste0("You said: ", input$chat_user_input)
415437# ' chat_append("chat", response)
@@ -426,4 +448,4 @@ chat_clear <- function(id, session = getDefaultReactiveDomain()) {
426448 obj = NULL
427449 )
428450 )
429- }
451+ }
0 commit comments