--- title: "Get started with shinygenui" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Get started with shinygenui} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` 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](https://ellmer.tidyverse.org) 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 ```{r} 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 ellmer `Chat`. It also adds tools for changing the canvas, installs the system prompt, and connects the model to the `shinychat::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: ```{r} 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.") } } ) ``` - `name` and `description` tell the model when to use the component. Write descriptions that are clear and specific. - `args` is a named list of ellmer types. A plain string is shorthand for `ellmer::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, `column` only accepts names that occur in `mtcars`. Validation rejects any other name before your code runs. - `ui(id, args)` returns htmltools tags. `id` is the module id for this instance. Use `shiny::NS(id)` for inputs and outputs inside the component. - `server(id, args, data)` is optional. It usually calls `shiny::moduleServer()`, as in the example. `data` is the reactive passed to `genui_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 of `data`. Return `NULL` to 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: ```{r} # 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.