shinygenui adds two things to a Shiny app: a chat panel where people can ask questions and a canvas where a large language model can add UI components. The app developer decides which UI components the model may use and which arguments each one accepts. Together, these components form a catalog.
Each component in the catalog becomes an ellmer tool. The model builds the interface by calling these tools with data. It never writes R code, and shinygenui never evaluates model output. If the model supplies an invalid argument, such as a column that does not exist, validation rejects the call. The model receives a useful error and can try again while the Shiny session keeps running.
A complete app
library(shiny)
library(bslib)
library(shinygenui)
ui <- page_sidebar(
title = "mtcars explorer",
sidebar = sidebar(width = 380, shinychat::chat_ui("chat", height = "100%")),
genui_canvas("canvas")
)
server <- function(input, output, session) {
catalog <- genui_catalog(genui_components_bslib(data = mtcars))
genui_server(
"canvas",
catalog = catalog,
chat = ellmer::chat_anthropic(),
data = reactive(mtcars),
chat_id = "chat",
greeting = "Ask me about the mtcars data.",
system_prompt = genui_prompt(
catalog,
context = "The data is the mtcars dataset included with R."
)
)
}
shinyApp(ui, server)The app has three main pieces:
-
genui_canvas()creates the space where components appear. -
genui_catalog()collects the components the model may use. This example uses the components supplied by shinygenui: a value box, a Markdown card, a data table, a scatter plot, and a histogram. -
genui_server()turns the catalog into tools and adds them to the ellmerChat. It also adds tools for changing the canvas, installs the system prompt, and connects the model to theshinychat::chat_ui()in the sidebar.
Any ellmer provider works here: you can swap
ellmer::chat_anthropic() for
ellmer::chat_openai(), ellmer::chat_ollama(),
and so on. Create the Chat inside the server function so
that each session gets its own object. This keeps the conversation and
the functions used by its tools separate for each user.
Defining a component
A component describes what the model can ask for and how Shiny should display the result:
histogram <- genui_component(
name = "histogram",
description = "A numeric histogram with a slider for the number of bins.",
args = list(
column = ellmer::type_enum(names(mtcars), "Column to plot."),
bins = ellmer::type_integer("Initial number of bins.", required = FALSE)
),
ui = function(id, args) {
ns <- shiny::NS(id)
bslib::card(
shiny::plotOutput(ns("plot")),
shiny::sliderInput(ns("bins"), "Bins", 5, 60, args$bins %||% 30)
)
},
server = function(id, args, data) {
shiny::moduleServer(id, function(input, output, session) {
output$plot <- shiny::renderPlot({
hist(data()[[args$column]], breaks = input$bins)
})
})
},
check = function(args, data) {
if (!is.numeric(data[[args$column]])) {
paste0("Column \"", args$column, "\" is not numeric.")
}
}
)-
nameanddescriptiontell the model when to use the component. Write descriptions that are clear and specific. -
argsis a named list of ellmer types. A plain string is shorthand forellmer::type_string(). You can build the catalog inside the server function, which means its choices can depend on values available in the current session. In this example,columnonly accepts names that occur inmtcars. Validation rejects any other name before your code runs. -
ui(id, args)returns htmltools tags.idis the module id for this instance. Useshiny::NS(id)for inputs and outputs inside the component. -
server(id, args, data)is optional. It usually callsshiny::moduleServer(), as in the example.datais the reactive passed togenui_server(). The slider is a regular Shiny input, so moving it redraws the plot without contacting the model. If the module creates observers, return them on their own or in a list. shinygenui will destroy them when it updates or removes the component. -
check(args, data)runs after argument validation and receives the current value ofdata. ReturnNULLto accept the arguments or a string to reject them. The model receives this string as an error.
The component lifecycle
Each component created by the model gets an id such as
c1 or c2. The model receives this id, which
lets it refer to the same component later. shinygenui also gives the
model four tools for working with the canvas:
-
update_component(id, args)changes only the arguments supplied by the model, validates the result, and starts the module again in the same place. Because the module restarts, its inputs return to their default values. -
remove_component(id)removes the component. If it is a container, this also removes its children. -
clear_canvas()empties the canvas. -
get_canvas_state()reports each component, its arguments, and the current values of its Shiny inputs. It does not change the canvas. The model can use this information to see what a user has adjusted.
The system prompt created by genui_prompt() asks the
model to keep its chat response brief while it adds components. It also
asks the model to update an existing component when the user refines a
request and to avoid filling the canvas with unnecessary components. Use
the context argument to explain your app and its data.
Useful context might include a description of the columns, a few sample
rows, or terms that are specific to your organization. shinygenui only
puts raw data in the prompt if you include it yourself.
Containers
A component declared with container = TRUE can hold
other components. The model creates the container first, then passes its
id as parent_id when it creates each child.
genui_card_row() is an example supplied by the package. It
creates a titled row that can group value boxes or plots. Removing a
container also removes its children. See
inst/examples/02-layout/app.R for a full app.
Trace and replay
shinygenui records each successful call as a plain list. Together,
these lists describe the current canvas in the order it was built.
genui_trace() returns this record as a reactive, and
genui_replay() passes a saved record through the same
validation and rendering code to rebuild the canvas. You can replay it
in a new session without an LLM:
# In the session that built the canvas
observe({
saveRDS(genui_trace(session)(), "canvas-trace.rds")
})
# In a later session, no chat anywhere
server <- function(input, output, session) {
genui_replay(
readRDS("canvas-trace.rds"),
catalog = catalog,
target = "canvas",
data = reactive(mtcars)
)
}The same calls produce the same instance ids, and ids are never reused. The record does not include values from Shiny inputs, so replay restores those inputs to their defaults.
When things go wrong
If argument validation, check(), or rendering fails, the
model receives an error and can try again. A failed create leaves
nothing behind, and a failed update leaves the existing component
unchanged. The error does not crash the Shiny session. Because the model
can only call components in your catalog, a prompt injection cannot make
it run arbitrary code. Failures are always written to the server log.
Set options(shinygenui.verbose = TRUE) to also log
successful changes to the canvas.