Get started with shinygenui

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:

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.")
    }
  }
)

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:

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.