| Title: | Chat UI Component for 'shiny' |
|---|---|
| Description: | Provides a scrolling chat interface with multiline input, suitable for creating chatbot apps based on Large Language Models (LLMs). Designed to work particularly well with the 'ellmer' R package for calling LLMs. |
| Authors: | Joe Cheng [aut], Carson Sievert [aut], Garrick Aden-Buie [aut, cre] (ORCID: <https://orcid.org/0000-0002-7111-0077>), Barret Schloerke [aut] (ORCID: <https://orcid.org/0000-0001-9986-114X>), Posit Software, PBC [cph, fnd] (ROR: <https://ror.org/03wc8by49>) |
| Maintainer: | Garrick Aden-Buie <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.4.0.9000 |
| Built: | 2026-07-06 16:26:40 UTC |
| Source: | https://github.com/posit-dev/shinychat |
Create a simple Shiny app for live chatting using an ellmer::Chat object.
Note that these functions will mutate the input client object as
you chat because your turns will be appended to the history.
The app created by chat_app() is suitable for interactive use by a single
user. For multi-user Shiny apps, use chat_ui() and chat_server() and be
sure to create a new chat client for each user session.
chat_app(client, ..., bookmark_store = "url", allow_attachments = TRUE) chat_server( id, client, greeting = NULL, history = TRUE, bookmark_on_input = lifecycle::deprecated(), bookmark_on_response = lifecycle::deprecated(), session = shiny::getDefaultReactiveDomain() )chat_app(client, ..., bookmark_store = "url", allow_attachments = TRUE) chat_server( id, client, greeting = NULL, history = TRUE, bookmark_on_input = lifecycle::deprecated(), bookmark_on_response = lifecycle::deprecated(), session = shiny::getDefaultReactiveDomain() )
client |
A chat object created by ellmer, e.g.
|
... |
Additional arguments passed to |
bookmark_store |
The bookmarking store to use for the app. Passed to
|
allow_attachments |
Controls the file-attachment affordance (an attach
button, plus clipboard paste and drag-and-drop) in the chat input.
The shape of The maximum combined size of all attachments in a single message is
controlled globally by the |
id |
The ID of the chat element |
greeting |
Optional greeting to set when the module initializes.
Accepts a static value (string, |
history |
Conversation history configuration. |
bookmark_on_input |
A logical value determines if the bookmark should be updated when the user submits a message. Default is |
bookmark_on_response |
A logical value determines if the bookmark should be updated when the response stream completes. Default is |
session |
The Shiny session. Defaults to the current reactive domain. |
chat_app() returns a shiny::shinyApp() object.
chat_server() includes the shinychat server logic, and
returns an environment containing:
last_input: A reactive value containing the last user input (a string
when attachments are disabled, a list of ellmer Content objects when
enabled).
last_turn: A reactive value containing the last assistant turn.
update_user_input(): A function to update the chat input or submit a
new user input. Takes the same arguments as update_chat_user_input(),
except for id and session, which are supplied automatically.
append(): A function to append a new message to the chat UI. Takes
the same arguments as chat_append(), except for id and session,
which are supplied automatically.
clear(): A function to clear the chat history and the chat UI.
clear() takes an optional list of messages used to initialize the
chat after clearing. messages should be a list of messages, where
each message is a list with role and content fields. The
client_history argument controls how the chat client's history is
updated after clearing. It can be one of: "clear" the chat history;
"set" the chat history to messages; "append" messages to the
existing chat history; or "keep" the existing chat history.
set_greeting(): A function to set, stream, or clear the chat
greeting. Pass a chat_greeting() object, a plain string, or
NULL to clear. Streaming greetings run inside an
shiny::ExtendedTask so the session stays responsive; if called
while a greeting is already streaming, the new greeting is queued.
If the greeting has already been dismissed, calling set_greeting()
updates the content but does not make it visible again; call
clear(greeting = TRUE) first to show a new greeting after dismissal.
status: A reactive value indicating the current chat interaction
state. Returns "idle" when no response is in progress, or
"streaming" while a response is actively being received.
client: The current chat client object (an active binding that
always reflects the latest client, even after set_client()
is called).
set_client(new_client, sync = TRUE): Replace the chat client used by
the module. When sync is TRUE (the default), the new client
inherits conversation turns, system prompt, and tools from the previous
client so the conversation continues seamlessly. Set sync = FALSE to
use the new client as-is. If a response is currently streaming, the
swap is deferred until the stream completes. If called multiple times
while streaming, only the most recent new client is used.
slash_command(name, description, handler, ..., echo, force): Register
a slash command. handler is required: pass a function (taking 0 or 1
argument), or NULL for a client-side command handled in JavaScript via
the shiny:chat-slash-command DOM event. A handler that takes one
argument receives a ContentSlashCommand object (not a plain string).
See ContentSlashCommand for details on how to use this object to
preserve the original command text across bookmarks. echo controls
whether invoking the command is echoed as a user message and awaits a
response; it defaults to TRUE when a handler is given and FALSE
otherwise (set echo = FALSE for a handler that only performs side
effects). Returns a function that removes the command. Errors if a
command with the same name is already registered unless
force = TRUE.
chat_app(): A simple Shiny app for live chatting. Note that this
app is suitable for interactive use by a single user; do not use
chat_app() in a multi-user Shiny app context.
chat_server(): Wire up batteries-included chat server logic in a Shiny session.
When greeting is a function, it is called each time the
greeting_requested event fires — on first view when the chat is empty,
and again after clear(greeting = TRUE). The function should return a
chat_greeting() (typically wrapping a stream). Static values (strings,
chat_greeting() objects) are set once at init and do not regenerate.
The function signature determines what is passed. Currently the only
recognized argument is client.
function(client) (recommended). A clone of the client with its turn
history wiped is passed as client. This avoids manually creating and
configuring a separate client:
chat_server("chat", client, greeting = function(client) {
stream <- client$stream_async("Generate a short welcome message.")
chat_greeting(stream)
})
function() (zero arguments). You create and manage your own client:
chat_server("chat", client, greeting = function() {
greeter <- ellmer::chat_openai(model = "gpt-4o")
stream <- greeter$stream_async("Generate a short welcome message.")
chat_greeting(stream)
})
Static value. Set once; does not regenerate after clear():
chat_server("chat", client, greeting = "## Welcome!\n\nHow can I help?")
The returned set_greeting() helper is available for cases where you need
to set a greeting outside the greeting lifecycle.
## Not run: # Interactive in the console ---- client <- ellmer::chat_anthropic() chat_app(client) # Inside a Shiny app ---- library(shiny) library(bslib) library(shinychat) ui <- page_fillable( titlePanel("shinychat example"), layout_columns( card( card_header("Chat with Claude"), chat_ui( "claude", messages = list( "Hi! Use this chat interface to chat with Anthropic's `claude-3-5-sonnet`." ) ) ), card( card_header("Chat with ChatGPT"), chat_ui( "openai", messages = list( "Hi! Use this chat interface to chat with OpenAI's `gpt-4o`." ) ) ) ) ) server <- function(input, output, session) { claude <- ellmer::chat_anthropic(model = "claude-3-5-sonnet-latest") # Requires ANTHROPIC_API_KEY openai <- ellmer::chat_openai(model = "gpt-4o") # Requires OPENAI_API_KEY chat_server("claude", claude) chat_server("openai", openai) } shinyApp(ui, server) ## End(Not run)## Not run: # Interactive in the console ---- client <- ellmer::chat_anthropic() chat_app(client) # Inside a Shiny app ---- library(shiny) library(bslib) library(shinychat) ui <- page_fillable( titlePanel("shinychat example"), layout_columns( card( card_header("Chat with Claude"), chat_ui( "claude", messages = list( "Hi! Use this chat interface to chat with Anthropic's `claude-3-5-sonnet`." ) ) ), card( card_header("Chat with ChatGPT"), chat_ui( "openai", messages = list( "Hi! Use this chat interface to chat with OpenAI's `gpt-4o`." ) ) ) ) ) server <- function(input, output, session) { claude <- ellmer::chat_anthropic(model = "claude-3-5-sonnet-latest") # Requires ANTHROPIC_API_KEY openai <- ellmer::chat_openai(model = "gpt-4o") # Requires OPENAI_API_KEY chat_server("claude", claude) chat_server("openai", openai) } shinyApp(ui, server) ## End(Not run)
The chat_append function appends a message to an existing chat_ui(). The
response can be a string, string generator, string promise, or string
promise generator (as returned by the 'ellmer' package's chat, stream,
chat_async, and stream_async methods, respectively).
This function should be called from a Shiny app's server. It is generally
used to append the client's response to the chat, while user messages are
added to the chat UI automatically by the front-end. You'd only need to use
chat_append(role="user") if you are programmatically generating queries
from the server and sending them on behalf of the user, and want them to be
reflected in the UI.
chat_append( id, response, role = c("assistant", "user"), icon = NULL, session = getDefaultReactiveDomain() )chat_append( id, response, role = c("assistant", "user"), icon = NULL, session = getDefaultReactiveDomain() )
id |
The ID of the chat element |
response |
The message or message stream to append to the chat element. The actual message content can one of the following:
|
role |
The role of the message (either "assistant" or "user"). Defaults to "assistant". |
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
|
session |
The Shiny session object |
Returns a promise that resolves to the contents of the stream, or an error. This promise resolves when the message has been successfully sent to the client; note that it does not guarantee that the message was actually received or rendered by the client. The promise rejects if an error occurs while processing the response (see the "Error handling" section).
If the response argument is a generator, promise, or promise generator, and
an error occurs while producing the message (e.g., an iteration in
stream_async fails), the promise returned by chat_append will reject with
the error. If the chat_append call is the last expression in a Shiny
observer, shinychat will log the error message and show a message that the
error occurred in the chat UI.
library(shiny) library(coro) library(bslib) library(shinychat) # Dumbest chatbot in the world: ignores user input and chooses # a random, vague response. fake_chatbot <- async_generator(function(input) { responses <- c( "What does that suggest to you?", "I see.", "I'm not sure I understand you fully.", "What do you think?", "Can you elaborate on that?", "Interesting question! Let's examine thi... **See more**" ) await(async_sleep(1)) for (chunk in strsplit(sample(responses, 1), "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable( chat_ui("chat", fill = TRUE) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { response <- fake_chatbot(input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)library(shiny) library(coro) library(bslib) library(shinychat) # Dumbest chatbot in the world: ignores user input and chooses # a random, vague response. fake_chatbot <- async_generator(function(input) { responses <- c( "What does that suggest to you?", "I see.", "I'm not sure I understand you fully.", "What do you think?", "Can you elaborate on that?", "Interesting question! Let's examine thi... **See more**" ) await(async_sleep(1)) for (chunk in strsplit(sample(responses, 1), "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable( chat_ui("chat", fill = TRUE) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { response <- fake_chatbot(input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)
For advanced users who want to control the message chunking behavior. Most
users should use chat_append() instead.
chat_append_message( id, msg, chunk = TRUE, operation = c("append", "replace"), icon = NULL, session = getDefaultReactiveDomain() )chat_append_message( id, msg, chunk = TRUE, operation = c("append", "replace"), icon = NULL, session = getDefaultReactiveDomain() )
id |
The ID of the chat element |
msg |
The message to append. Should be a named list with |
chunk |
Whether |
operation |
The operation to perform on the message. If |
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.,
|
session |
The Shiny session object |
Returns nothing (invisible(NULL)).
library(shiny) library(coro) library(bslib) library(shinychat) # Dumbest chatbot in the world: ignores user input and chooses # a random, vague response. fake_chatbot <- async_generator(function(id, input) { responses <- c( "What does that suggest to you?", "I see.", "I'm not sure I understand you fully.", "What do you think?", "Can you elaborate on that?", "Interesting question! Let's examine thi... **See more**" ) # Use low-level chat_append_message() to temporarily set a progress message chat_append_message(id, list(role = "assistant", content = "_Thinking..._ ")) await(async_sleep(1)) # Clear the progress message chat_append_message(id, list(role = "assistant", content = ""), operation = "replace") for (chunk in strsplit(sample(responses, 1), "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable( chat_ui("chat", fill = TRUE) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { response <- fake_chatbot("chat", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)library(shiny) library(coro) library(bslib) library(shinychat) # Dumbest chatbot in the world: ignores user input and chooses # a random, vague response. fake_chatbot <- async_generator(function(id, input) { responses <- c( "What does that suggest to you?", "I see.", "I'm not sure I understand you fully.", "What do you think?", "Can you elaborate on that?", "Interesting question! Let's examine thi... **See more**" ) # Use low-level chat_append_message() to temporarily set a progress message chat_append_message(id, list(role = "assistant", content = "_Thinking..._ ")) await(async_sleep(1)) # Clear the progress message chat_append_message(id, list(role = "assistant", content = ""), operation = "replace") for (chunk in strsplit(sample(responses, 1), "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable( chat_ui("chat", fill = TRUE) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { response <- fake_chatbot("chat", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)
Reads a file, base64-encodes its contents, and returns a list in the format
expected by the attachments argument of update_chat_user_input().
chat_attachment(path, mime = NULL, name = NULL)chat_attachment(path, mime = NULL, name = NULL)
path |
Path to the file. The file must exist. |
mime |
MIME type of the file. When |
name |
Filename shown in the attachment chip. Defaults to
|
A list with elements mime, name, size, and data_url,
ready to pass as an element of the attachments argument of
update_chat_user_input().
Removes all messages from the chat UI. Set greeting = TRUE to also
clear the greeting, which re-triggers greeting_requested (see the
Greeting section in chat_ui()).
chat_clear(id, greeting = FALSE, session = getDefaultReactiveDomain())chat_clear(id, greeting = FALSE, session = getDefaultReactiveDomain())
id |
The ID of the chat element |
greeting |
If |
session |
The Shiny session object |
library(shiny) library(bslib) ui <- page_fillable( chat_ui("chat", fill = TRUE), actionButton("clear", "Clear chat") ) server <- function(input, output, session) { observeEvent(input$clear, { chat_clear("chat") }) observeEvent(input$chat_user_input, { response <- paste0("You said: ", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) # Regenerate greeting on clear ui <- page_fillable( chat_ui("chat"), actionButton("new_chat", "New chat") ) server <- function(input, output, session) { observeEvent(input$chat_greeting_requested, { chat_set_greeting("chat", "## Welcome!\n\nHow can I help?") }) observeEvent(input$new_chat, { # Clearing with greeting = TRUE triggers greeting_requested again chat_clear("chat", greeting = TRUE) }) observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server)library(shiny) library(bslib) ui <- page_fillable( chat_ui("chat", fill = TRUE), actionButton("clear", "Clear chat") ) server <- function(input, output, session) { observeEvent(input$clear, { chat_clear("chat") }) observeEvent(input$chat_user_input, { response <- paste0("You said: ", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) # Regenerate greeting on clear ui <- page_fillable( chat_ui("chat"), actionButton("new_chat", "New chat") ) server <- function(input, output, session) { observeEvent(input$chat_greeting_requested, { chat_set_greeting("chat", "## Welcome!\n\nHow can I help?") }) observeEvent(input$new_chat, { # Clearing with greeting = TRUE triggers greeting_requested again chat_clear("chat", greeting = TRUE) }) observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server)
Enable conversation history for a chat
chat_enable_history( id, client, ..., on_save = NULL, on_restore = NULL, options = history_options(), restore_ui = TRUE, session = shiny::getDefaultReactiveDomain() )chat_enable_history( id, client, ..., on_save = NULL, on_restore = NULL, options = history_options(), restore_ui = TRUE, session = shiny::getDefaultReactiveDomain() )
id |
The chat element ID. |
client |
An ellmer::Chat object. |
... |
Reserved for future use. |
on_save |
An optional |
on_restore |
An optional Note: This callback does not fire when |
options |
A |
restore_ui |
Whether to render the active conversation into the chat
UI and fire |
session |
The Shiny session. |
Invisibly, a function that cancels all history registrations.
Get the current greeting content
chat_get_greeting(id, session = getDefaultReactiveDomain())chat_get_greeting(id, session = getDefaultReactiveDomain())
id |
The ID of the chat element |
session |
The Shiny session object |
A character string with the current greeting content, or NULL if
no greeting is set or has been cleared.
Creates a greeting object for use with chat_ui() or chat_set_greeting().
A greeting is displayed when the chat first loads and is dismissed when the
user sends their first message.
chat_greeting( content, ..., persistent = FALSE, dismissible = lifecycle::deprecated() )chat_greeting( content, ..., persistent = FALSE, dismissible = lifecycle::deprecated() )
content |
The greeting content. Can be:
|
... |
These dots are for future extensions and must be empty. |
persistent |
Whether the greeting persists after the user sends a
message. Defaults to |
dismissible |
|
An S3 object of class "chat_greeting".
Persistent greeting (stays visible after the user sends a message):
chat_greeting("Please read our [terms of service](https://example.com).", persistent = TRUE)
Greeting with suggestion cards (clickable chips that fill the input):
chat_greeting(paste( "## Welcome!\n\n", "Try one of these:\n\n", '<span class="suggestion">Summarize my data</span>\n', '<span class="suggestion">Create a plot</span>\n', '<span class="suggestion">Explain this code</span>' ))
Greeting with HTML tags (Shiny inputs/outputs):
chat_greeting(htmltools::tagList(
htmltools::h2("Welcome!"),
shiny::selectInput("model", "Choose a model:", c("gpt-4o", "claude-3"))
))
library(shiny) library(bslib) library(shinychat) ui <- page_fillable( chat_ui( "chat", greeting = chat_greeting("## Welcome!\n\nHow can I help you today?") ) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { response <- paste0("You said: ", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)library(shiny) library(bslib) library(shinychat) ui <- page_fillable( chat_ui( "chat", greeting = chat_greeting("## Welcome!\n\nHow can I help you today?") ) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { response <- paste0("You said: ", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)
Adds Shiny bookmarking hooks to save and restore the ellmer chat
client. Also restores chat messages from the history in the client.
If either bookmark_on_input or bookmark_on_response is TRUE, the Shiny
App's bookmark will be automatically updated without showing a modal to the
user.
Note: The client's chat state and the greeting content are both
saved/restored automatically. If the client's state doesn't properly
capture the chat's UI (i.e., a transformation is applied in-between
receiving and displaying the message), you may need to implement your own
session$onRestore() (and possibly session$onBookmark) handler to restore
any additional state.
To avoid restoring chat history from the client, you can ensure that the
history is empty by calling client$set_turns(list()) before passing the
client to chat_restore().
chat_restore() bookmarks the whole session and doesn't know about
multiple conversations. If you need per-conversation history (the chat
history drawer, switching between saved conversations), use
chat_enable_history() with history_options(restore_mode = "bookmark")
instead — it replaces chat_restore()'s job for history-aware apps. The
two are mutually exclusive; chat_app() picks one or the other based on
whether history is set.
chat_restore( id, client, ..., bookmark_on_input = TRUE, bookmark_on_response = TRUE, restore_ui = TRUE, session = getDefaultReactiveDomain() )chat_restore( id, client, ..., bookmark_on_input = TRUE, bookmark_on_response = TRUE, restore_ui = TRUE, session = getDefaultReactiveDomain() )
id |
The ID of the chat element |
client |
The ellmer LLM chat client. |
... |
Used for future parameter expansion. |
bookmark_on_input |
A logical value determines if the bookmark should be updated when the user submits a message. Default is |
bookmark_on_response |
A logical value determines if the bookmark should be updated when the response stream completes. Default is |
restore_ui |
Whether to render the client's existing turns into the
chat UI on registration. Default is |
session |
The Shiny session object |
Invisibly returns a function that, when called, cancels all
bookmark registrations made by this call. This is useful when swapping
the chat client: cancel the previous bookmarks, then call
chat_restore() again with the new client.
library(shiny) library(bslib) library(shinychat) ui <- function(request) { page_fillable( chat_ui("chat", fill = TRUE) ) } server <- function(input, output, session) { chat_client <- ellmer::chat_ollama( system_prompt = "Important: Always respond in a limerick", model = "qwen2.5-coder:1.5b", echo = TRUE ) # Update bookmark to chat on user submission and completed response chat_restore("chat", chat_client) observeEvent(input$chat_user_input, { stream <- chat_client$stream_async(input$chat_user_input) chat_append("chat", stream) }) } # Enable bookmarking! shinyApp(ui, server, enableBookmarking = "server")library(shiny) library(bslib) library(shinychat) ui <- function(request) { page_fillable( chat_ui("chat", fill = TRUE) ) } server <- function(input, output, session) { chat_client <- ellmer::chat_ollama( system_prompt = "Important: Always respond in a limerick", model = "qwen2.5-coder:1.5b", echo = TRUE ) # Update bookmark to chat on user submission and completed response chat_restore("chat", chat_client) observeEvent(input$chat_user_input, { stream <- chat_client$stream_async(input$chat_user_input) chat_append("chat", stream) }) } # Enable bookmarking! shinyApp(ui, server, enableBookmarking = "server")
Sets or clears the greeting displayed in an existing chat_ui(). The
greeting is shown when the chat first loads and is dismissed when the user
sends their first message.
Call chat_set_greeting() from the server to display a dynamic or streaming
greeting. This is typically used inside an shiny::observeEvent() watching
input$<id>_greeting_requested – an event that fires when the chat is
visible, has no messages, and has no greeting set. See the Greeting
section in chat_ui() for details on greeting_requested.
If the greeting has already been dismissed, calling this function updates
the greeting content but does not make it visible again. To show a new
greeting after dismissal, first clear the chat with
chat_clear(id, greeting = TRUE).
Pass NULL to clear the current greeting entirely.
chat_set_greeting(id, greeting, session = getDefaultReactiveDomain())chat_set_greeting(id, greeting, session = getDefaultReactiveDomain())
id |
The ID of the chat element |
greeting |
The greeting to display. Can be:
|
session |
The Shiny session object |
Returns invisible(NULL) for static and NULL greetings, or a
promise for streaming greetings (resolves when streaming is complete).
library(shiny) library(bslib) library(shinychat) # Static greeting set from the server ui <- page_fillable(chat_ui("chat")) server <- function(input, output, session) { chat_set_greeting("chat", "## Welcome!\n\nHow can I help you today?") observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) library(coro) # Streaming greeting generated by a function greeting_generator <- async_generator(function() { for (chunk in strsplit("## Hello!\n\nHow can I help you?", "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable(chat_ui("chat")) server <- function(input, output, session) { chat_set_greeting("chat", chat_greeting(greeting_generator())) observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) # LLM-generated greeting using greeting_requested ui <- page_fillable(chat_ui("chat")) server <- function(input, output, session) { chat_client <- ellmer::chat_openai(model = "gpt-4o") observeEvent(input$chat_greeting_requested, { stream <- chat_client$stream_async( "Generate a short, friendly welcome message." ) chat_set_greeting("chat", chat_greeting(stream)) }) observeEvent(input$chat_user_input, { stream <- chat_client$stream_async(input$chat_user_input) chat_append("chat", stream) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) # Regenerate pattern: chat_clear(greeting = TRUE) triggers greeting_requested ui <- page_fillable( chat_ui("chat"), actionButton("regenerate", "New greeting") ) server <- function(input, output, session) { observeEvent(input$chat_greeting_requested, { chat_set_greeting( "chat", paste("## Welcome!\n\nGenerated at", Sys.time()) ) }) observeEvent(input$regenerate, { chat_clear("chat", greeting = TRUE) }) observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server)library(shiny) library(bslib) library(shinychat) # Static greeting set from the server ui <- page_fillable(chat_ui("chat")) server <- function(input, output, session) { chat_set_greeting("chat", "## Welcome!\n\nHow can I help you today?") observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) library(coro) # Streaming greeting generated by a function greeting_generator <- async_generator(function() { for (chunk in strsplit("## Hello!\n\nHow can I help you?", "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable(chat_ui("chat")) server <- function(input, output, session) { chat_set_greeting("chat", chat_greeting(greeting_generator())) observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) # LLM-generated greeting using greeting_requested ui <- page_fillable(chat_ui("chat")) server <- function(input, output, session) { chat_client <- ellmer::chat_openai(model = "gpt-4o") observeEvent(input$chat_greeting_requested, { stream <- chat_client$stream_async( "Generate a short, friendly welcome message." ) chat_set_greeting("chat", chat_greeting(stream)) }) observeEvent(input$chat_user_input, { stream <- chat_client$stream_async(input$chat_user_input) chat_append("chat", stream) }) } shinyApp(ui, server) library(shiny) library(bslib) library(shinychat) # Regenerate pattern: chat_clear(greeting = TRUE) triggers greeting_requested ui <- page_fillable( chat_ui("chat"), actionButton("regenerate", "New greeting") ) server <- function(input, output, session) { observeEvent(input$chat_greeting_requested, { chat_set_greeting( "chat", paste("## Welcome!\n\nGenerated at", Sys.time()) ) }) observeEvent(input$regenerate, { chat_clear("chat", greeting = TRUE) }) observeEvent(input$chat_user_input, { chat_append("chat", paste0("You said: ", input$chat_user_input)) }) } shinyApp(ui, server)
Inserts a chat UI element into a Shiny UI, which includes a scrollable section for displaying chat messages, and an input field for the user to enter new messages.
To respond to user input, listen for input$ID_user_input (for example, if
id="my_chat", user input will be at input$my_chat_user_input), and use
chat_append() to append messages to the chat.
chat_ui( id, ..., messages = NULL, greeting = NULL, placeholder = "Enter a message...", width = "min(680px, 100%)", height = "auto", fill = TRUE, icon_assistant = NULL, enable_cancel = NULL, submit_key = c("enter", "enter+modifier"), allow_attachments = NULL, footer = NULL )chat_ui( id, ..., messages = NULL, greeting = NULL, placeholder = "Enter a message...", width = "min(680px, 100%)", height = "auto", fill = TRUE, icon_assistant = NULL, enable_cancel = NULL, submit_key = c("enter", "enter+modifier"), allow_attachments = NULL, footer = NULL )
id |
The ID of the chat element |
... |
Extra HTML attributes to include on the chat element |
messages |
A list of messages to prepopulate the chat with. Each message can be one of the following:
|
greeting |
An optional greeting to display when the chat first loads.
Can be a |
placeholder |
The placeholder text for the chat's user input field |
width |
The CSS width of the chat element |
height |
The CSS height of the chat element |
fill |
Whether the chat element should try to vertically fill its container, if the container is fillable |
icon_assistant |
The icon to use for the assistant chat messages.
Can be HTML or a tag in the form of |
enable_cancel |
Whether to show a stop button during streaming that
allows the user to cancel the in-progress response. When using
|
submit_key |
Controls which key combination submits the chat message.
|
allow_attachments |
Controls the file-attachment affordance (an attach
button, plus clipboard paste and drag-and-drop) in the chat input.
The shape of The maximum combined size of all attachments in a single message is
controlled globally by the |
footer |
Optional HTML content to display below the chat input.
This can be any HTML content (tags, tag lists, or character strings).
Useful for adding disclaimers, attribution, or other information.
The footer text is styled slightly smaller and lighter than body text
by default. Customize with CSS properties |
A Shiny tag object, suitable for inclusion in a Shiny UI
A greeting is an optional welcome message shown before any conversation
messages. It is automatically dismissed when the user sends their first
message (unless created with persistent = TRUE).
Static greeting. Pass a string or chat_greeting() to the greeting
parameter:
chat_ui("chat", greeting = "## Hello!\n\nHow can I help you today?")
Dynamic greeting from the server. Leave greeting unset and use
chat_set_greeting() from your server function. This is useful when the
greeting depends on session state or is generated by a model.
greeting_requested input. When the chat is visible on the page, has
no messages, and has no greeting set, Shiny fires
input$<id>_greeting_requested (e.g. input$chat_greeting_requested for
chat_ui("chat")). The value is an event counter suitable for
shiny::observeEvent(). Use it to trigger server-side greeting generation:
observeEvent(input$chat_greeting_requested, {
stream <- chat_client$stream_async("Generate a short welcome message.")
chat_set_greeting("chat", chat_greeting(stream))
})
This input fires when the chat component is first viewed on the page and
empty, and again after chat_clear() (greeting = TRUE), enabling a
regenerate pattern where clearing the greeting automatically triggers a
fresh one.
greeting_dismissed input. When the user dismisses the greeting,
input$<id>_greeting_dismissed fires with a Date.now() timestamp. If the
greeting is later cleared after being dismissed, the input resets to NULL.
If you use chat_server(), you can access the greeting_dismissed
reactive from the returned value instead of the raw namespaced input
string.
When a model produces reasoning or "thinking" tokens, shinychat renders them in a collapsible panel above the response. The panel shows a live stream of the model's reasoning while it thinks, then auto-collapses when the response begins.
Thinking display works automatically with any model that supports it. Two paths are supported:
ellmer's ContentThinking objects. Models that provide a structured
thinking API (e.g., Claude with extended thinking) emit ContentThinking
objects when you stream with stream = "content". shinychat detects these
and routes them to the thinking panel. This is what chat_append() uses
internally when you pass it an ellmer content stream.
Raw <thinking> tags. Many open-source and local models (DeepSeek,
QwQ, Qwen, etc.) emit <thinking>...</thinking> tags directly in their
markdown output. shinychat detects these tags during streaming and renders
the enclosed text in the thinking panel with no extra configuration.
You can optionally get labeled sub-sections within the thinking panel by
asking the model to emit <topic>...</topic> tags in its reasoning. These
are extracted and rendered as section headings inside the thinking display,
and the current topic appears in the collapsed header as a live status.
To use topic labels, add something like this to your system prompt:
When thinking through a problem, wrap brief topic labels in <topic> tags to indicate what you're currently reasoning about. For example: <topic>parsing the input</topic>
Topic labels are entirely optional. Without them, the thinking panel still works – it just won't have sub-section headings.
library(shiny) library(bslib) library(shinychat) ui <- page_fillable( chat_ui("chat", fill = TRUE) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { # In a real app, this would call out to a chat client or API, # perhaps using the 'ellmer' package. response <- paste0( "You said:\n\n", "<blockquote>", htmltools::htmlEscape(input$chat_user_input), "</blockquote>" ) chat_append("chat", response) chat_append("chat", stream) }) } shinyApp(ui, server)library(shiny) library(bslib) library(shinychat) ui <- page_fillable( chat_ui("chat", fill = TRUE) ) server <- function(input, output, session) { observeEvent(input$chat_user_input, { # In a real app, this would call out to a chat client or API, # perhaps using the 'ellmer' package. response <- paste0( "You said:\n\n", "<blockquote>", htmltools::htmlEscape(input$chat_user_input), "</blockquote>" ) chat_append("chat", response) chat_append("chat", stream) }) } shinyApp(ui, server)
Format ellmer content for shinychat
contents_shinychat(content)contents_shinychat(content)
content |
An |
Returns text, HTML, or web component tags formatted for use in
chat_ui().
contents_shinychat()
You can extend contents_shinychat() to handle custom content types in your
application. contents_shinychat() is an S7 generic. If
you haven't worked with S7 before, you can learn more about S7 classes,
generics and methods in the S7 documentation.
We'll work through a short example creating a custom display for the results of a tool that gets local weather forecasts. We first need to create a custom class that extends ellmer::ContentToolResult.
library(ellmer)
WeatherToolResult <- S7::new_class(
"WeatherToolResult",
parent = ContentToolResult,
properties = list(
location_name = S7::class_character
)
)
Next, we'll create a simple ellmer::tool() that gets the weather forecast
for a location and returns our custom WeatherToolResult class. The custom
class works just like a regular ContentToolResult, but it has an additional
location_name property.
get_weather_forecast <- tool(
function(lat, lon, location_name) {
WeatherToolResult(
weathR::point_tomorrow(lat, lon, short = FALSE),
location_name = location_name
)
},
name = "get_weather_forecast",
description = "Get the weather forecast for a location.",
arguments = list(
lat = type_number("Latitude"),
lon = type_number("Longitude"),
location_name = type_string("Name of the location for display to the user")
)
)
Finally, we can extend contents_shinychat() to render our custom content
class for display in the chat interface. The basic process is to define a
contents_shinychat() external generic and then implement a method for your
custom class.
contents_shinychat <- S7::new_external_generic(
package = "shinychat",
name = "contents_shinychat",
dispatch_args = "contents"
)
S7::method(contents_shinychat, WeatherToolResult) <- function(content) {
# Your custom rendering logic here
}
You can use this pattern to completely customize how the content is displayed inside shinychat by returning HTML objects directly from this method.
You can also use this pattern to build upon the default shinychat display for
tool requests and results. By using S7::super(), you can create the
object shinychat uses for tool results (or tool requests), and then modify it
to suit your needs.
S7::method(contents_shinychat, WeatherToolResult) <- function(content) {
# Call the super method for ContentToolResult to get shinychat's defaults
res <- contents_shinychat(S7::super(content, ContentToolResult))
# Then update the result object with more specific content
# In this case, we render the tool result dataframe as a {gt} table...
res$value <- gt::as_raw_html(gt::gt(content@value))
res$value_type <- "html"
# ...and update the tool result title to include the location name
res$title <- paste("Weather Forecast for", content@location_name)
res
}
Note that you do not need to create a new class or extend
contents_shinychat() to customize the tool display. Rather, you can use the
strategies discussed in the Tool Calling UI article to
customize the tool request and result display by providing a display list
in the extra argument of the tool result.
An ellmer::ContentText subclass that preserves the original slash command
entered by the user. When the chat UI is restored from a bookmark (or
pre-existing turns), the original /command user_text is shown in the UI
instead of the (possibly transformed) text that was sent to the LLM.
ContentSlashCommand( text = stop("Required"), command = character(0), user_text = "" )ContentSlashCommand( text = stop("Required"), command = character(0), user_text = "" )
text |
The text sent to the LLM. When constructed by the chat module,
this defaults to a descriptive string like
|
command |
The slash command name (without the leading |
user_text |
The text the user typed after the command name. Defaults
to |
A ContentSlashCommand object.
Slash command handlers that accept an argument receive a
ContentSlashCommand object rather than a plain string. The object has
three properties:
command: the command name (e.g., "greet").
user_text: the text the user typed after the command (e.g., "world").
text: the text that will be sent to the LLM. This starts as a
descriptive string like
"The user entered the /greet slash command with arguments: world".
Set it to whatever text the LLM should actually see.
Because ContentSlashCommand extends ellmer::ContentText, it works
anywhere a ContentText does – including client$stream(),
client$chat(), etc. LLM providers read the text property
(inherited behavior), while contents_shinychat() reconstructs the
original /command user_text for display in the chat UI.
Bookmark serialization via ellmer::contents_record() /
ellmer::contents_replay() is automatic.
chat$slash_command("greet", "Greet someone", function(content) {
content@text <- paste("Say hello to", content@user_text)
stream <- client$stream(content)
chat_append("chat", stream)
})
The LLM sees "Say hello to world", but on restore the chat UI shows
/greet world.
chat_server() for registering slash commands via the
slash_command() method on the returned object.
Subclass this to plug a custom persistence backend into
chat_enable_history() via history_options(store = ). All methods
are partitioned by a conversation_partition() (chat id + owner scope);
implementations should not need to know about users, sessions, or Shiny
beyond that.
A conversation record is a list with fields schema_version, id,
title, title_source ("llm", "user", or NULL), response_count,
created_at, updated_at (ISO 8601 strings), client_info, nodes (a
named list of turn nodes forming the conversation tree), current_leaf
(id of the most recent node, or NULL), values (the app state dict
captured by on_save), and bookmark_state_id. A conversation meta list
is the lightweight summary returned by list(): id, title,
created_at, updated_at, and size_bytes (the backend's storage
footprint for that conversation, e.g. on-disk bytes).
ConversationStore$list()Must be implemented by subclasses. All conversations in
partition, newest-first by updated_at.
ConversationStore$list(partition)
partitionA conversation_partition().
A list of conversation meta lists.
ConversationStore$get()Must be implemented by subclasses. The full conversation
record for id in partition.
ConversationStore$get(partition, id)
partitionA conversation_partition().
idA conversation id, as found in the id field of a
conversation meta list.
The conversation record, or NULL if missing.
ConversationStore$put()Must be implemented by subclasses. Upsert record into
partition. A rename is just mutating record$title and calling
put() again.
ConversationStore$put(partition, record)
partitionA conversation_partition().
recordA conversation record, in the same shape returned by
get().
NULL, invisibly.
ConversationStore$delete()Must be implemented by subclasses. Remove the
conversation id from partition. Missing ids are a no-op.
ConversationStore$delete(partition, id)
partitionA conversation_partition().
idA conversation id, as found in the id field of a
conversation meta list.
NULL, invisibly.
ConversationStore$search()Case-insensitive substring match of query against
title, over list(partition). Backends don't need to override this
unless they have a more efficient search path.
ConversationStore$search(partition, query)
partitionA conversation_partition().
queryA search string.
A list of conversation meta lists whose title matches query.
ConversationStore$total_size()Total bytes used by all conversations in partition,
derived from list()'s per-record size_bytes. Backends don't need
to override this unless they have a cheaper way to compute it.
ConversationStore$total_size(partition)
partitionA conversation_partition().
The total size in bytes, as a double.
ConversationStore$clone()The objects of this class are cloneable with this method.
ConversationStore$clone(deep = FALSE)
deepWhether to make a deep clone.
File-based conversation storage backend
ConversationStore -> FileConversationStore
FileConversationStore$new()Create a new file-based conversation store.
FileConversationStore$new(dir = NULL)
dirDirectory to store conversations under. Defaults to
NULL, which resolves a redeploy-safe location at first use (see
resolve_history_dir()).
FileConversationStore$list()FileConversationStore$list(partition)
partitionA conversation_partition().
FileConversationStore$get()FileConversationStore$get(partition, id)
partitionA conversation_partition().
idA conversation id, as found in the id field of a
conversation meta list.
FileConversationStore$put()FileConversationStore$put(partition, record)
partitionA conversation_partition().
recordA conversation record, in the same shape returned by
get().
FileConversationStore$delete()FileConversationStore$delete(partition, id)
partitionA conversation_partition().
idA conversation id, as found in the id field of a
conversation meta list.
FileConversationStore$clone()The objects of this class are cloneable with this method.
FileConversationStore$clone(deep = FALSE)
deepWhether to make a deep clone.
Configure chat history options
history_options( restore_mode = c("browser", "url", "none", "bookmark"), store = "auto", scope = NULL, title = "auto", max_store_mb = 100 )history_options( restore_mode = c("browser", "url", "none", "bookmark"), store = "auto", scope = NULL, title = "auto", max_store_mb = 100 )
restore_mode |
How a previous conversation is reloaded when the page
opens. |
store |
Storage backend: |
scope |
Storage namespace for conversations. A string, a
|
title |
Title generation strategy. |
max_store_mb |
Maximum total storage in megabytes per chat history
partition. Oldest conversations are evicted when the limit is exceeded.
Defaults to |
A configuration object for use with chat_enable_history().
Streams markdown content into a output_markdown_stream() UI element. A
markdown stream can be useful for displaying generative AI responses (outside
of a chat interface), streaming logs, or other use cases where chunks of
content are generated over time.
markdown_stream( id, content_stream, operation = c("replace", "append"), session = getDefaultReactiveDomain() )markdown_stream( id, content_stream, operation = c("replace", "append"), session = getDefaultReactiveDomain() )
id |
The ID of the markdown stream to stream content to. |
content_stream |
A string generator (e.g., |
operation |
The operation to perform on the markdown stream. The default,
|
session |
The Shiny session object. |
library(shiny) library(coro) library(bslib) library(shinychat) # Define a generator that yields a random response # (imagine this is a more sophisticated AI generator) random_response_generator <- async_generator(function() { responses <- c( "What does that suggest to you?", "I see.", "I'm not sure I understand you fully.", "What do you think?", "Can you elaborate on that?", "Interesting question! Let's examine thi... **See more**" ) await(async_sleep(1)) for (chunk in strsplit(sample(responses, 1), "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable( actionButton("generate", "Generate response"), output_markdown_stream("stream") ) server <- function(input, output, session) { observeEvent(input$generate, { markdown_stream("stream", random_response_generator()) }) } shinyApp(ui, server)library(shiny) library(coro) library(bslib) library(shinychat) # Define a generator that yields a random response # (imagine this is a more sophisticated AI generator) random_response_generator <- async_generator(function() { responses <- c( "What does that suggest to you?", "I see.", "I'm not sure I understand you fully.", "What do you think?", "Can you elaborate on that?", "Interesting question! Let's examine thi... **See more**" ) await(async_sleep(1)) for (chunk in strsplit(sample(responses, 1), "")[[1]]) { yield(chunk) await(async_sleep(0.02)) } }) ui <- page_fillable( actionButton("generate", "Generate response"), output_markdown_stream("stream") ) server <- function(input, output, session) { observeEvent(input$generate, { markdown_stream("stream", random_response_generator()) }) } shinyApp(ui, server)
Creates a UI element for a markdown_stream(). A markdown stream can be
useful for displaying generative AI responses (outside of a chat interface),
streaming logs, or other use cases where chunks of content are generated
over time.
output_markdown_stream( id, ..., content = "", content_type = "markdown", auto_scroll = TRUE, width = "min(680px, 100%)", height = "auto" )output_markdown_stream( id, ..., content = "", content_type = "markdown", auto_scroll = TRUE, width = "min(680px, 100%)", height = "auto" )
id |
A unique identifier for this markdown stream. |
... |
Extra HTML attributes to include on the chat element |
content |
A string of content to display before any streaming occurs.
When |
content_type |
The content type. Default is |
auto_scroll |
Whether to automatically scroll to the bottom of a scrollable container when new content is added. Default is True. |
width |
The width of the UI element. |
height |
The height of the UI element. |
A shiny tag object.
Update the user input of a chat control
update_chat_user_input( id, ..., value = NULL, placeholder = NULL, submit = FALSE, focus = FALSE, attachments = NULL, attachment_mode = c("append", "set"), session = getDefaultReactiveDomain() )update_chat_user_input( id, ..., value = NULL, placeholder = NULL, submit = FALSE, focus = FALSE, attachments = NULL, attachment_mode = c("append", "set"), session = getDefaultReactiveDomain() )
id |
The ID of the chat element |
... |
Currently unused, but reserved for future use. |
value |
The value to set the user input to. If |
placeholder |
The placeholder text for the user input |
submit |
Whether to automatically submit the text for the user. Requires |
focus |
Whether to move focus to the input element. Requires |
attachments |
A list of attachment objects created by
|
attachment_mode |
How to combine |
session |
The Shiny session object |
library(shiny) library(bslib) library(shinychat) ui <- page_fillable( chat_ui("chat"), layout_columns( fill = FALSE, actionButton("update_placeholder", "Update placeholder"), actionButton("update_value", "Update user input") ) ) server <- function(input, output, session) { observeEvent(input$update_placeholder, { update_chat_user_input("chat", placeholder = "New placeholder text") }) observeEvent(input$update_value, { update_chat_user_input("chat", value = "New user input", focus = TRUE) }) observeEvent(input$chat_user_input, { response <- paste0("You said: ", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)library(shiny) library(bslib) library(shinychat) ui <- page_fillable( chat_ui("chat"), layout_columns( fill = FALSE, actionButton("update_placeholder", "Update placeholder"), actionButton("update_value", "Update user input") ) ) server <- function(input, output, session) { observeEvent(input$update_placeholder, { update_chat_user_input("chat", placeholder = "New placeholder text") }) observeEvent(input$update_value, { update_chat_user_input("chat", value = "New user input", focus = TRUE) }) observeEvent(input$chat_user_input, { response <- paste0("You said: ", input$chat_user_input) chat_append("chat", response) }) } shinyApp(ui, server)