diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index 5a91e2ef..5a9a2cd0 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,5 +1,7 @@ # querychat (development version) +* Added bookmarking support to `QueryChat$server()` and `querychat_app()`. When bookmarking is enabled (via `bookmark_store = "url"` or `"server"` in `querychat_app()` or `$app_obj()`, or via `enable_bookmarking = TRUE` in `$server()`), the chat state (including current query, title, and chat history) will be saved and restored with Shiny bookmarks. (#107) + * Nearly the entire functional API (i.e., `querychat_init()`, `querychat_sidebar()`, `querychat_server()`, etc) has been hard deprecated in favor of a simpler OOP-based API. Namely, the new `QueryChat$new()` class is now the main entry point (instead of `querychat_init()`) and has methods to replace old functions (e.g., `$sidebar()`, `$server()`, etc). (#109) * In addition, `querychat_data_source()` was renamed to `as_querychat_data_source()`, and remains exported for a developer extension point, but users no longer have to explicitly create a data source. (#109) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 3c214b2d..de998397 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -309,7 +309,9 @@ QueryChat <- R6::R6Class( } server <- function(input, output, session) { - qc_vals <- self$server() + # Enable bookmarking if bookmark_store is enabled + enable_bookmarking <- bookmark_store %in% c("url", "server") + qc_vals <- self$server(enable_bookmarking = enable_bookmarking) output$query_title <- shiny::renderText({ if (shiny::isTruthy(qc_vals$title())) { @@ -432,6 +434,12 @@ QueryChat <- R6::R6Class( #' the reactive logic for the chat interface and returns session-specific #' reactive values. #' + #' @param enable_bookmarking Whether to enable bookmarking for the chat + #' state. Default is `FALSE`. When enabled, the chat state (including + #' current query, title, and chat history) will be saved and restored + #' with Shiny bookmarks. This requires that the Shiny app has bookmarking + #' enabled via `shiny::enableBookmarking()` or the `enableBookmarking` + #' parameter of `shiny::shinyApp()`. #' @param session The Shiny session object. #' #' @return A list containing session-specific reactive values and the chat @@ -446,14 +454,17 @@ QueryChat <- R6::R6Class( #' qc <- QueryChat$new(mtcars, "mtcars") #' #' server <- function(input, output, session) { - #' qc_vals <- qc$server() + #' qc_vals <- qc$server(enable_bookmarking = TRUE) #' #' output$data <- renderDataTable(qc_vals$df()) #' output$query <- renderText(qc_vals$sql()) #' output$title <- renderText(qc_vals$title() %||% "No Query") #' } #' } - server = function(session = shiny::getDefaultReactiveDomain()) { + server = function( + enable_bookmarking = FALSE, + session = shiny::getDefaultReactiveDomain() + ) { if (is.null(session)) { rlang::abort( "$server() must be called within a Shiny server function." @@ -464,7 +475,8 @@ QueryChat <- R6::R6Class( self$id, data_source = private$.data_source, greeting = self$greeting, - client = private$.client + client = private$.client, + enable_bookmarking = enable_bookmarking ) }, diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index 8e3dc20c..b721a2ea 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -20,10 +20,17 @@ mod_ui <- function(id, ...) { } # Main module server function -mod_server <- function(id, data_source, greeting, client) { +mod_server <- function( + id, + data_source, + greeting, + client, + enable_bookmarking = FALSE +) { shiny::moduleServer(id, function(input, output, session) { current_title <- shiny::reactiveVal(NULL, label = "current_title") current_query <- shiny::reactiveVal("", label = "current_query") + has_greeted <- shiny::reactiveVal(FALSE, label = "has_greeted") filtered_df <- shiny::reactive(label = "filtered_df", { execute_query(data_source, query = DBI::SQL(current_query())) }) @@ -56,20 +63,26 @@ mod_server <- function(id, data_source, greeting, client) { # Prepopulate the chat UI with a welcome message that appears to be from the # chat model (but is actually hard-coded). This is just for the user, not for # the chat model to see. - greeting_content <- if (!is.null(greeting) && any(nzchar(greeting))) { - greeting - } else { - # Generate greeting on the fly if none provided - rlang::warn(c( - "No greeting provided; generating one now. This adds latency and cost.", - "i" = "Consider using $generate_greeting() to create a reusable greeting." - )) - chat_temp <- client$clone() - prompt <- "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." - chat_temp$stream_async(prompt) - } + shiny::observe(label = "greet_on_startup", { + if (has_greeted()) { + return() + } - shinychat::chat_append("chat", greeting_content) + greeting_content <- if (!is.null(greeting) && any(nzchar(greeting))) { + greeting + } else { + # Generate greeting on the fly if none provided + rlang::warn(c( + "No greeting provided; generating one now. This adds latency and cost.", + "i" = "Consider using $generate_greeting() to create a reusable greeting." + )) + prompt <- "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list." + chat$stream_async(prompt) + } + + shinychat::chat_append("chat", greeting_content) + has_greeted(TRUE) + }) append_stream_task <- shiny::ExtendedTask$new( function(client, user_input) { @@ -94,6 +107,28 @@ mod_server <- function(id, data_source, greeting, client) { current_title(input$chat_update$title) }) + if (enable_bookmarking) { + shinychat::chat_restore("chat", chat, session = session) + + shiny::onBookmark(function(state) { + state$values$querychat_sql <- current_query() + state$values$querychat_title <- current_title() + state$values$querychat_has_greeted <- has_greeted() + }) + + shiny::onRestore(function(state) { + if (!is.null(state$values$querychat_sql)) { + current_query(state$values$querychat_sql) + } + if (!is.null(state$values$querychat_title)) { + current_title(state$values$querychat_title) + } + if (!is.null(state$values$querychat_has_greeted)) { + has_greeted(state$values$querychat_has_greeted) + } + }) + } + list( client = chat, sql = current_query, diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 6612e235..f3e9c14b 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -150,7 +150,7 @@ ui <- fluidPage( qc <- QueryChat$new(mtcars, "mtcars") server <- function(input, output, session) { - qc_vals <- qc$server() + qc_vals <- qc$server(enable_bookmarking = TRUE) output$data <- renderDataTable(qc_vals$df()) output$query <- renderText(qc_vals$sql()) @@ -495,12 +495,22 @@ This method must be called within a Shiny server function. It sets up the reactive logic for the chat interface and returns session-specific reactive values. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$server(session = shiny::getDefaultReactiveDomain())}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{QueryChat$server( + enable_bookmarking = FALSE, + session = shiny::getDefaultReactiveDomain() +)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ +\item{\code{enable_bookmarking}}{Whether to enable bookmarking for the chat +state. Default is \code{FALSE}. When enabled, the chat state (including +current query, title, and chat history) will be saved and restored +with Shiny bookmarks. This requires that the Shiny app has bookmarking +enabled via \code{shiny::enableBookmarking()} or the \code{enableBookmarking} +parameter of \code{shiny::shinyApp()}.} + \item{\code{session}}{The Shiny session object.} } \if{html}{\out{
}} @@ -521,7 +531,7 @@ client with the following elements: qc <- QueryChat$new(mtcars, "mtcars") server <- function(input, output, session) { - qc_vals <- qc$server() + qc_vals <- qc$server(enable_bookmarking = TRUE) output$data <- renderDataTable(qc_vals$df()) output$query <- renderText(qc_vals$sql())