Introducing Shiny Assistant - You Can Now Build Shiny Applications with GPT and GenerativeAI

Estimated time:
time
min

It finally happened.

It’s been almost two years since the first release of ChatGPT. It took the world by storm, to put it mildly. In the short period that followed, we now have multiple companies building Generative AI platforms that were unimaginable before late 2022. What a great time to be a consumer.

Your new sidekick for building Shiny apps - Shiny Assistant - has nothing to do with the company behind ChatGPT, but is wrapped in an interface that’ll feel right at home. It’s built on the Claude 3.5 Sonnet model released by Anthropic, a San Francisco-based AI company.

To be more precise, the Assistant is built on a Large Language Model specialized in Shiny, R, and Python coding, allowing you to create applications, debug, refactor code, and switch between Shiny for R and Python.

How good is Shiny Assistant in the current closed beta stage? Join us on a journey building an entire application to find out.

Is your R Shiny app slow? Consider handling large datasets with DuckDB.

Table of contents:

What is Shiny Assistant and Why Should You Care?

Think of Shiny Assistant as your AI companion for building Shiny apps.

It doesn’t matter if you prefer R or Python, as the Assistant can work with both. Just keep in mind that Shiny for Python is about a decade younger than its R equivalent, so the quality and variety of answers might not be at the same level. We’ll test only the R implementation in this article, and leave Python for some other time.

Shiny Assistant is currently in an invite-only closed beta stage:

Image 1 - Shiny Assistant invitation message

We encourage you to join the waitlist. As soon as you’re accepted, you’ll see the following screen:

Image 2 - Shiny Assistant start page

From here, you can start communicating with the chatbot or you can tweak the response type (“Code only”, ”Concise”, or ”Verbose”), but this feature isn’t currently working as expected (more about it later). You can also specify your own API key for Anthropic through the settings menu.

And that’s about it - the starting interface is simple.

It’s when you submit your first message that you’ll see a 3-column screen layout with a chatbot (left), generated code (middle), and live preview (right):

Image 3 - Shiny Assistant chatbot layout

You’ll get really familiar with the UI in the following section, as we’ll use the chatbot to build an entire app from scratch!

Shiny Assistant in Action - Let’s Build an R Shiny Application from Scratch

There are numerous ways to build applications with AI.

It’s hard to argue which one works best, but a logical assumption is that building an app incrementally (prompt by prompt, or component by component) leaves less room for error than if you were to dump every requirement into one massive prompt. The component-based approach might still fail, but spotting and fixing errors is much faster since you’re working on one thing at a time.

For that reason, we’ll first give the Shiny Assistant a chance to get familiar with the data and project requirements and then build the app piece by piece.

By the end, you’ll end up with the following app:

Image 4 - Final R Shiny application

Long story short, the app uses the US Airports dataset to provide an overview of airport locations for the entire United States and per individual state.

You can find the source code (without test cases) at the end of this section.

Let’s start with the first prompt!

Prompt #1 - Data Familiarization and UI Structure

The idea is to start small and let Shiny Assistant get familiar with the dataset and project requirements.

This is the prompt we used to provide context about the data and how we want the UI to be structured. Note that no UI elements are constructed yet, except for the filter used to select the state. We’ve also instructed the Assistant to have responsive design in mind:

I have a dataset in an airports.csv file.
This is what a data sample looks like (the first row is the header):
```
IATA	AIRPORT	CITY	STATE	COUNTRY	LATITUDE	LONGITUDE
ABQ	Albuquerque International	Albuquerque	NM	USA	35.04022222	-106.6091944
ANC	Ted Stevens Anchorage International	Anchorage	AK	USA	61.17432028	-149.9961856
ATL	William B Hartsfield-Atlanta Intl	Atlanta	GA	USA	33.64044444	-84.42694444
AUS	Austin-Bergstrom International	Austin	TX	USA	30.19453278	-97.66987194
BDL	Bradley International	Windsor Locks	CT	USA	41.93887417	-72.68322833
```

I want to build an R Shiny application around it. It should have 2 main UI components - a sidebar and a main content body.
In the sidebar you should add the following filters:
- State - dropdown - allows the user to select exactly one state. Data for all states is shown by default (All)

In the main content body, for now, don't output anything - I plan to instruct you what to do on the fly.

Notes:
- Wrap UI components in HTML Divs and give them meaningful CSS class names. I plan to add CSS later, so don't worry about it yet.
- Have responsive design in mind (mobile, tablet, desktop)
Image 5 - Prompt 1 output

Overall, we were satisfied with the response.

Shiny Assistant understood the context has constructed the UI correctly. The single input allows the user to view statistics for all states and on an individual state level.

The Assistant used the `bslib` package to handle responsive design. It’s a neat shortcut, but some users might prefer to implement everything from scratch through CSS.

We’ll let it slide this time.

Prompt #2 - Global Summary Statistics

The first UI component we want to build is a set of global statistics.

It should only be visible when “All” is selected in the state filter and should show four cards with the following information:

  • Total number of airports
  • Average number of airports per state
  • The state with the most airports
  • The state with the least airports

It’s quite basic, sure, but will give you an idea of how Shiny Assistant works with data aggregation. We’ve also instructed it to lay out the statistics differently on different screen sizes:

Great job!

The next step is to show four summary statistics, but only if the "All" is selected in the state dropdown. If an individual state is selected, don't display these statistics!
The statistics are:
- Total number of airports (count distinct airport names)
- Average number of airports in a single state (count distinct airport names divided by the number of distinct states)
- Name of the state with the most airports (group by count, first)
- Name of the state with the least airports (group by count, last)

As for the design, you should display all states in small cards containing small paragraphs with the statistic name and a larger statistic value text.
Display the cards in a single row on desktop, in a 2 by 2 grid on tablet, and one below another on mobile.
Give CSS class names to cards, statistic names, and statistic values.
Image 6 - Prompt 2 output

The Assistant returned the R application code and corresponding stylesheets (CSS). It calculated the statistics correctly, and has also layed out the UI correctly. The only problem is - the responsive design doesn’t work:

Image 7 - Prompt 2 output (2)

After inspection, we found that the most obvious thing went wrong - the Assistant forgot to link the stylesheet in the R Shiny application.

Prompt #3 - CSS Linking Issue Fix

To fix this, we manually inserted a line of code and shown the Assistant what was wrong:

Your R code and CSS code are correct, but you forgot to link the CSS file in app.R.
I've added the following code to `ui` to make it work:
```
includeCSS("www/custom.css"),
```

From now on, always add this code at the top of the `ui`.
Image 8 - Prompt 3 output

Note that the Assistant has access only to a handful of data samples.

When we copied the code to a local IDE and run the app, the correct values were shown:

Image 9 - Prompt 3 output (2)

This is the only time we’ve manually fixed the Assistant’s mistakes. From now on, we’ll only show the error messages.

Prompt #4 - Global Choropleth Map

Now the fun part begins!

Probably the most challenging task of the day is building an interactive map. It should be displayed only for global statistics, and should show a choropleth map of all 50 states and their airport counts.

We’ve instructed the Assistant on our color preference and told it to implement a tooltip that’s shown when the user hovers over a single state:

Your next task is to create a Choropleth map, but only if the "All" is selected in the state dropdown. If an individual state is selected, don't do anything!
The map should show US states colored in a tint of color blue. States with fewer airports have a lighter tint, while states with more airports have a darker tint of blue.
When the user hovers over an individual state, you should show a tooltip showing the state name and the number of airports.
As for the UI, embed the map in a card identical to the one used for statistics, but larger. It should always take the full width of the screen.
Don't add any titles to the card. Instead, make the map take the full width of the card - no margins and paddings.
Image 10 - Prompt 4 output

And we got our first error!

Shiny Assistant called a function `replace_na()` that doesn’t exist. The function name sounds reasonable, but it wasn’t found in any of the imported packages.

Prompt #5 - Global Choropleth Map Bug Fix (Iteration 1)

So, let’s instruct it to fix the error.

We’ve copied the error message and provided additional context that the function doesn’t exist:

Your code produces the following error:
```
Error in `stopifnot()`:
ℹ In argument: `count = replace_na(count, 0)`.
Caused by error in `replace_na()`:
! could not find function "replace_na"
```

The `replace_na()` function doesn't exist. Use an alternative that won't raise an error.
Image 11 - Prompt 5 output

Shiny Assistant decided to replace the non-existent function with `ifelse()`. Good choice, as it got our app working again:

Image 12 - Shiny app after Prompt 5

The map overlay works, but there’s a problem with the airport counts now.

Prompt #6 - Global Choropleth Map Bug Fix (Iteration 2)

Upon code inspection, we’ve spotted that the generated code is trying to join two data frames on columns that have different values:

Image 13 - Data frame column differences

One shows state names as a two-letter abbreviation (e.g., CA), while the other contains full state names (e.g., California). The semantic information is there, but that’s not enough for a data frame join to work with.

We’ve pointed out this error to the Assistant:

The state data count is always zero. 
I've figured out the problem. The `us_states` dataset before joining contains full state names, while `airports_per_state` dataset contains counts with abbreviated two-letter state codes. That's why the counts aren't showing.
Fix this issue.
Image 14 - Prompt 6 output

The live display functionality has stopped working in Shiny Assistant, so all further screenshots will be from the app running locally.

This is what the app looks like after the generated bug fix:

Image 15 - Shiny app after prompt 6

Now the app works as expected.

Prompt #7 - Global Choropleth Map Style Fix

There are still two styling issues that are bothering us.

The map shows the data correctly, but the state name is lowercased, and the user can accidentally scroll in or out of the map with the scroll wheel. The map has +/- controls that are enough to adjust the zoom.

The following prompt should fix both issues:

Almost there!
Here are a couple of more issues you need to fix:
- On the tooltip, the state name is lowercased. I want you to use a title case (capitalize each word)
- I can use a mouse scroll wheel to zoom in and out - disable this behavior
Image 16 - Shiny app after Prompt 7

That’s it regarding the map - it displays the data correctly and formats the tooltip just as we wanted.

Let’s move on to the next component.

Prompt #8 - Global Table with Reactable

The reactable package in R is used to build interactive tables.

We’ll use only the most basic functionality of the package to show everything shown on the map, but now in a table format.

This table will only be displayed when the user is viewing global airport statistics (dropdown menu set to “All”):

Great! Let's move on.

Your next task is to create a table with the reactable package, but only if the "All" is selected in the state dropdown. If an individual state is selected, don't do anything!
The table should contain the following values:
- State name (string)
- Number of airports (integer)

Basically, it shows the same data that the map shows, but in a different format.
Regarding the UI, add the table into a card similar to the one for statistics, but larger, taking full-screen width on all device types.
Sort the table according to the number of airports in descending order.
Image 17 - Shiny app after Prompt 8

The table with pagination is displayed, but it also includes an unnecessary `glob` column. It’s something we’ll try to remove next.

Prompt #9 - Global Table with Reactable Bug Fix

The following prompt instructs Shiny Assistant to remove the `glob` column, as it’s not needed in the app:

You did a good job with the table, but it contains a `geom` column that is not needed.
Remove it.
Image 18 - Shiny app after Prompt 9

And with that, we’ve finished working on the global airport statistics. From now on, we’ll shift our efforts to state-specific views.

Prompt #10 - State-Specific UI Elements

If you were to select an individual state from the dropdown, you’d see the following output:

Image 19 - R Shiny app contents for an individual state

No errors are raised, but no data is displayed either.

The following prompt instructs Shiny Assistant to include a:

  • Scatter Map showing a geolocation of all airports in a selected state. More details are displayed on hover (airport code, name, latitude, and longitude)
  • Table showing the same information
Amazing job! 
That's it for the case when "All" is selected in the state dropdown. Moving forward, you'll work on the individual states.
When an individual state is selected, I want you to display two things:
- Scatter map showing the state and scatter points showing locations of individual airports. The original dataset has latitude and longitude information, so use these columns for marker location. On marker hover, show the airport code, airport name, latitude, and longitude.
- Table using reactable package, showing all airports in a selected state. Add columns for the airport code, airport name, latitude, and longitude.
Image 20 - Shiny app after Prompt 10

It looks like we have another error. Hopefully, it’ll be easy to fix since it looks like the app is referencing a data frame column that doesn’t exist.

Prompt #11 - State-Specific UI Elements Bug Fix

Upon further inspection, that was the only error.

It’s a strange one since we explicitly told the Assistant the structure of our data 10 prompts ago (potentially a context limit?), but it’s an easy error to fix.

The following prompt shows the Assistant all the column names we have available, and instructs it to fix the issue:

The code you generated references a column named `FACILITY_NAME` which doesn't exist.
Here are the column names you have in the dataset:
```
IATA	AIRPORT	CITY	STATE	COUNTRY	LATITUDE	LONGITUDE
```

Fix the issue.
Image 21 - Shiny app after Prompt 11

The table could benefit from some stylings, but let’s say we’re satisfied with the results.

The app is now complete! The only problem is that the `app.R` file is a mess of 260 lines of code. In other words, you should reformat the code base before marking your work as done.

Prompt #12 - Better Code Organization with Shiny Modules

Shiny Modules allow you to separate reusable app logic into smaller, more manageable R scripts.

That’s exactly what we want to do next. The following prompt instructs Shiny Assistant to split the logic into multiple files and provide better organization for the app:

That's it! You've implemented everything I wanted!
What you'll have to do moving forward is strictly related to code organization and testing.

To start, use Shiny modules to split the code into multiple files and organize it better.

After restructuring the Shiny app in the way proposed by Shiny Assistant, we ended up with the following project structure:

.
├── app.R
├── modules
│   ├── data_module.R
│   ├── map_module.R
│   ├── stats_module.R
│   ├── table_module.R
│   └── ui_module.R
└── www
    └── custom.css

It’s much better, maintainability-wise.

It’s also worth noting that Shiny Assistant forgot to set the working directory in `app.R`, which means the module imports won’t work out of the box. But that’s an easy fix.

After running the app, it seems like the global statistics work as before:

Image 22 - Shiny app after Prompt 12

But things go south if you try to view state-specific airport statistics:

Image 23 - Shiny app after Prompt 12 (2)

It’s time for another round of bux fixes!

Prompt #13 - Better Code Organization with Shiny Modules Bug Fix

The error message printed to the R console wasn’t super specific, but it pointed to the `data_module.R` file.

It was enough for Shiny Assistant to figure out what was the mistake and to rewrite the code in `data_module.R` and `app.R`:

The code you generated works when viewing "All" states, but throws an error on individual state level:
```
Warning: Error in : object 'input' not found
  114:  [modules/data_module.R#37]
   98: filtered_data
   97: ::
htmlwidgets
shinyRenderWidget [modules/map_module.R#49]
   96: func
   83: renderFunc
   82: output$map
    1: runApp
```

Fix this issue.
Image 24 - Shiny app after Prompt 13

The app is now working exactly as it did before modularization, so we can consider our work done.

Prompt #14 - Unit Tests

The last prompt for today has to do with unit tests.

We don’t want to write them manually, and instead, would like for Shiny Assistant to come up with test cases for every Shiny module.

This is the prompt we gave it:

That's it regarding the app!
Your final job for today is to create test cases. Test everything you think is relevant and show me how to run the tests.

And after including tests in our app, the project folder structure looked like the following:

.
├── app.R
├── modules
│   ├── data_module.R
│   ├── map_module.R
│   ├── stats_module.R
│   ├── table_module.R
│   └── ui_module.R
├── tests
│   ├── testthat
│   │   ├── test-data-module.R
│   │   ├── test-map-module.R
│   │   ├── test-stats-module.R
│   │   ├── test-table-module.R
│   │   └── test-ui-module.R
│   └── testthat.R
└── www
    └── custom.css

Shiny Assistant used the `testthat` package to write a series of tests.

It also gave us instructions on how to run the tests:

testthat::test_dir("tests/testthat")
Image 25 - R Shiny app testing progress

After unit testing has finished, you can see that some test cases passed and some failed:

Image 26 - R Shiny app testing results

But that has less to do with Shiny Assistant capabilities and more with the open and unspecific nature of our prompt.

We’re still quite satisfied with the results. Unit testing isn’t specific to R Shiny, but the Assistant still has no trouble showing us how to test a modularized Shiny application.

Shiny Application Source Code

This section contains the source code for the finished and modularized version of the app generated by Shiny Assistant.

You can copy this code and use it as-is. The only thing you need to tweak is the working directory. We’ve deliberately left the code in the original format, so you can see firsthand if the chosen Shiny Assistant code formatting styles match yours.

www/custom.css

.statistics-container {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.card {
  flex: 1 1 calc(25% - 1rem);
  min-width: 200px;
}

.stat-name {
  font-size: 1rem;
  color: #666;
}

.stat-value {
  font-size: 1.5rem;
  font-weight: bold;
  color: #333;
}

.map-card {
  width: 100%;
  padding: 0;
}

.map-card .card-body {
  padding: 0;
}

@media (max-width: 991px) {
  .card {
    flex: 1 1 calc(50% - 1rem);
  }
}

@media (max-width: 575px) {
  .card {
    flex: 1 1 100%;
  }
}

modules/data_module.R

library(dplyr)
library(sf)
library(maps)
library(stringr)

dataModule <- function(input) {
  # Read the CSV file
  airports <- read.csv("airports.csv")
  
  # Get US states map data
  us_states <- st_as_sf(maps::map("state", plot = FALSE, fill = TRUE))
  
  # Create a mapping of state names to abbreviations
  state_mapping <- data.frame(
    name = tolower(state.name),
    abbr = state.abb
  )
  
  # Add state abbreviations to us_states
  us_states <- us_states %>% 
    mutate(name = ID) %>%
    left_join(state_mapping, by = "name") %>%
    mutate(STATE = toupper(abbr),
           name = str_to_title(name))  # Convert state names to title case
  
  # Count airports per state
  airports_per_state <- airports %>%
    group_by(STATE) %>%
    summarise(count = n_distinct(AIRPORT))
  
  # Merge map data with airport counts
  us_states <- us_states %>%
    left_join(airports_per_state, by = "STATE") %>%
    mutate(count = ifelse(is.na(count), 0, count))  # Replace NA with 0
  
  filtered_data <- function() {
    if (input$state == "All") {
      airports
    } else {
      airports %>% filter(STATE == input$state)
    }
  }
  
  return(list(
    airports = airports,
    us_states = us_states,
    filtered_data = filtered_data
  ))
}

modules/map_module.R

library(leaflet)

mapModule <- function(input, output, session, filtered_data, us_states) {
  output$map_card <- renderUI({
    card(
      full_screen = TRUE,
      class = "map-card",
      leafletOutput("map", height = "400px")
    )
  })
  
  output$map <- renderLeaflet({
    if (input$state == "All") {
      # Choropleth map for all states
      pal <- colorNumeric("Blues", domain = us_states$count)
      
      leaflet(us_states, options = leafletOptions(scrollWheelZoom = FALSE)) %>%
        addTiles() %>%
        addPolygons(
          fillColor = ~pal(count),
          weight = 2,
          opacity = 1,
          color = "white",
          dashArray = "3",
          fillOpacity = 0.7,
          highlightOptions = highlightOptions(
            weight = 5,
            color = "#666",
            dashArray = "",
            fillOpacity = 0.7,
            bringToFront = TRUE
          ),
          label = ~paste0(name, ": ", count, " airports"),
          labelOptions = labelOptions(
            style = list("font-weight" = "normal", padding = "3px 8px"),
            textsize = "15px",
            direction = "auto"
          )
        ) %>%
        addLegend(
          pal = pal,
          values = ~count,
          opacity = 0.7,
          title = "Number of Airports",
          position = "bottomright"
        )
    } else {
      # Scatter map for individual state
      state_data <- filtered_data()
      state_bounds <- us_states %>% filter(STATE == input$state)
      
      leaflet() %>%
        addTiles() %>%
        addPolygons(data = state_bounds, fillColor = "lightblue", fillOpacity = 0.2, weight = 2, color = "blue") %>%
        addCircleMarkers(
          data = state_data,
          lng = ~LONGITUDE,
          lat = ~LATITUDE,
          radius = 5,
          fillOpacity = 0.8,
          color = "red",
          stroke = FALSE,
          label = ~paste(
            "Code:", IATA, "
", "Name:", AIRPORT, "
", "Lat:", LATITUDE, "
", "Lon:", LONGITUDE ) %>% lapply(htmltools::HTML), labelOptions = labelOptions(style = list("font-weight" = "normal", padding = "3px 8px")) ) %>% fitBounds( lng1 = min(state_data$LONGITUDE), lat1 = min(state_data$LATITUDE), lng2 = max(state_data$LONGITUDE), lat2 = max(state_data$LATITUDE) ) } }) }

modules/stats_module.R

statsModule <- function(input, output, session, airports) {
  output$statistics_cards <- renderUI({
    if (input$state == "All") {
      total_airports <- n_distinct(airports$AIRPORT)
      avg_airports_per_state <- round(total_airports / n_distinct(airports$STATE), 2)
      state_most_airports <- airports %>%
        group_by(STATE) %>%
        summarise(count = n_distinct(AIRPORT)) %>%
        arrange(desc(count)) %>%
        slice(1) %>%
        pull(STATE)
      state_least_airports <- airports %>%
        group_by(STATE) %>%
        summarise(count = n_distinct(AIRPORT)) %>%
        arrange(count) %>%
        slice(1) %>%
        pull(STATE)
      
      div(
        class = "statistics-container",
        card(
          full_screen = TRUE,
          card_body(
            div(class = "stat-name", "Total Airports"),
            div(class = "stat-value", total_airports)
          )
        ),
        card(
          full_screen = TRUE,
          card_body(
            div(class = "stat-name", "Avg Airports per State"),
            div(class = "stat-value", avg_airports_per_state)
          )
        ),
        card(
          full_screen = TRUE,
          card_body(
            div(class = "stat-name", "State with Most Airports"),
            div(class = "stat-value", state_most_airports)
          )
        ),
        card(
          full_screen = TRUE,
          card_body(
            div(class = "stat-name", "State with Least Airports"),
            div(class = "stat-value", state_least_airports)
          )
        )
      )
    }
  })
}

modules/table_module.R

library(reactable)

tableModule <- function(input, output, session, filtered_data, us_states) {
  output$table_card <- renderUI({
    card(
      full_screen = TRUE,
      max_width = "100%",
      card_body(
        reactableOutput("airport_table")
      )
    )
  })
  
  output$airport_table <- renderReactable({
    if (input$state == "All") {
      table_data <- us_states %>%
        st_drop_geometry() %>%
        select(name, count) %>%
        arrange(desc(count)) %>%
        rename("State" = name, "Number of Airports" = count)
      
      reactable(table_data,
                columns = list(
                  State = colDef(width = 200),
                  "Number of Airports" = colDef(width = 200)
                ),
                striped = TRUE,
                highlight = TRUE,
                bordered = TRUE,
                theme = reactableTheme(
                  borderColor = "#dfe2e5",
                  stripedColor = "#f6f8fa",
                  highlightColor = "#f0f5f9",
                  cellPadding = "8px 12px",
                  style = list(fontFamily = "-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif")
                )
      )
    } else {
      table_data <- filtered_data() %>%
        select(IATA, AIRPORT, LATITUDE, LONGITUDE) %>%
        rename("Airport Code" = IATA, "Airport Name" = AIRPORT)
      
      reactable(table_data,
                columns = list(
                  "Airport Code" = colDef(width = 120),
                  "Airport Name" = colDef(width = 250),
                  LATITUDE = colDef(width = 120),
                  LONGITUDE = colDef(width = 120)
                ),
                striped = TRUE,
                highlight = TRUE,
                bordered = TRUE,
                theme = reactableTheme(
                  borderColor = "#dfe2e5",
                  stripedColor = "#f6f8fa",
                  highlightColor = "#f0f5f9",
                  cellPadding = "8px 12px",
                  style = list(fontFamily = "-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif")
                )
      )
    }
  })
}

modules/ui_module.R

uiModule <- function(input, output, session, airports) {
  output$state_select <- renderUI({
    selectInput("state", "Select State:",
                choices = c("All", sort(unique(airports$STATE))),
                selected = "All")
  })
}

app.R

library(shiny)
library(bslib)

setwd("your-app-directory")

# Source the module files
source("modules/data_module.R")
source("modules/ui_module.R")
source("modules/map_module.R")
source("modules/table_module.R")
source("modules/stats_module.R")

ui <- page_sidebar(
  title = "Airport Information",
  includeCSS("www/custom.css"),
  sidebar = sidebar(
    div(
      class = "sidebar-content",
      uiOutput("state_select")
    )
  ),
  div(
    class = "main-content",
    uiOutput("statistics_cards"),
    uiOutput("map_card"),
    uiOutput("table_card")
  )
)

server <- function(input, output, session) {
  # Call the data module
  data <- reactive(dataModule(input))
  
  # Call the UI module
  uiModule(input, output, session, data()$airports)
  
  # Call the map module
  mapModule(input, output, session, data()$filtered_data, data()$us_states)
  
  # Call the table module
  tableModule(input, output, session, data()$filtered_data, data()$us_states)
  
  # Call the stats module
  statsModule(input, output, session, data()$airports)
}

shinyApp(ui, server)

Is Shiny Assistant Worth Your Time? Pros and Cons

After that behemoth of a section, what are our final thoughts on the beta version of Shiny Assistant?

We’ll give you an answer through a list of pros and cons. We’ll further split the cons to explain what’s specific to Shiny Assistant, and what ties more to Generative AI itself.

Shiny Assistant Pros

Shiny Assistant is an amazing tool that can help both beginner and seasoned Shiny developers. Here are the things we particularly loved about it:

  • It’s free - Yup, it knows as much about Shiny as the developers behind it, but won’t cost you a dime. You can add your Anthropic API key which will cost you on a per-request basis, but the tool itself is free of charge (at least as of September 2024).
  • It supports both R and Python - Shiny Assistant can build Shiny apps in R and Python. We haven’t covered the latter in today’s article, but plan to do so in the future. It will be interesting to compare the two since R’s version of Shiny has been around the block for approximately 10 years longer.
  • It supports a wider R Shiny ecosystem - This pro is possibly limited to R Shiny, but nevertheless, the Assistant knows how to work with packages frequently used with Shiny, such as Shiny Modules, Rhino, R6, and so on.
  • It reduces friction in building Shiny apps - The Assistant can’t do everything for you, but if you have some fundamental knowledge it can do most of the heavy lifting. In our example, it’s clear that we’re holding the Assistant by hand most of the time as we know exactly what we want. When our prompts were more open to interpretation, so were the results. Overall, if you have a general idea of what you want to accomplish, Shiny Assistant can create it, fast.
  • It increases your learning speed - You can leverage Shiny Assistant to learn new concepts and packages in a non-conventional way. Instead of going through package documentation, simply ask the Assistant to show you how to incorporate the needed functionality. You learn faster and develop faster - win-win!
  • Great for creating starter code - We found Shiny Assistant to be great for getting you 70% to the goal. The remaining 30% boils down to fixing bugs the Assistant can’t solve and fine-tuning and polishing application aspects that are faster to implement by hand.
  • Less Googling - R is a widely used language, but it still has a smaller community than Python or JavaScript. Googling R or R Shiny errors might not always result in a solution you can use out of the box. Shiny Assistant can fix most of the bugs, saving you time manually searching and custom tailoring a solution.

Generative AI Cons

Some aspects of Shiny Assistant rub us wrong and they have nothing to do with the tool. They are generic downsides to using Generative AI:

  • It shortens your attention span - If AI can write Shiny applications from scratch, there’s no need for you to invest too much concentration into the project. Writing text prompts in plain English is less involved than writing code. Everything is fine until AI can’t solve the bugs you’re running into, and you’re left examining hundreds of lines of code you haven’t written in the first place, not knowing where to start.
  • It’s just a matter of time before you hit a wall - When working with LLMs, the focus shifts from building to delegating and correcting mistakes. Delegating is still hard, but it’s the easier of the two. Fixing bugs can be frustrating and time-consuming, and it often requires a deeper understanding of Shiny.
  • It doesn’t save you that much time - This argument is twofold. First, you need to write and refine the prompts before submitting them to an AI model. It took us close to two hours to write the prompts for this app! And second, if you reach a point where AI can’t solve bug(s), you don’t only have to solve them yourself, but you also need to understand potentially messy AI-generated code.
  • Sensitive data - You should never share sensitive information about your company and security credentials with an AI tool. Shiny Assistant is no exception. Masking sensitive parts of your code takes time, both going in and out.

Shiny Assistant Cons

The tool is still in closed beta, so expect this list to change (and shorten) as time moves on.

Here are our gripes with Shiny Assistant after trying it for a couple of days:

  • Assistant modes - Right now, you can switch between “Code only”, “Concise”, and “Verbose”, with “Concise” being the default. We’ve opted for “Code only” for this article, but Shiny Assistant still generated a lot of explainer text. It’s not a biggie, but it’s a bug we have to mention. It looks like a basic prompting error, so it shouldn’t take the Posit’s team too much time to fix it.
  • Live preview sometimes breaks - You’ve seen it in the screenshots of the previous section. Again, it’s not a deal-breaker, but copying or downloading the code to a local machine takes time and breaks the seamlessness of the Assistant.
  • Code generation - Shiny Assistant always writes the entire application code from scratch. Since the model running in the background generates it token by token, it takes time, even for the simplest of prompts. You might consider this a real issue for more complex apps, but in our case, it was just a minor inconvenience.
  • It’s currently just a web app - We’d love to see Shiny Assistant as a plugin or an integral part of RStudio and/or Positron.

The last two points are more of a wishlist for a production version of Shiny Assistant.

Summing up Shiny Assistant

To conclude, we think most Shiny apps will be kicked off with Shiny Assistant in 2025 and beyond.

There’s no reason not to use the tool. If you’re starting from a blank slate, the Assistant will get you moving in minutes instead of hours. Even if you’ve already built an app from scratch, you can still use Shiny Assistant as an idea generator to explore new features and improvements to the existing ones.

We’d love to see Shiny Assistant as an integral part of data science IDEs, such as RStudio and Positron. Not only would that make it easier for Shiny developers, but it’ll give the Assistant a chance to generate and change only the code that’s needed at the moment instead of generating the entire app from scratch every time.

Long story short - join the waitlist if you haven’t already. Try it out.

We’d love to hear your thoughts on Shiny Assistant. Join our Slack community and share your experiences so far. It’s the best place to share prompts, code, and screenshots.

What else is new in the world of R and Data Science? Read our experiences from posit::conf 2024.

Have questions or insights?

Engage with experts, share ideas and take your data journey to the next level!

Sign up for ShinyWeekly

Join 4,2k explorers and get the Shiny Weekly Newsletter into your mailbox
for the latest in R/Shiny and Data Science.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Explore Possibilities

Share Your Data Goals with Us

From advanced analytics to platform development and pharma consulting, we craft solutions tailored to your needs.

Talk to our Experts
posit
shiny