| Title: | Chat UI Component for 'shiny' |
| Version: | 0.5.0 |
| 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. |
| License: | MIT + file LICENSE |
| URL: | https://posit-dev.github.io/shinychat/r/, https://github.com/posit-dev/shinychat |
| BugReports: | https://github.com/posit-dev/shinychat/issues |
| Imports: | base64enc, bslib (≥ 0.12.0), cli, coro, ellmer (≥ 0.4.1), fastmap, htmltools, jsonlite, lifecycle, promises (≥ 1.3.2), R6 (≥ 2.5.0), rlang (≥ 1.2.0), S7, shiny (≥ 1.10.0) |
| Suggests: | chromote, covr, knitr, later, rmarkdown, shinytest2, testthat (≥ 3.0.0), withr |
| VignetteBuilder: | knitr, rmarkdown |
| Config/Needs/website: | tidyverse/tidytemplate, rmarkdown, weathR, gt, glue, bsicons, reactable, htmlwidgets |
| Config/roxygen2/version: | 8.1.0 |
| Config/testthat/edition: | 3 |
| Encoding: | UTF-8 |
| NeedsCompilation: | no |
| Packaged: | 2026-09-09 17:39:07 UTC; garrick |
| Author: | Joe Cheng [aut],
Carson Sievert [aut],
Garrick Aden-Buie |
| Maintainer: | Garrick Aden-Buie <garrick@adenbuie.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-09 18:50:02 UTC |
shinychat: 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.
Author(s)
Maintainer: Garrick Aden-Buie garrick@adenbuie.com (ORCID)
Authors:
Garrick Aden-Buie garrick@adenbuie.com (ORCID)
Joe Cheng joe@posit.co
Carson Sievert carson@posit.co
Barret Schloerke barret@posit.co (ORCID)
Other contributors:
Posit Software, PBC (ROR) [copyright holder, funder]
See Also
Useful links:
Report bugs at https://github.com/posit-dev/shinychat/issues
Slash command content
Description
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.
Usage
ContentSlashCommand(
text = stop("Required"),
command = character(0),
user_text = ""
)
Arguments
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 |
Value
A ContentSlashCommand object.
How it works
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.
Example
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.
See Also
chat_server() for registering slash commands via the
slash_command() method on the returned object.
Abstract base class for conversation storage backends
Description
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).
schema_version compatibility is enforced by the framework's
HistoryController, not by this class – it calls check_schema_version()
on every record it reads from a store and every record it is about to
write, so a custom store doesn't need to check it itself.
Methods
Public methods
ConversationStore$list()
Must be implemented by subclasses. All conversations in
partition, newest-first by created_at.
Usage
ConversationStore$list(partition)
Arguments
partitionA
conversation_partition().
Returns
A list of conversation meta lists.
ConversationStore$get()
Must be implemented by subclasses. The full conversation
record for id in partition.
Usage
ConversationStore$get(partition, id)
Arguments
partitionA
conversation_partition().idA conversation id, as found in the
idfield of a conversation meta list.
Returns
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.
Usage
ConversationStore$put(partition, record)
Arguments
partitionA
conversation_partition().recordA conversation record, in the same shape returned by
get().
Returns
NULL, invisibly.
ConversationStore$delete()
Must be implemented by subclasses. Remove the
conversation id from partition. Missing ids are a no-op.
Usage
ConversationStore$delete(partition, id)
Arguments
partitionA
conversation_partition().idA conversation id, as found in the
idfield of a conversation meta list.
Returns
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.
Usage
ConversationStore$search(partition, query)
Arguments
partitionA
conversation_partition().queryA search string.
Returns
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.
Usage
ConversationStore$total_size(partition)
Arguments
partitionA
conversation_partition().
Returns
The total size in bytes, as a double.
ConversationStore$clone()
The objects of this class are cloneable with this method.
Usage
ConversationStore$clone(deep = FALSE)
Arguments
deepWhether to make a deep clone.
File-based conversation storage backend
Description
Uses temporary records and journal rollback to protect against ordinary I/O failures, but does not fsync files or directories. This store also does not coordinate concurrent access across processes; callers must serialize reads and writes for each conversation.
Super class
ConversationStore -> FileConversationStore
Methods
Public methods
Inherited methods
FileConversationStore$new()
Create a new file-based conversation store.
Usage
FileConversationStore$new(dir = NULL)
Arguments
dirDirectory to store conversations under. Defaults to
NULL, which resolves a redeploy-safe location at first use (seeresolve_history_dir()).
FileConversationStore$list()
All conversations in partition, newest-first by
created_at, read from one record.json per conversation directory
on disk.
Usage
FileConversationStore$list(partition)
Arguments
partitionA
conversation_partition().
Returns
A list of conversation meta lists.
FileConversationStore$get()
The full conversation record for id in partition,
reassembled from record.json, turns.jsonl, and ui.jsonl.
Usage
FileConversationStore$get(partition, id)
Arguments
partitionA
conversation_partition().idA conversation id, as found in the
idfield of a conversation meta list.
Returns
The conversation record, or NULL if missing.
FileConversationStore$put()
Upsert record into partition, appending new turns and
UI data to turns.jsonl/ui.jsonl and rewriting record.json.
Usage
FileConversationStore$put(partition, record)
Arguments
partitionA
conversation_partition().recordA conversation record, in the same shape returned by
get().
Returns
NULL, invisibly.
FileConversationStore$delete()
Remove the conversation id from partition by deleting
its directory. Missing ids are a no-op.
Usage
FileConversationStore$delete(partition, id)
Arguments
partitionA
conversation_partition().idA conversation id, as found in the
idfield of a conversation meta list.
Returns
NULL, invisibly.
FileConversationStore$clone()
The objects of this class are cloneable with this method.
Usage
FileConversationStore$clone(deep = FALSE)
Arguments
deepWhether to make a deep clone.
Open a live chat application in the browser
Description
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.
Usage
chat_app(
client,
...,
title = NULL,
icon = NULL,
window_title = NULL,
id = "chat",
greeting = NULL,
history = TRUE,
bookmark_store = "url",
app_options = list()
)
chat_server(
id,
client,
greeting = NULL,
history = TRUE,
bookmark_on_input = lifecycle::deprecated(),
bookmark_on_response = lifecycle::deprecated(),
session = shiny::getDefaultReactiveDomain()
)
Arguments
client |
A chat object created by ellmer, e.g.
|
... |
Named arguments passed to |
title |
The title displayed in the page header. If |
icon |
Optional UI displayed before |
window_title |
The browser-window title. If |
id |
The ID shared by |
greeting |
Optional greeting to set when the module initializes.
Accepts a static value (string, |
history |
Conversation history configuration. |
bookmark_store |
The bookmarking store to use for the app. Passed to
|
app_options |
A list passed to the |
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. |
Value
-
chat_app()returns ashiny::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 ellmerContentobjects 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 asupdate_chat_user_input(), except foridandsession, which are supplied automatically. -
append(): A function to append a new message to the chat UI. Takes the same arguments aschat_append(), except foridandsession, which are supplied automatically. -
clear(): A function to clear the chat client turns and the chat UI. It optionally takes a list ofmessagesused to initialize the chat after clearing.messagesshould be a list of messages, where each message is a list withroleandcontentfields. Theclient_historyargument controls how the chat client's history is updated after clearing. It can be one of:"clear"the chat history;"set"the chat history tomessages;"append"messagesto the existing chat history; or"keep"the existing chat history.clear()is unavailable when conversation history is enabled; usenew_chat()instead. -
new_chat(): A function to save the current conversation and start a new one by clearing the chat client's turns and chat UI, resetting the active conversation, and updating the history drawer. It is available only when conversation history is enabled.new_chat(greeting = TRUE)also clears the greeting and requests a new one.new_chat()errors while a response is streaming; wait for it to complete or stop it first. -
set_greeting(): A function to set, stream, or clear the chat greeting. Pass achat_greeting()object, a plain string, orNULLto 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, callingset_greeting()updates the content but does not make it visible again; callclear(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. -
history: A namespace for managing conversation-history callbacks and persistence.saved <- chat_module$history$save()saves only the existing active conversation and returns whether it was saved. Storage and bookmark errors propagate to the caller. -
last_error: A reactive value holding the condition from the most recent response if it failed, andNULLotherwise. Both a finished and a failed response read as"idle"instatus, so this is what tells them apart. Responses only: a greeting streams from its own task, and an error raised by a slash command handler is reported as a notification, so neither appears here. -
client: The current chat client object (an active binding that always reflects the latest client, even afterset_client()is called). -
history$conversation_id(): A reactive expression returning the active conversation ID:NULLwhen history is disabled or the chat is still an empty draft, otherwise the ID allocated on the first user submission – before the model call – that the saved conversation record carries. The ID is stable across retries, restores, conversation switches, andset_client()calls. The ID is also handed to the client (via itsconversation_idbinding, when supported), which records it as thegen_ai.conversation.idattribute on its own OpenTelemetry spans. -
set_client(new_client, sync = TRUE): Replace the chat client used by the module. WhensyncisTRUE(the default), the new client inherits conversation turns, system prompt, and tools from the previous client so the conversation continues seamlessly. Setsync = FALSEto 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.handleris required: pass a function (taking 0 or 1 argument), orNULLfor a client-side command handled in JavaScript via theshiny:chat-slash-commandDOM 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.echocontrols whether invoking the command is echoed as a user message and awaits a response; it defaults toTRUEwhen a handler is given andFALSEotherwise (setecho = FALSEfor 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 unlessforce = TRUE.
-
Functions
-
chat_app(): A simple Shiny app for live chatting. Note that this app is suitable for interactive use by a single user; do not usechat_app()in a multi-user Shiny app context. -
chat_server(): Wire up batteries-included chat server logic in a Shiny session. Pair withchat_ui()by passing it the sameid; see Pairing withchat_server()inchat_ui()for the top-level and module-based patterns.
Migration
... now configures page_chat() instead of shiny::shinyApp(). Pass
Shiny app options through app_options, and use bookmark_store instead of
enableBookmarking. To customize onStart or uiPattern, compose
page_chat() and chat_server() manually.
This is a breaking change: ... no longer accepts arguments for
shiny::shinyApp(), including options, enableBookmarking, onStart,
and uiPattern.
Greeting
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.
Examples
## 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",
greeting = "Hi! Use this chat interface to chat with Anthropic's `claude-3-5-sonnet`."
)
),
card(
card_header("Chat with ChatGPT"),
chat_ui(
"openai",
greeting = "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)
Append an assistant response (or user message) to a chat control
Description
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.
Usage
chat_append(
id,
response,
role = c("assistant", "user"),
icon = NULL,
session = getDefaultReactiveDomain()
)
Arguments
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 |
Value
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).
Error handling
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.
Asides
An aside is a small pill that appears at the end of the paragraph or list
item it's attached to, showing a popover on hover, click, or keyboard
focus. Create one by writing (or prompting an LLM to write) an inline
<shiny-aside> tag anywhere in a block's markdown; the tag's content
becomes the popover body:
-
<shiny-aside label="a source name" url="https://...">markdown shown in the popover</shiny-aside>
label controls the text on the identity chip. A safe url makes the
source heading in the popover a link. It also supplies a derived favicon
unless icon overrides it. Without a label, the aside falls back to a
plain numbered marker. The body is ordinary markdown: inline for a one-liner,
or — by separating it with blank lines — a rich block body (paragraphs,
lists, code) shown in the popover. Labeled asides in the same paragraph or
list item collapse into one pill, with each aside kept as a separate popover
page. Each unlabeled aside remains a separate numbered pill. The grouped
pill shows a +N overflow count only when its labeled asides have different
labels. Asides that share one label use a single face with no count.
Set these CSS properties on the chat container to style aside markers:
-
--shiny-chat-aside-marker-color -
--shiny-chat-aside-marker-hover-color -
--shiny-chat-aside-marker-bg -
--shiny-chat-aside-marker-hover-bg -
--shiny-chat-aside-marker-font-family
grounded-span identifies the answer text that is related to an aside.
Its value must exactly match text before the tag in the same paragraph or
list item. When the popover opens, shinychat highlights the most recent
match. If the value does not match, no text is highlighted.
Long content wraps and scrolls within the viewport. The popover keeps the nearest scoped Bootstrap theme. In a paged popover, page changes are announced to assistive technology without repeating the body.
Set display="compact" to show a compact numbered reference in the message.
The popover retains the source label. Compact asides in the same paragraph or
list item share a marker, such as [2, 3].
To style only compact markers, set the CSS properties above on
[data-shinychat-aside-display="compact"].
The favicon is fetched at render time from a third-party service
(DuckDuckGo's icon service), which receives the cited site's hostname. To
avoid that request — for privacy, or for offline/air-gapped deployments —
set the SHINYCHAT_ASIDE_FAVICON environment variable to false. You can
still set icon to a URL you control; an explicit icon bypasses the
lookup entirely.
A labeled aside with a grounded span and a one-line body:
chat_append(
"chat",
paste0(
"Hub motors are cheaper",
paste0(
'<shiny-aside label="eBicycles" ',
'url="https://ebicycles.example/hub-vs-mid-drive" ',
'grounded-span="Hub motors are cheaper">'
),
"[Hub Motor vs. Mid-Drive Motor Differences Explained]",
"(https://ebicycles.example/hub-vs-mid-drive)",
"</shiny-aside>",
", and ideal for flatter terrain."
)
)
Compact labeled asides that share one numbered marker:
chat_append(
"chat",
paste0(
"Revenue is recognized at shipment",
'<shiny-aside display="compact" label="Revenue policy">',
"Exact revenue policy.</shiny-aside>",
" and records are retained for 30 days",
'<shiny-aside display="compact" label="Retention policy">',
"Exact retention policy.</shiny-aside>."
)
)
Two asides cited in the same sentence collapse into one pill (the first source's label becomes the face, with a "+1" overflow):
chat_append(
"chat",
paste0(
"Hub motors are cheaper",
'<shiny-aside label="eBicycles" url="https://ebicycles.example">...</shiny-aside>',
'<shiny-aside label="WIRED" url="https://wired.example">...</shiny-aside>',
", and ideal for flatter terrain."
)
)
A label-less aside with a rich block body (falls back to a plain numbered pill):
chat_append(
"chat",
paste0(
"Battery quality matters more than raw power",
"<shiny-aside>\n\n",
"**Methodology**\n\n",
"- 40 commuter e-bike models\n",
"- released in 2024\n\n",
"</shiny-aside>"
)
)
Examples
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)
Low-level function to append a message to a chat control
Description
For advanced users who want to control the message chunking behavior. Most
users should use chat_append() instead.
Usage
chat_append_message(
id,
msg,
chunk = TRUE,
operation = c("append", "replace"),
icon = NULL,
session = getDefaultReactiveDomain()
)
Arguments
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 |
Value
Returns nothing (invisible(NULL)).
Examples
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)
Create an attachment from a local file path
Description
Reads a file, base64-encodes its contents, and returns a list in the format
expected by the attachments argument of update_chat_user_input().
Usage
chat_attachment(path, mime = NULL, name = NULL)
Arguments
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
|
Value
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().
Clear all messages from a chat control
Description
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()).
This is a UI-level primitive: it does not clear an ellmer client's turns or
reset conversation history managed by chat_server(). For a full reset of a
managed chat, call the new_chat() method on the value returned by
chat_server().
Usage
chat_clear(id, greeting = FALSE, session = getDefaultReactiveDomain())
Arguments
id |
The ID of the chat element |
greeting |
If |
session |
The Shiny session object |
Examples
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)
Create a chat drawer configuration
Description
An drawer displays UI content adjacent to a chat interface, such as
a preview, a generated report, or a detail view. Use
chat_drawer() to supply its initial content and layout to the
drawer argument of chat_ui() or page_chat(). Update the panel
later with the other drawer functions.
Usage
chat_drawer(..., title = NULL, width = 400, open = TRUE, resizable = TRUE)
Arguments
... |
UI content to display in the drawer. |
title |
An optional drawer title. |
width |
The initial drawer width. Positive numbers are converted to pixels; character values must be valid CSS lengths. |
open |
Whether the drawer is initially visible. |
resizable |
Whether the drawer can be resized on desktop. |
Value
A configuration object for use with chat_ui() or page_chat().
See Also
chat_ui() and page_chat() accept this configuration through
their drawer argument.
Other chat drawers:
chat_drawer_hide(),
chat_drawer_show(),
chat_drawer_toggle(),
chat_drawer_update()
Hide a chat drawer
Description
Hide a chat drawer
Usage
chat_drawer_hide(id, session = shiny::getDefaultReactiveDomain())
Arguments
id |
The ID of the chat element. |
session |
The Shiny session object. |
Value
Invisibly, NULL.
See Also
chat_drawer() to configure a drawer, and chat_ui() or
page_chat() to display one.
Other chat drawers:
chat_drawer(),
chat_drawer_show(),
chat_drawer_toggle(),
chat_drawer_update()
Show a chat drawer
Description
Shows a chat's drawer. Supplying content or title updates that
field before the panel is shown. Omitted fields preserve their current value.
Usage
chat_drawer_show(
id,
content = NULL,
title = NULL,
session = shiny::getDefaultReactiveDomain()
)
Arguments
id |
The ID of the chat element. |
content |
Optional UI content for the drawer. Use an empty
|
title |
Optional drawer title. Use |
session |
The Shiny session object. |
Value
Invisibly, NULL.
See Also
chat_drawer() to configure a drawer, and chat_ui() or
page_chat() to display one.
Other chat drawers:
chat_drawer(),
chat_drawer_hide(),
chat_drawer_toggle(),
chat_drawer_update()
Toggle a chat drawer
Description
Toggle a chat drawer
Usage
chat_drawer_toggle(id, session = shiny::getDefaultReactiveDomain())
Arguments
id |
The ID of the chat element. |
session |
The Shiny session object. |
Value
Invisibly, NULL.
See Also
chat_drawer() to configure a drawer, and chat_ui() or
page_chat() to display one.
Other chat drawers:
chat_drawer(),
chat_drawer_hide(),
chat_drawer_show(),
chat_drawer_update()
Update a chat drawer
Description
Updates a chat's drawer content or title without changing its visibility.
Omitted fields preserve their current value. Use an empty
htmltools::tagList() to clear content or "" to clear the title.
Usage
chat_drawer_update(
id,
content = NULL,
title = NULL,
session = shiny::getDefaultReactiveDomain()
)
Arguments
id |
The ID of the chat element. |
content |
Optional UI content for the drawer. |
title |
Optional drawer title. |
session |
The Shiny session object. |
Value
Invisibly, NULL.
See Also
chat_drawer() to configure a drawer, and chat_ui() or
page_chat() to display one.
Other chat drawers:
chat_drawer(),
chat_drawer_hide(),
chat_drawer_show(),
chat_drawer_toggle()
Enable conversation history for a chat
Description
Enable conversation history for a chat
Usage
chat_enable_history(
id,
client,
...,
on_save = NULL,
on_restore = NULL,
options = history_options(),
restore_ui = TRUE,
session = shiny::getDefaultReactiveDomain()
)
Arguments
id |
The chat element ID. |
client |
An ellmer::Chat object. |
... |
Reserved for future use. |
on_save |
An optional |
on_restore |
An optional |
options |
A |
restore_ui |
Whether to render the active conversation into the chat
UI and fire |
session |
The Shiny session. |
Value
Invisibly, a function that cancels all history registrations.
Get the current greeting content
Description
Get the current greeting content
Usage
chat_get_greeting(id, session = getDefaultReactiveDomain())
Arguments
id |
The ID of the chat element |
session |
The Shiny session object |
Value
A character string with the current greeting content, or NULL if
no greeting is set or has been cleared.
Create a greeting for a chat UI
Description
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.
Usage
chat_greeting(
content,
...,
persistent = FALSE,
dismissible = lifecycle::deprecated()
)
Arguments
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 |
|
Value
An S3 object of class "chat_greeting".
Patterns
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"))
))
Examples
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)
Deprecated chat module functions
Description
These functions are deprecated as of shinychat 0.5.0. Use chat_ui() and
chat_server() instead, pairing them by id as described in Pairing with
chat_server() in chat_ui().
Usage
chat_mod_ui(
id,
...,
client = deprecated(),
messages = NULL,
allow_attachments = TRUE
)
chat_mod_server(
id,
client,
greeting = NULL,
history = TRUE,
bookmark_on_input = lifecycle::deprecated(),
bookmark_on_response = lifecycle::deprecated()
)
Arguments
id |
The chat module ID. |
... |
Extra HTML attributes to include on the chat element |
client |
Deprecated. The client state is now managed by |
messages |
Initial messages shown in the chat, used only when |
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 |
greeting |
See |
bookmark_on_input |
See |
bookmark_on_response |
See |
Value
-
chat_mod_ui()returns the UI for a shinychat module. -
chat_mod_server()returns the value ofchat_server().
Functions
-
chat_mod_server(): A Shiny module server for chat (deprecated). Usechat_server()instead.
Create a page-chat navigation panel
Description
Creates a secondary page for page_chat() in the same style as
bslib::nav_panel() and bslib::page_navbar(). When users navigate to the
panel, the chat remains mounted on the home page so its conversation and UI
state persist.
Usage
chat_nav_panel(
title,
...,
value = NULL,
icon = NULL,
sidebar = FALSE,
toolbar = NULL,
content_width = "min(680px, 100%)"
)
Arguments
title |
The panel title. |
... |
UI content to display when the panel is active. |
value |
An optional unique navigation value. Defaults to |
icon |
An optional icon to display with the title. |
sidebar |
Whether to use the default sidebar ( |
toolbar |
|
content_width |
Maximum panel-content width. Content is centered and
receives responsive inline padding. Use exactly |
Value
A configuration object for use with page_chat().
Add Shiny bookmarking for shinychat
Description
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.
Usage
chat_restore(
id,
client,
...,
bookmark_on_input = TRUE,
bookmark_on_response = TRUE,
restore_ui = TRUE,
session = getDefaultReactiveDomain()
)
Arguments
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 |
Value
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.
Examples
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")
Set the greeting for a chat UI
Description
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.
Usage
chat_set_greeting(id, greeting, session = getDefaultReactiveDomain())
Arguments
id |
The ID of the chat element |
greeting |
The greeting to display. Can be:
|
session |
The Shiny session object |
Value
Returns invisible(NULL) for static and NULL greetings, or a
promise for streaming greetings (resolves when streaming is complete).
Examples
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)
Create a chat sidebar configuration
Description
Configures sidebar content for the home view or a chat_nav_panel() in
page_chat(). A page-chat sidebar behaves like a compact
bslib::sidebar() beside the chat and can include the chat's conversation
history selector.
Usage
chat_sidebar(..., history = NULL, width = 280, open = "auto", resizable = TRUE)
Arguments
... |
UI content to display in the sidebar. |
history |
Whether to display the chat history selector in the sidebar.
When |
width |
The initial sidebar width. Positive numbers are converted to pixels; character values must be valid CSS lengths. |
open |
The initial sidebar state. One of |
resizable |
Whether the sidebar can be resized on desktop. |
Value
A configuration object for use with page_chat() or
chat_nav_panel().
Examples
ui <- page_chat(
"Assistant",
sidebar = chat_sidebar(
shiny::tags$p("Workspace"),
history = TRUE,
open = "open"
)
)
Create a chat UI element
Description
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.
Usage
chat_ui(
id,
...,
greeting = NULL,
placeholder = "Enter a message...",
drawer = TRUE,
footer = NULL,
toolbar_input = NULL,
show_history = TRUE,
show_thinking_after_s = 0,
width = "min(clamp(680px, 50vw, 760px), 100%)",
height = "auto",
fill = TRUE,
icon_assistant = NULL,
icon_send = NULL,
enable_cancel = NULL,
submit_key = c("enter", "enter+modifier"),
allow_attachments = NULL,
tool_grouping = c("tool", "none", "all"),
messages = NULL
)
Arguments
id |
The ID of the chat element |
... |
Extra HTML attributes to include on the chat element |
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 |
drawer |
Whether to enable the drawer. |
footer |
Optional HTML content to display in a bottom-pinned, full-width
chat region.
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 |
toolbar_input |
Optional HTML content to display directly below the chat
input. Use |
show_history |
Whether to show the built-in history selector. Defaults
to |
show_thinking_after_s |
The minimum number of seconds a contiguous
thinking block must run before it is displayed. Defaults to |
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 |
icon_send |
The icon to use for the chat input's ready-state submit
button. 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 |
tool_grouping |
Controls how tool calls are grouped together in the compact activity rows:
Prose or thinking between tool calls starts a new tool loop, so calls on
opposite sides of either boundary never group together. Individual tools
can override |
messages |
Deprecated. A list of messages to prepopulate the chat with.
Startup messages can't be recorded by the conversation-history feature.
Use
|
Value
A Shiny tag object, suitable for inclusion in a Shiny UI
Pairing with chat_server()
chat_ui(id) and chat_server(id, client) pair by matching id. This
works the same way at the top level of an app and inside your own Shiny
module — chat_server() is not itself a module, so no NS(id, "chat")
wrapping is required:
# Top-level app, no module
ui <- page_fillable(chat_ui("chat"))
server <- function(input, output, session) {
chat_server("chat", client)
}
# Inside your own module: pass the same literal id to both, and call
# chat_server() from inside moduleServer() so it inherits the module's
# already-namespaced `session`
mod_ui <- function(id) {
ns <- NS(id)
chat_ui(ns("chat"))
}
mod_server <- function(id, client) {
moduleServer(id, function(input, output, session) {
chat_server("chat", client)
})
}
Greeting
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.
Thinking display
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
ContentThinkingobjects. Models that provide a structured thinking API (e.g., Claude with extended thinking) emitContentThinkingobjects when you stream withstream = "content". shinychat detects these and routes them to the thinking panel. This is whatchat_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.
Topic labels
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.
Set show_thinking_after_s in chat_ui() to delay the thinking panel until a
contiguous thinking block has run for a minimum number of seconds. The
default, 0, displays thinking immediately; a negative value always hides
it. Values greater than 60 seconds are not supported. Durations are measured
in the browser while streaming, so preloaded or restored thinking is
displayed only with the default.
Customizing the send button
The send button is a filled circle (24px by default) whose background
color reflects the current state (primary when ready, gray when
empty/disabled, danger when cancelling) with a white icon (22px by
default) centered inside. The icon_send parameter swaps the
ready-state icon without changing the button's surface.
Custom icon. Pass an SVG from bsicons::bs_icon() or
fontawesome::fa(). The button provides the surface, so a bare glyph
gets the same filled-circle treatment as the default arrow:
chat_ui("chat", icon_send = bsicons::bs_icon("send-fill"))
Icon with text. Use htmltools::tagList() to pass an icon and a text
label as siblings (not wrapped in a <span>) so they lay out side by
side, with a Bootstrap margin utility for spacing. Then override the
button to size to its content instead of the default fixed circle:
chat_ui("chat",
icon_send = tagList(
bsicons::bs_icon("airplane-fill"),
span("Send", class = "ms-2")
)
)
:root .shiny-chat-btn-send {
width: auto;
height: auto;
padding: 4px 10px;
border-radius: 6px;
}
Per-state color overrides. Each state's color can be set independently via CSS variables on the chat container or any ancestor. These are only read by the component (never set on the button), so inline styles inherit cleanly:
#chat {
--shiny-chat-btn-send-color-cancel: #abc123;
}
Ghost (outline) style. Make the button transparent at rest with the
state color on the icon and border, filling on hover. Target the button
element (not an ancestor) because the internal --_btn-send-state-color
variable resolves on the button itself:
:root .shiny-chat-btn-send {
--shiny-chat-btn-send-bg: transparent;
--shiny-chat-btn-send-color: var(--_btn-send-state-color);
--shiny-chat-btn-send-border: 1px solid var(--_btn-send-state-color);
--shiny-chat-btn-send-color-hover: #fff;
--shiny-chat-btn-send-bg-hover: var(--_btn-send-state-color);
}
Key CSS variables:
-
--shiny-chat-btn-send-size— Button width and height (default24px) -
--shiny-chat-input-icon-size— Icon size, shared with the attach button (default22px) -
--shiny-chat-btn-send-bg— Button background (default: state color) -
--shiny-chat-btn-send-color— Icon color (default:#fff) -
--shiny-chat-btn-send-border— Button border (default:none) -
--shiny-chat-btn-send-color-ready— Override ready/pending color (default:--bs-primary) -
--shiny-chat-btn-send-color-empty— Override empty/disabled color (default:--bs-gray-500) -
--shiny-chat-btn-send-color-cancel— Override cancel/cancelling color (default:--bs-danger)
Examples
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)
})
}
shinyApp(ui, server)
Create a chat history selector
Description
Create a chat history selector
Usage
chat_ui_history(id, ...)
Arguments
id |
The ID of the associated chat. |
... |
Named HTML attributes to apply to the selector. |
Value
A Shiny tag object.
Format ellmer content for shinychat
Description
Format ellmer content for shinychat
Usage
contents_shinychat(content)
Arguments
content |
An |
Value
Returns text, HTML, or web component tags formatted for use in
chat_ui().
Extending 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.
For most tool-result customization, use tool_result_display() in the
result's extra = list(display = ...). It keeps shinychat's compact activity
row and drill-down card while letting you set a title, label, result preview,
and rich card content. The Tool Calling UI article
describes that recommended path.
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, define the external generic and 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
}
Use S7::super() when you want to extend shinychat's default card. The
resulting output still participates in the normal compact activity row and
drill-down card:
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("Got weather forecast for", content@location_name)
res$label <- content@location_name
res$value_preview <- paste(nrow(content@value), "hourly readings")
res
}
Alternatively, return arbitrary HTML or Shiny UI directly from the method to replace the default card completely. While the tool runs, shinychat still shows its activity row. When the custom result settles, shinychat renders that UI as standalone output and removes the call from the activity row.
This extension point is for fully custom standalone output. To customize the
default card, use tool_result_display() instead of constructing a generic
display list yourself.
Configure chat history options
Description
Configure chat history options
Usage
history_options(
restore_mode = c("browser", "url", "none", "bookmark"),
store = "auto",
scope = NULL,
title = "auto",
max_store_mb = 100
)
Arguments
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 |
Value
A configuration object for use with chat_enable_history().
Stream markdown content
Description
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.
Usage
markdown_stream(
id,
content_stream,
operation = c("replace", "append"),
session = getDefaultReactiveDomain()
)
Arguments
id |
The ID of the markdown stream to stream content to. |
content_stream |
A string generator (e.g., An item may also be a structured content block (a
|
operation |
The operation to perform on the markdown stream. The default,
|
session |
The Shiny session object. |
Value
A promise that resolves to the accumulated stream content as a single string. Structured blocks contribute nothing to the string.
Examples
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)
Create a UI element for a markdown stream.
Description
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.
Usage
output_markdown_stream(
id,
...,
content = "",
content_type = "markdown",
auto_scroll = TRUE,
width = "min(680px, 100%)",
height = "auto"
)
Arguments
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. |
Value
A shiny tag object.
See Also
Create a full-window chat page
Description
page_chat() creates a fillable page containing one persistent chat_ui()
home view, optional navigation pages, and a responsive app-menu sidebar.
Use page_chat() as the top-level page UI when the chat owns the full
browser window. It owns the page layout, the single mounted chat, and the
responsive app-menu controls. Use chat_ui() directly when the chat is
embedded in an existing layout or alongside other top-level page content.
For a standalone interactive chat application, use chat_app(), which
composes page_chat() with chat_server().
Usage
page_chat(
title,
icon = NULL,
...,
id = "chat",
pages_navbar = NULL,
toolbar = NULL,
toolbar_global = bslib::toolbar(bslib::input_dark_mode()),
toolbar_input = NULL,
navbar_options = NULL,
sidebar = TRUE,
messages = NULL,
greeting = NULL,
placeholder = "Enter a message...",
width = "min(clamp(680px, 50vw, 760px), 100%)",
icon_assistant = NULL,
icon_send = NULL,
enable_cancel = NULL,
allow_attachments = NULL,
footer = NULL,
drawer = TRUE,
window_title = NA,
lang = NULL,
theme = page_chat_theme()
)
Arguments
title |
The display title. May be text or reactive/static UI. |
icon |
Optional UI displayed before |
... |
Named lower-frequency |
id |
A non-empty string identifying the chat. The currently selected
page is readable server-side as |
pages_navbar |
|
toolbar |
Optional home-page-scoped UI displayed with the navigation
controls. Use |
toolbar_global |
Optional persistent UI displayed after the page-scoped
toolbar in the navigation controls. Use |
toolbar_input |
Optional UI displayed directly below the chat input.
Use |
navbar_options |
Optional |
sidebar |
Whether to use the default history sidebar ( |
messages, greeting, placeholder, width, icon_assistant, icon_send, enable_cancel, allow_attachments, footer, drawer |
Common arguments passed to |
window_title |
A static browser-window title. The default, |
lang |
An optional non-empty document language string. |
theme |
A |
Value
A fillable bslib page.
Migration from page_fillable()
Replace:
bslib::page_fillable(chat_ui("chat", fill = TRUE))
with:
page_chat("Assistant", id = "chat")
The page supplies the full-window sizing and keeps show_history = TRUE
on the mounted chat. Do not wrap page_chat() in another page container or pass
height, fill, or show_history; those arguments are page-owned.
Navigation, sidebars, and artifacts
pages_navbar accepts a list of additional navbar items. Use
chat_nav_panel() when a page needs page-chat-specific sidebar, toolbar, or
content-width options. It also accepts bslib::nav_panel(),
bslib::nav_panel_hidden(), bslib::nav_menu(), bslib::nav_item(), and
bslib::nav_spacer().
Programmatic navigation uses standard bslib helpers against the derived
"<id>_page" id: bslib::nav_select() to switch pages (including hidden
panels and nav_menu() children), bslib::nav_show() and
bslib::nav_hide() to reveal or hide nav controls. The active page is
readable as input$<id>_page ("__home__" when the main chat page is active).
Sidebar navigation is not yet implemented.
Each panel can use the default sidebar, no page-specific sidebar, or its
own chat_sidebar() or bslib::sidebar() configuration. The sidebar
argument configures the
home view. Use bslib::toolbar() to group controls in toolbar; it is a
home-page-scoped segment rendered with the page navigation controls and
follows them into the mobile app menu. A panel's toolbar = NULL omits that
scoped segment; chat_nav_panel(toolbar = bslib::toolbar(...)) supplies a
page-specific replacement. Use toolbar_global = bslib::toolbar(...) for a
persistent segment that remains mounted on every page after the active
scoped toolbar. On narrow screens, navigation and toolbar controls move into
the app menu above the active page's sidebar content without duplicating
Shiny input or output IDs. By default, toolbar_global contains
bslib::input_dark_mode(); use NULL to opt out.
Set drawer to a chat_drawer() configuration to provide
initial content and layout options. Update the mounted drawer from
the server
with chat_drawer_show(), chat_drawer_update(),
chat_drawer_hide(), and chat_drawer_toggle(). Artifact content is
static UI passed through those server functions; use ordinary Shiny
inputs and outputs inside that content when needed.
You can try navigation and artifact-control examples, which do not require
credentials, through
shiny::runExample("page-chat-navigation", package = "shinychat") and
shiny::runExample("page-chat-drawer-controls", package = "shinychat").
page_chat() owns page composition and accepts one chat root. Do not pass
unrelated top-level UI or a second chat root. Existing apps that need those
layouts should continue using chat_ui() with bslib::page_fillable(),
bslib::page_sidebar(), or another appropriate container.
Examples
library(shiny)
library(shinychat)
artifact_content <- function(label) {
tags$div(
tags$h3("Preview"),
tags$p(label)
)
}
ui <- page_chat(
"Assistant",
messages = "Welcome! Ask a question to get started.",
toolbar = bslib::toolbar(actionButton("show_preview", "Show preview")),
toolbar_global = actionButton("help", "Help"),
sidebar = chat_sidebar(
tags$p("Home tools"),
history = FALSE,
open = "open"
),
pages_navbar = list(
chat_nav_panel(
"About",
tags$p("This is a secondary page."),
value = "about",
),
chat_nav_panel(
"Settings",
tags$p("Settings live here."),
value = "settings",
sidebar = chat_sidebar(
tags$p("Settings menu"),
width = 320,
open = "closed"
),
toolbar = bslib::toolbar(actionButton("save_settings", "Save settings"))
)
),
drawer = chat_drawer(
artifact_content("Initial preview"),
title = "Preview"
)
)
server <- function(input, output, session) {
observeEvent(input$chat_user_input, {
chat_append("chat", paste0("You said: ", input$chat_user_input))
})
observeEvent(input$show_preview, {
chat_drawer_show(
"chat",
content = artifact_content("Preview opened from the server"),
title = "Preview"
)
})
}
shinyApp(ui, server)
Create a theme for page_chat()
Description
page_chat_theme() layers page-scoped surface, chat-radius, and density
tokens and system typography over bslib's "shiny" preset. Supply a
different preset to start from another bslib or Bootswatch preset, or pass
a regular bslib::bs_theme() directly to page_chat() to omit the page-chat
baseline.
Usage
page_chat_theme(..., preset = "shiny")
Arguments
... |
Sass variables forwarded to |
preset |
A bslib or Bootswatch preset name. |
Value
A bslib::bs_theme() suitable for the theme argument of
page_chat().
Customize how a tool result is displayed
Description
tool_result_display() creates an object you can assign to the
display item of the extra argument of an ellmer::ContentToolResult
to customize how shinychat displays the tool result to the user, while
keeping the underlying value sent to the model unchanged.
Usage
tool_result_display(
title = NULL,
icon = NULL,
html = NULL,
markdown = NULL,
text = NULL,
show_request = TRUE,
open = FALSE,
full_screen = FALSE,
footer = NULL,
label = NULL,
value_preview = NULL,
open_style = "minimal"
)
Arguments
title |
The title to use for the settled call and drill-down card. It
replaces the definition-level title from |
icon |
An icon to display with the settled call and drill-down card. Can be a character string or HTML content (e.g. from htmltools::tags). |
html |
Custom HTML content (to use in place of the default result content in the drill-down card). |
markdown |
Custom Markdown string (to use in place of the default result content in the drill-down card). |
text |
Custom plain text string (to use in place of the default result content in the drill-down card). |
show_request |
Whether to show the tool request inside the drill-down card. |
open |
Whether to open the drill-down card by default when the result settles. |
full_screen |
Whether or not to display a fullscreen toggle button on the drill-down card. |
footer |
Optional HTML content to display below the drill-down card body. |
label |
A short, per-call identifying value shown in the activity row
(e.g. a filename or query). Distinguishes this call from other calls to the
same tool. Without one, shinychat falls back to the call's own |
value_preview |
A terse, per-call preview of the tool result, shown in the activity row before its drill-down card is opened. |
open_style |
Whether the result uses the minimal drill-down style or a framed style when open. |
Details
It preserves shinychat's compact activity row and drill-down card. Use it for
result titles, per-call labels and previews, or rich card content. To replace
the settled card with fully custom standalone UI, extend
contents_shinychat() instead. See the Tool Calling UI article for a
complete guide.
Value
An object of class shinychat_tool_result_display, for use as
extra = list(display = tool_result_display(...)) when creating an
ellmer::ContentToolResult.
Examples
library(ellmer)
get_current_weather <- function(location) {
ContentToolResult(
value = "72 degrees and sunny",
extra = list(
display = tool_result_display(
title = paste("Got weather for", location),
label = location,
value_preview = "72°F and sunny",
markdown = "It's **72°F** and sunny."
)
)
)
}
Update the user input of a chat control
Description
Update the user input of a chat control
Usage
update_chat_user_input(
id,
...,
value = NULL,
placeholder = NULL,
submit = FALSE,
focus = FALSE,
attachments = NULL,
attachment_mode = c("append", "set"),
session = getDefaultReactiveDomain()
)
Arguments
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 |
Examples
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)