| Title: | Generative UI for 'shiny' |
| Version: | 0.1.0 |
| Description: | Build interactive user interfaces for 'shiny' applications through a conversation with a large language model (LLM). Developers choose a set of reusable components, and the model arranges and updates those components as the user describes what they need. Each component's inputs are checked before it is shown, and the model supplies data rather than executable code. Applications can also save and replay the sequence of interface changes without contacting a model. For background on generative user interfaces, see Leviathan et al. (2026) <doi:10.48550/arXiv.2604.09577>. |
| License: | MIT + file LICENSE |
| URL: | https://nanx.me/shinygenui/, https://github.com/nanxstats/shinygenui |
| BugReports: | https://github.com/nanxstats/shinygenui/issues |
| Encoding: | UTF-8 |
| Depends: | R (≥ 4.1.0) |
| Imports: | bslib, cli, ellmer (≥ 0.4.0), htmltools, jsonlite, promises, R6, rlang, shiny, shinychat (≥ 0.4.0), stats, utils, whisker |
| Suggests: | DT, ggplot2, knitr, rmarkdown, shinytest2, testthat (≥ 3.0.0), withr |
| VignetteBuilder: | knitr |
| Config/testthat/edition: | 3 |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-08-28 05:07:02 UTC; nanx |
| Author: | Nan Xiao |
| Maintainer: | Nan Xiao <me@nanx.me> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-09 16:40:02 UTC |
shinygenui: Generative UI for 'shiny'
Description
Build interactive user interfaces for 'shiny' applications through a conversation with a large language model (LLM). Developers choose a set of reusable components, and the model arranges and updates those components as the user describes what they need. Each component's inputs are checked before it is shown, and the model supplies data rather than executable code. Applications can also save and replay the sequence of interface changes without contacting a model. For background on generative user interfaces, see Leviathan et al. (2026) doi:10.48550/arXiv.2604.09577.
Author(s)
Maintainer: Nan Xiao me@nanx.me (ORCID) [copyright holder]
Authors:
Nan Xiao me@nanx.me (ORCID) [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/nanxstats/shinygenui/issues
Create a normalized tool call
Description
A genui_call is the normalized representation of one model-issued tool
call: the tool name plus its arguments as plain data. genui_dispatch()
consumes these. In production they are produced by the compiled ellmer
tools; in tests and replays you can hand-build them.
Usage
genui_call(tool, args = list())
Arguments
tool |
Tool name: a component name from the catalog, or one of the
built-ins |
args |
Named list of arguments as supplied by the model. For
|
Value
A genui_call object.
Examples
genui_call("scatter_plot", list(x = "mpg", y = "hp"))
genui_call("update_component", list(id = "c1", args = list(x = "wt")))
genui_call("remove_component", list(id = "c1"))
Canvas container for generated components
Description
Place this anywhere in your UI, typically as the main content next to a
shinychat::chat_ui() sidebar. Components emitted by the model are
inserted into it progressively as tool calls complete. Pair it with a
genui_server() call using the same id.
Usage
genui_canvas(id, ..., placeholder = "Components will appear here.")
Arguments
id |
Module id, matching the |
... |
Attributes and initial children added to the canvas |
placeholder |
Text shown while the canvas is empty. |
Value
An htmltools::tag() object.
Examples
genui_canvas("canvas")
Container component: a titled row of cards
Description
A container the model can place other components into: create the row,
then create children with parent_id set to the row's instance id.
Children lay out in a responsive grid inside the row's card. Removing
the row removes its children. Add it to your genui_catalog() alongside
genui_components_bslib() (or your own components) to let the model
group related views.
Usage
genui_card_row()
Value
A genui_component() object with container = TRUE.
Examples
catalog <- genui_catalog(
genui_card_row(),
genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
)
Collect components into a catalog
Description
A catalog is the finite set of components the model is allowed to render.
Each component compiles to one ellmer::tool(); the built-in lifecycle
tools (update_component, remove_component, clear_canvas) are
registered alongside it by genui_server().
Usage
genui_catalog(...)
Arguments
... |
|
Value
A genui_catalog object: a named list of components, keyed by
component name.
Examples
note <- genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
catalog <- genui_catalog(note)
names(catalog)
Define a generative UI component
Description
A component is one entry in the finite catalog that constrains what the
model can render. It couples a model-facing tool schema (name,
description, typed args) with developer-written rendering code: a
ui function plus an optional server function, instantiated together
as a dynamic Shiny module every time the model creates or updates an
instance. The model only ever supplies data arguments validated against
the declared types; it never emits code.
Usage
genui_component(
name,
description,
args = list(),
ui,
server = NULL,
check = NULL,
container = FALSE,
width = c("auto", "wide", "full")
)
Arguments
name |
Tool name the model sees. A snake_case string: lowercase
letters, digits, and underscores, starting with a letter. Must be unique
within a catalog and must not collide with the built-in lifecycle tools
( |
description |
One to three sentences telling the model what the component shows and when to use it. This is the model-facing documentation for the component, so write it well. |
args |
Named list of ellmer type specifications
(for example |
ui |
|
server |
Optional |
check |
Optional |
container |
If |
width |
Layout hint for the canvas grid: |
Value
A genui_component object.
Examples
genui_component(
name = "note_card",
description = "A card showing a short markdown note. Use for narrative
text that should live on the canvas rather than in the chat.",
args = list(
title = "A short title for the card.",
text = ellmer::type_string("The markdown body text.")
),
ui = function(id, args) {
htmltools::div(
htmltools::h5(args$title),
htmltools::p(args$text)
)
}
)
Starter component pack built on bslib
Description
A small, general-purpose catalog: a value box, a Markdown card, a data
table, a scatter plot, and a histogram with an embedded bin-count slider
(the reference interactive component: dragging the slider re-renders at
Shiny speed with no LLM round trip). Pass the result to
genui_catalog(), optionally alongside your own components.
Usage
genui_components_bslib(data = NULL)
Arguments
data |
Optional data frame used to ground column arguments as enums
at catalog build time. Typically the same data your |
Details
When data is supplied, column arguments become enums of the actual
column names, so a hallucinated column is a schema violation the model
must correct. Each component also validates columns against the live
data through its check() hook at render time.
Value
A list of genui_component() objects.
Examples
catalog <- genui_catalog(genui_components_bslib(data = mtcars))
names(catalog)
Validate one tool call and plan its effect
Description
The pure core of shinygenui: given the catalog, one normalized call, and the current instance state, either return a plan describing what should happen (create, update, remove, or clear) or signal a classed error whose message is written for the model to read and correct. No Shiny session, registry mutation, or LLM is involved; the Shiny executor applies the returned plan.
Usage
genui_dispatch(catalog, call, state, data = NULL)
Arguments
catalog |
|
call |
A |
state |
Current instance state: a |
data |
Current value of the app's data object, passed to component
|
Value
A genui_plan object, a list with at least action (one of
"create", "update", "remove", "clear") plus the fields the
executor needs: id, component, args (full args to render),
delta (validated partial args, updates only), parent_id, and ids
(teardown order, removes and clears only).
Examples
catalog <- genui_catalog(
genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
)
state <- list(instances = list(), next_id = 1L)
genui_dispatch(catalog, genui_call("note_card", list(text = "hi")), state)
Assemble the system prompt from a catalog
Description
Renders the packaged whisker template (inst/prompts/system.md) with the
catalog's components and an optional developer-supplied context string.
The result instructs the model to narrate briefly in chat while placing
visuals through tools, to reuse update_component when the user refines
an existing view, and to prefer few, dense components.
Usage
genui_prompt(catalog, context = NULL, template = NULL)
Arguments
catalog |
|
context |
Optional string appended as an "App context" section: the data schema, a few sample rows, a business glossary; whatever the model needs to ground its answers. Raw data is never included unless you put it here yourself. |
template |
Optional path to an alternative whisker template, or a
template string. It receives |
Value
A string: the assembled system prompt.
Examples
catalog <- genui_catalog(
genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
)
cat(genui_prompt(catalog, context = "The data is mtcars."))
Rebuild a canvas from a saved trace, no LLM required
Description
Folds a trace (from genui_trace(), or the trace reactive returned by
genui_server()) through the same validate-and-execute pipeline the
model's tool calls use, recreating every instance in a genui_canvas()
with id target. Instance ids come back identical because ids are
assigned deterministically and never reused. Embedded inputs return at
their default values: input state is ephemeral by design and not part of
the trace.
Usage
genui_replay(
trace,
catalog,
target,
data = NULL,
session = shiny::getDefaultReactiveDomain()
)
Arguments
trace |
A trace list, as returned by |
catalog |
The |
target |
Module id of the |
data |
Optional reactive (or function) returning the data object,
passed to component servers and |
session |
The Shiny session (defaults to the current reactive domain). |
Details
Entries that no longer validate (for example after the catalog changed) are skipped with a warning; the rest of the trace still replays. The target canvas is emptied first, so replaying twice is idempotent.
Value
(Invisibly) a list with reactives trace and instances
describing the rebuilt canvas, as in genui_server().
Examples
note <- genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
catalog <- genui_catalog(note)
saved_trace <- list(list(
op = "create",
id = "c1",
component = "note_card",
args = list(text = "Hello")
))
shiny::testServer(
function(input, output, session) {
suppressWarnings(genui_replay(saved_trace, catalog, target = "canvas"))
},
{
stopifnot(identical(names(session$returned$instances()), "c1"))
}
)
Server logic for a generative UI canvas
Description
Wires a genui_catalog() to an ellmer Chat: compiles every component
into a schema-validated tool, registers the built-in lifecycle tools
(update_component, remove_component, clear_canvas), installs the
assembled system prompt, and executes validated tool calls against the
genui_canvas() with the matching id. Components stream onto the
canvas progressively as the model emits tool calls; validation and
rendering failures are returned to the model as tool errors and never
crash the session.
Usage
genui_server(
id,
catalog,
chat,
data = NULL,
chat_id = NULL,
greeting = NULL,
system_prompt = NULL
)
Arguments
id |
Module id, matching the |
catalog |
|
chat |
An ellmer |
data |
A reactive (or function) returning the app's current data
object. It is passed to component |
chat_id |
Id of a |
greeting |
Optional markdown string shown as the assistant's first
message (only used when |
system_prompt |
System prompt to install on |
Details
With chat_id, the package also runs the chat loop for a
shinychat::chat_ui() you placed in the UI: user input is streamed
through chat$stream_async() inside a shiny::ExtendedTask (so other
sessions never block) and appended with shinychat::chat_append(),
with cancel support. With chat_id = NULL you run your own loop on
chat; the registered tools work all the same.
Create the Chat object inside your server function, one per session.
Sharing a single Chat across sessions would cross-wire the tool
closures and leak conversation history between users.
Value
(Invisibly) a list with chat (the wired Chat), and the
reactives trace (the ordered call trace, see genui_trace()) and
instances (the live instance state, a named list keyed by id).
Update semantics
update_component re-instantiates the component's module with the merged
arguments inside the instance's stable shell: same canvas position, same
ids, no page flicker. Because the module restarts, embedded input state
(like the starter histogram's bin slider) resets to its defaults on
update; snapshotting and restoring embedded input values across updates
is explicitly future work.
Error feedback
Any failure while handling a tool call — schema validation, a
component's check() hook, or a rendering error — is signaled as a
regular R condition. ellmer catches it and returns
conditionMessage() to the model as the tool error, so the model can
correct its arguments and retry; the Shiny session itself never crashes,
and a failed call never leaves a half-rendered component behind.
Failures are always logged to the app's server log; set
options(shinygenui.verbose = TRUE) to also log successful canvas
operations.
Examples
note <- genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
catalog <- genui_catalog(note)
chat <- ellmer::chat_openai(
model = "gpt-5.6-sol",
credentials = function() list(api_key = "not-used")
)
shiny::testServer(
genui_server,
args = list(id = "canvas", catalog = catalog, chat = chat),
{
stopifnot("note_card" %in% names(chat$get_tools()))
}
)
Read the ordered call trace of a canvas
Description
The trace is the spec of record for a canvas: every validated call the
model made (create, update, remove, clear), in order, as plain
JSON-friendly lists. genui_replay() can rebuild the canvas from it with
no LLM configured. Embedded input state is intentionally ephemeral and
never recorded.
Usage
genui_trace(session = shiny::getDefaultReactiveDomain(), id = NULL)
Arguments
session |
The Shiny session (defaults to the current reactive domain). |
id |
The |
Value
A reactive expression returning the trace: a list of entries of
the form list(op = "create", id = "c1", component = "...", args = list(...)) (plus parent_id for children; update entries carry the
validated argument delta). Persist it with saveRDS() to replay in a
later session.
Examples
note <- genui_component(
name = "note_card",
description = "A card showing a short note.",
args = list(text = "The note text."),
ui = function(id, args) htmltools::p(args$text)
)
catalog <- genui_catalog(note)
chat <- ellmer::chat_openai(
model = "gpt-5.6-sol",
credentials = function() list(api_key = "not-used")
)
shiny::testServer(
genui_server,
args = list(id = "canvas", catalog = catalog, chat = chat),
{
trace <- genui_trace(session)
stopifnot(identical(trace(), list()))
}
)