-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshiny_assist.qmd
490 lines (387 loc) · 17.7 KB
/
shiny_assist.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# Shiny Assistant {#sec-shiny-assist}
```{r}
#| eval: true
#| echo: false
#| include: false
source("_common.R")
```
```{r}
#| label: co_box_dev
#| echo: false
#| results: asis
#| eval: true
co_box(
color = "y",
look = "default",
header = "Caution",
contents = "The contents for this section are being developed. Thank you for your patience."
)
```
```{=html}
<!--
https://shiny.posit.co/blog/posts/shiny-assistant/
https://gallery.shinyapps.io/assistant/
-->
```
The [Shiny Assistant](https://gallery.shinyapps.io/assistant/) is an AI-powered chat bot anyone can use to help them build a Shiny application.
> "*you can ask it questions about Shiny, or to create a Shiny application from scratch, or ask it to make changes to an existing application.*" - [Shiny Assistant](https://shiny.posit.co/blog/posts/shiny-assistant/), Winston Chang
If you're new to Shiny, or you have an app you'd like to add features to, I highly recommend using the Shiny Assistant.
{fig-align="center" width="100%"}
The UI for Shiny Assistant looks like a standard chat, but if you ask it to "Open the editor", we see an interface with a script editor, console, and viewer pane:
{fig-align="center" width="100%"}
Shiny Assistant is great for trying different UI components, layouts, and styling (color palettes, themes, fonts, etc.). We're going to walk through using the assistant for building an Shiny app-package, but we'll limit the number of prompts to five to keep this chapter brief. However, I still think you'll find the Assistant is a helpful tool to have in your Shiny toolbox.
## Prompt 1: Build an app
Let's start with trying to reproduce some of the applications we have in `sap`. I typically have data before I have an idea for a dashboard or app, so we'll pass the structure of simplified version our `movies` data to the Assistant with some instructions on what we'd like to see:[^movies-sample]
[^movies-sample]: I've included a .csv version of these data in the [`data-raw/` folder of this branch](https://raw.githubusercontent.com/mjfrigaard/sap/refs/heads/23_llm-shiny-assist/data-raw/movies_sample.csv).
**Prompt**: <code>Create a shiny application that visualizes IMDB movie data using a scatter plot. Include inputs for x, y, color, size, and transparency.
The column names are below:</code>
````{=html}
<pre>
```
Rows: 651
Columns: 18
$ title <chr> …
$ genre <fct> …
$ runtime <dbl> …
$ year <dbl> …
$ top200_box <fct> …
$ director <chr> …
$ mpaa_rating <fct> …
$ imdb_rating <dbl> …
$ critics_rating <fct> …
$ audience_rating <fct> …
$ imdb_num_votes <int> …
$ critics_score <dbl> …
$ audience_score <dbl> …
$ best_pic_nom <fct> …
$ best_pic_win <fct> …
$ best_actor_win <fct> …
$ best_actress_win <fct> …
$ best_dir_win <fct> …
```
</pre>
````
<code>Use the data stored in this URL:</code>
```{=html}
<pre>
https://raw.githubusercontent.com/mjfrigaard/sap/refs/heads/23_llm-shiny-assist/data-raw/movies_sample.csv
</pre>
```
```{r}
#| label: co_box_url_data
#| echo: false
#| results: asis
#| eval: true
co_box(
color = "g",
look = "default", hsize = "1.15", size = "1.10", fold = FALSE,
header = "Tip: Providing data",
contents = "I've found providing a data structure (from `str()` or `dplyr::glimpse()`) is incredibly helpful when guiding AI chat bots on building dashboards. [Shiny Assistant](https://gallery.shinyapps.io/assistant/) is great because we can even pass it a URL with a .csv data file."
)
```
Below we can see the initial app:
{fig-align="center" width="100%"}
The initial application has nearly identical input, outputs, and layout. However, we can see an additional checkbox for **Show Movie Titles** and the **Size by** input has been mapped to a variable (not a numerical value).
In our next prompt, we'll refine the app to more closely match what we have in our `sap` app-package.
## Prompt 2: Refine
**Prompt**: <code> Change the size to a numeric slider and include a text input for a plot title. </code>
The application is updated with the changes and we can view the changes to `app.R` in the Shiny Assistant UI:
{fig-align="center" width="100%"}
```{r}
#| label: co_box_aes_string
#| echo: false
#| results: asis
#| eval: true
co_box(
color = "y",
look = "default", hsize = "1.15", size = "1.10", fold = FALSE,
header = "Warning: `aes_string()`",
contents = "
We can see the following warning regarding the `ggplot2` graph:
{fig-align='center' width='100%'}
We avoided this warning with the `scatter_plot()` utility function in previous branches, but for now we will just note this is something we should return to and refactor after our application in a more complete state.
")
```
## Prompt 3: Functionalize
Now that we have a UI with the correct inputs and outputs, we'll convert these into modules and separate the application into it's respective components.
**Prompt**: <code> Convert the inputs and outputs into modules and create a standalone app function that calls separate UI and server functions. </code>
The response to this prompt won't change the app, but we're given a summary of the changes in the chat:
{fig-align="center" width="100%"}
> I've modified the code to use a modular structure following these changes:
>
> 1. Created separate modules for:
>
> - Controls (inputs) - `controlsUI` and `controlsServer`
>
> - Plot (output) - `plotOutputUI` and `plotOutputServer`
>
> - About data section - `aboutDataUI` (UI only as it's static)
>
> 2. Created standalone UI and server functions:
>
> - `ui_function()` - assembles all UI components
>
> - `server_function()` - coordinates between modules
>
> 3. Created an application function:
>
> - `movie_visualization_app()` - bundles everything together
>
> 4. The communication between modules:
>
> - The controls module returns all inputs as a reactive list
>
> - The plot module takes two parameters: the data and controls reactives
>
> This modular approach makes the code more maintainable and easier to extend. Each component has a well-defined responsibility, and they communicate through clear interfaces.
I've left the Shiny Assistant chat setting to **Verbose**, but this can be changed to return less descriptive details on the code being produced:

## Prompt 4: Structure
Now we're reading to start structure the application as an R package. We'll attempt to create the files and documentation with a single prompt.
**Prompt**: <code> Store the modules, UI, server, and standalone app functions in separate files and include roxygen2 documentation. </code>
This results in an error when the Shiny Assistant tries to run our updates:
{fig-align="center" width="100%"}
The summary tells us what changed, but doesn't give us advice on (or even acknowledge) the error.
> I've reorganized the code into separate files with roxygen2 documentation:
>
> 1. **controls_module.R**: Contains the UI and server functions for the controls module with full roxygen2 documentation including parameters, return values, and imported functions.
>
> 2. **plot_module.R**: Contains the UI and server functions for the plot output module with documentation for both components.
>
> 3. **about_module.R**: Contains the UI function for the about data section.
>
> 4. **ui.R**: Contains the main UI function that assembles all UI components.
>
> 5. **server.R**: Contains the main server function that coordinates between modules.
>
> 6. **app.R**: Contains the standalone app function that sources all other files and runs the application.
>
> Each function includes proper roxygen2 documentation with:
>
> - Function descriptions
>
> - Parameter descriptions
>
> - Return value descriptions
>
> - \@importFrom tags for explicit dependencies
>
> - \@export tags for functions that should be exported
>
> The app.R file also includes logic to run the app only if the file is being executed directly in an interactive session.
## Prompt 5: Error
The error above tells us the `bslib::page_sidebar()` function isn't being loaded before `runApp()` is being called, but the call to `library(bslib)` is in the `app.R` file. We'll see if providing some direction on function/file names and where to call `shinyApp()` will resolve this error:
**Prompt**: <code> An error occurred because the `page_sidebar` function could not be found. The `app.R` file needs to contain the `library()` and `source()` calls before the call to `shinyApp()`. Also, rename the `ui.R` and `server.R` to `app_ui.R` and `app_server.R`. </code>
This results in the following changes to the app code structure:
> I've made the following changes to fix the issues:
>
> 1. Renamed the files:
>
> - Changed `ui.R` to `app_ui.R`
>
> - Changed `server.R` to `app_server.R`
>
> 2. Modified the `app.R` file to:
>
> - Load all required libraries at the beginning of the file
>
> - Source all module and function files
>
> - Define the `movie_visualization_app()` function
>
> - Call the function directly (removed the `if (interactive())` check to ensure it runs)
>
> 3. Maintained the same structure for all modules and functions
>
> These changes ensure that:
>
> 1. All necessary files are sourced before using their functions
>
> 2. The application will properly find all functions when running
>
> 3. The organization follows a more standard Shiny module structure
>
> The application should now run without the "page_sidebar function could not be found" error since we're properly sourcing all the module files before they're used in the UI and server functions.
In the `app.R` file, if we explicitly run the call to `library(bslib)` and then `source("app_ui.R")` (which contains the call to `page_sidebar()`), we see these are executed without errors in the **Console**:
{fig-align="center" width="100%"}
However, when we try to source and call the standalone app function, we see the following error:
{fig-align="center" width="100%"}
```{verbatim}
Error in runApp(x) :
Can't call `runApp()` from within `runApp()`. If your application code contains
`runApp()`, please remove it.
```
This error is a little tricky, because we're calling `shinyApp()`, but this is happening because we're trying to explicitly call `shinyApp()` (and the UI works by call `runApp()`).
We can fix this by clicking on the icon in the UI to run the app:
{fig-align="center" width="100%"}
## Bonus: Theme
Now that we have our modules, app functions and `roxygen2` documentation, we'll finish by adding a dark [`thematic` theme](https://rstudio.github.io/thematic/):
**Prompt**: <code> Add a dark theme using the thematic package: https://rstudio.github.io/thematic/ </code>
The updated app includes a dark theme:
{fig-align="center" width="100%"}
And the summary of changes is below:
> I've integrated the thematic package to add a dark theme to the application. Here are the changes I made:
>
> 1. Added `library(thematic)` to the app.R file to load the thematic package.
>
> 2. Added `thematic_shiny(font = "auto")` to automatically adjust plot themes to match the app theme. This automatically styles the ggplot2 visualizations to match the Shiny app's theme.
>
> 3. Modified the UI function in app_ui.R to use a dark theme:
>
> - Created a dark theme using `bs_theme()` from bslib with dark background colors, light text, and appropriate accent colors
>
> - Applied the theme to the `page_sidebar()` using the `theme` parameter
>
> 4. Updated the plot_module.R to work with thematic:
>
> - Removed the explicit `theme_minimal()` call since thematic will handle the base theming
>
> - Kept specific theme adjustments for title styling and legend positioning
>
> The thematic package now automatically coordinates the ggplot2 theme with the application's dark theme, creating a cohesive dark mode experience throughout the entire application. The plots will automatically inherit the dark background and appropriate colors for text and elements.
After confirming the new theme works, we'll download the app files by clicking on the cloud icon in the upper right corner:
{fig-align="center" width="70%"}
## Running locally
I've placed the downloaded files in the `inst/shiny-assist/movies` folder in `sap`, and after some minor changes to the files/folder organization, we can see the app-package structure starting to take shape:
```{verbatim}
inst/shiny-assist/
└── movies
├── R
│ ├── about_module.R
│ ├── app_server.R
│ ├── app_ui.R
│ ├── controls_module.R
│ ├── movie_visualization_app.R
│ ├── movies_sample.rds
│ └── plot_module.R
└── app.R
3 directories, 8 files
```
The `app.R` file no longer needs to `source()` the modules and app functions if they are placed in an `R/` folder.[^shiny-assist-loadsupport]
[^shiny-assist-loadsupport]: `loadSupport()` was covered back in @sec-shiny-folders.
We can also move the standalone app function into it's own file in the `R/` folder, so `app.R` only loads the packages, theme, and launches the app:
```{r}
#| eval: false
#| code-fold: false
# Load required libraries
library(shiny)
library(ggplot2)
library(dplyr)
library(readr)
library(bslib)
library(thematic)
# Setup thematic to automatically adjust plot theme to match app theme
thematic_shiny(font = "auto")
# Run the application
movie_visualization_app()
```
In Positron, we can see the **Run App** icon is present at the top of `app.R`, and it's fully functional:
{fig-align="center" width="100%"}
Finally, we'll add this app to our `launch_app()` function:
```{r}
#| eval: false
#| code-fold: false
#' Launch the Movies Review Application
#'
#' Starts the Movies Review Shiny application, which provides a customizable
#' scatter plot interface for analyzing movie data.
#'
#' @param app which app to run. Options are:
#' * `NULL` = the default app (`"movies"`)
#' * `"bslib"` = alternative `bslib` layout
#' * `"ggp2"` = `ggplot2movies` (tidy) data app.
#' * `"quarto"` = Quarto movies app.
#' * `"assist"` = Application built with [Shiny Assistant](https://gallery.shinyapps.io/assistant/#).
#' @param options arguments to pass to `options()`
#' @param run where to launch app:
#' * `p` = launch in viewer pane
#' * `b` = launch in external browser
#' * `w` = launch in window
#' @param ... arguments passed to UI
#'
#' @return A **Shiny application** object.
#'
#' @section Details:
#' The application uses:
#' - **UI**: Defined in [`movies_ui()`].
#' - **Server Logic**: Defined in [`movies_server()`].
#'
#' @seealso
#' - [`movies_ui()`] for the user interface.
#' - [`movies_server()`] for the server logic.
#'
#' @family **Standalone Application**
#'
#' @details
#' See the [ggplot2movies](https://github.com/hadley/ggplot2movies) package.
#'
#' @export
#'
#' @import shiny
#'
launch_app <- function(app = NULL, options = list(), run = "p", ...) {
if (interactive()) {
display_type(run = run)
}
if (is.null(app)) {
app <- "movies"
}
logr_msg(glue::glue("Launching app: {app}"),
level = "INFO"
)
tryCatch({
if (app == "bslib") {
shinyApp(
ui = movies_ui(bslib = TRUE),
server = movies_server,
options = options
)
} else if (app == "ggp2") {
shinyAppDir(
appDir = system.file("tidy-movies", package = "sap"),
options = options
)
} else if (app == "quarto") {
quarto::quarto_preview(
system.file("quarto", "index.qmd", package = "sap"),
render = "all"
)
} else if (app == "assist") {
shinyAppDir(
appDir = system.file("shiny-assist", package = "sap"),
options = options
)
} else {
shinyApp(
ui = movies_ui(...),
server = movies_server,
options = options
)
}
}, error = function(e) {
logr_msg(glue::glue("FATAL: Application failed to launch. Reason: {e$message}"),
level = "FATAL"
)
stop("Application launch failed. Check logs for details.")
}
)
}
```
## Recap {.unnumbered}
This chapter covered building a Shiny app 'fit for a package' using the Shiny Assistant. As you can see, in just a few prompts, we can have a Shiny application that ~80% ready to be loaded, installed, and launched. The callout box below includes a few items to watch out for when using Shiny Assistant to build apps:
```{r}
#| label: co_box_recap
#| echo: false
#| results: asis
#| eval: true
co_box(
color = "g",
fold = FALSE,
look = "default", hsize = "1.10", size = "1.05",
header = "Recap: Shiny Assistant",
contents = "
+ **`ggplot2` (`aes_string()`) warning**: as noted above,
+ **`roxygen2` documentation**: the `@importFrom` and `@import` tags...
+ **module structure**: the Assistant came up with a different module structure than we have in our movies app in `sap`...
")
```