From b4c75222cc7e361e07f6311452c2ee956ae805db Mon Sep 17 00:00:00 2001 From: federiva Date: Fri, 29 Nov 2024 05:57:12 -0300 Subject: [PATCH 1/5] refactor: Extract to files --- inst/assets/custom_css.css | 52 +++++++++++++++++++ inst/assets/custom_js.js | 104 +++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 inst/assets/custom_css.css create mode 100644 inst/assets/custom_js.js diff --git a/inst/assets/custom_css.css b/inst/assets/custom_css.css new file mode 100644 index 0000000..1e94dec --- /dev/null +++ b/inst/assets/custom_css.css @@ -0,0 +1,52 @@ +/* Make tab content scrollable */ +.tab-content { + /* Enable vertical scrolling */ + overflow-y: auto; +} + +/* Style for custom tab content background */ +.custom-tab-content { + background-color: #f5f5f5; + padding-bottom: 5px; + border-radius: 4px; + margin-bottom: 20px; +} + +/* Reduce padding around close actionButton */ +.close-tab { + border: none; + padding: 5px; + margin: 5px; + color: red; +} + +/* Waiter */ +.waiter-overlay { + position: fixed; + height: 919px; + width: 1375px; + top: 0px; + left: 0px; + background-color: rgba(51, 62, 72, 0.5) !important; +} + +.title-container { + text-align: center; + + .title-text { + font-size: 2em; + font-weight: bold; + } + + .help-icon-container { + font-size: 1.5em; + margin-left: 10px; + cursor: pointer; + } +} + +.subtitle-container { + .subtitle-text { + font-size: 15px; + } +} diff --git a/inst/assets/custom_js.js b/inst/assets/custom_js.js new file mode 100644 index 0000000..cb73954 --- /dev/null +++ b/inst/assets/custom_js.js @@ -0,0 +1,104 @@ +// Initialize a variable to mirror the values of 'aiAssist', +// 'openInShiny' and 'openInRStudio' +var currentAiAssistValue = null; +var currentOpenInShinyValue = null; +var currentOpenInRstudioValue = null; + +function aiAssist(function_name) { + console.log('Executing JavaScript function aiAssist'); + console.log('Function name: ' + function_name); + + // Check if the current value is the same as 'function_name' + if (currentAiAssistValue === function_name) { + + // Temporarily set 'aiAssist' and 'close_tab' to null + // to ensure the change is detected + Shiny.setInputValue('aiAssist', null, {priority: 'event'}); + Shiny.setInputValue('close_tab', null, {priority: 'event'}); + + // Use setTimeout to ensure the null value is processed before setting + // the new value + setTimeout(function() { + Shiny.setInputValue('aiAssist', function_name, {priority: 'event'}); + }, 10); + + } else { + + // Directly set 'aiAssist' to 'function_name' + Shiny.setInputValue('aiAssist', function_name); + } + + // Update the mirror variable to reflect the new value + currentAiAssistValue = function_name; +} + +function openInShiny(file_location) { + console.log('Executing JavaScript function openInShiny'); + console.log('File location: ' + file_location); + + // Check if the current value is the same as 'file_location' + if (currentOpenInShinyValue === file_location) { + + // Temporarily set 'openInShiny' and 'close_tab' to null + // to ensure the change is detected + Shiny.setInputValue('openInShiny', null, {priority: 'event'}); + Shiny.setInputValue('close_tab', null, {priority: 'event'}); + + // Use setTimeout to ensure the null value is processed before setting + // the new value + setTimeout(function() { + Shiny.setInputValue('openInShiny', file_location, {priority: 'event'}); + }, 10); + } else { + + // Directly set 'openInShiny' to 'file_location' if the values are + // different + Shiny.setInputValue('openInShiny', file_location, {priority: 'event'}); + } + + // Update the mirror variable to reflect the new value + currentOpenInShinyValue = file_location; +} + +function openInRStudio(file_location) { + console.log('Executing JavaScript function openInRStudio'); + console.log('File location: ' + file_location); + + // Check if the current value is the same as 'file_location' + if (currentOpenInRstudioValue === file_location) { + + // Temporarily set 'openInRStudio' to null to ensure the change is + // detected + Shiny.setInputValue('openInRStudio', null, {priority: 'event'}); + + // Use setTimeout to ensure the null value is processed before setting + // the new value + setTimeout(function() { + Shiny.setInputValue('openInRStudio', file_location, {priority: 'event'}); + }, 10); + + } else { + + // Directly set 'openInRStudio' to 'file_location' + Shiny.setInputValue('openInRStudio', file_location); + } + + // Update the mirror variable to reflect the new value + currentOpenInRstudioValue = file_location; +} + +$(document).on('shiny:connected', function(event) { + function adjustTabHeight() { + var windowHeight = $(window).height(); + // Distance from the top of the viewport + var offsetTop = $('#fileTabs').offset().top; + // Subtract any additional margin or padding + var tabHeight = windowHeight - offsetTop - 20; + $('.tab-content').css('height', tabHeight + 'px'); + } + + // Adjust the height on window resize + $(window).resize(adjustTabHeight); + // Initial adjustment + adjustTabHeight(); +}); From 741a96db5f383c402786cd20a85872c5ac5d3699 Mon Sep 17 00:00:00 2001 From: federiva Date: Fri, 29 Nov 2024 05:57:56 -0300 Subject: [PATCH 2/5] refactor: Extract to files fix: Use shiny utilities feat: Use bslib fix: Extract help to notification/toast --- R/app_server.R | 401 ++++++++++++++++++++++++++++ R/app_ui.R | 92 +++++++ R/project_visualiser.R | 592 +---------------------------------------- 3 files changed, 495 insertions(+), 590 deletions(-) create mode 100644 R/app_server.R create mode 100644 R/app_ui.R diff --git a/R/app_server.R b/R/app_server.R new file mode 100644 index 0000000..1de6165 --- /dev/null +++ b/R/app_server.R @@ -0,0 +1,401 @@ +help_text <- div( + class = "subtitle-container", + span( + class = "subtitle-text", + "Hover over nodes for more information.", br(), + "Functions without a test are ", span(class="text-danger fw-bolder", "red"), + "and those with a test are ", span(class="text-success fw-bolder", "green"), ". ", + br(), + "Click on", shiny::icon( + name = "robot", + style = "color: #337ab7; margin-right: 5px; margin-left: 5px;" + ), + " to request an AI generated summary of the corresponding function.", br(), + shiny::icon( + name = "up-right-from-square", + style = "color: #75AADB; margin-right: 5px; margin-left: 5px;" + ), + " to open the file in RStudio, or", br(), + shiny::icon( + name = "eye", + style = "color: #337ab7; margin-right: 5px; margin-left: 5px;" + ), + " to request a visualisation of the function's call graph." + ) +) + +#' Remove artefacts from file path +#' +#' @param file_location Character scalar specifying the path of a file. +#' @param project_path Character scalar specifying the path of the project. +#' +#' @return A character scalar +#' +#' @importFrom here here +#' +#' @examples +#' \dontrun{ +#' cleaned_file_path <- get_function_path( +#' file_location = "tests/testthat/example_project/R/calculate_QALYs.R#L41" +#' ) +#' cleaned_file_path <- get_function_path( +#' file_location = c( +#' "tests/testthat/example_project/R/calculate_QALYs.R#L41", +#' "tests/testthat/example_project/R/calculate_QALYs.R#L49" +#' ) +#' ) +#' } +get_function_path <- function(file_location, project_path) { + + get_function_path <- gsub("#.*", "", file_location) + + full_file_path <- ifelse( + test = is.na(get_function_path), + yes = "", + no = paste0(project_path, "/", get_function_path) + ) + + return(full_file_path) +} + +#' Extract function line in file path +#' +#' @param file_location Character scalar specifying the path of a file. +#' +#' @return A numeric scalar +#' +#' @examples +#' \dontrun{ +#' cleaned_function_line <- get_function_line( +#' file_location = "tests/testthat/example_project/R/calculate_QALYs.R:L41" +#' ) +#' cleaned_function_line <- get_function_line( +#' file_location = c( +#' "tests/testthat/example_project/R/calculate_QALYs.R#L41", +#' "tests/testthat/example_project/R/calculate_QALYs.R#L49" +#' ) +#' ) +#' } +get_function_line <- function(file_location) { + + function_line <- gsub(".*#L", "", file_location) + + function_line <- ifelse( + test = is.na(function_line), + yes = -1L, + no = as.numeric(function_line) + ) + + return(function_line) +} + +#' Create closable shiny tab +#' +#' @param tab_name Character scalar representing the name or title of the shiny +#' tab. +#' @param content_output_Id Character scalar representing the id of the shiny +#' tab. +#' @param output_type Character scalar specifying the type of rendered output. +#' Default is `"text"` and can also accept `"HTML"`. +#' +#' @return A tab that can be passed to `shiny::tabsetPanel()` +make_closable_tab <- function( + tab_name, + content_output_Id, + output_type = "text") { + shiny::tabPanel( + title = shiny::div( + style = "display: flex; justify-content: space-between; align-items: center;", + shiny::span( + shiny::HTML( + paste0("", tab_name, "") + ), + style = "flex-grow: 1;" + ), + shiny::actionButton( + inputId = "close", + label = shiny::HTML( + '' + ), + class = "close-tab", + onclick = sprintf( + "Shiny.setInputValue('close_tab', '%s');", + content_output_Id + ) + ) + ), + shiny::div( + class = "custom-tab-content", + if(output_type != "HTML") { + shiny::verbatimTextOutput(outputId = content_output_Id) + } else { + shiny::htmlOutput(outputId = content_output_Id) + } + ), + value = content_output_Id + ) +} + +#' Create Shiny app server logic +#' +#' @inheritParams run_shiny_app +#' @param foo_path path to the function folder +#' @importFrom shinyWidgets show_toast +#' @importFrom shinyjs onclick +#' +#' @return Shiny app server logic +app_server <- function(network_object, project_path, foo_path) { + function(input, output, session) { + + # Create a new environment to avoid sourcing scripts into the namespace + pkg_env <- new.env(parent = baseenv()) + + # Load all functions into this environment + load_functions_into_env(path = foo_path, env = pkg_env) + + # Keep track of the current tab's output ID + currentTabId <- shiny::reactiveVal(NULL) + + # Keep track of the number of AI-generated summary requests + aiAssit_calls <- shiny::reactiveVal(0) + + # Render the network visual + output$networkPlot <- visNetwork::renderVisNetwork(network_object) + + onclick("question-icon-click", { + show_toast( + title = "Help", + text = help_text, + type = "info", + timer = 30000, + width = "500px" + ) + }) + + # Observer to handle AI response in a new tab within the shiny app + shiny::observeEvent( + ignoreNULL = TRUE, + ignoreInit = TRUE, + eventExpr = input$aiAssist, + handlerExpr = { + # If there's a current tab open, remove it + if (!is.null(currentTabId())) { + shiny::removeTab( + inputId = "fileTabs", + target = currentTabId() + ) + } + + if(input$aiAssist != "") { + function_name <- input$aiAssist + tab_name <- paste(function_name) + + # Check if the function name refers to an existing function + if (is.function(get(x = function_name, envir = pkg_env))) { + # Check if the number of calls did not exceed a maximum: + if(aiAssit_calls() < 5) { + # Waiter + waiter <- waiter::Waiter$new( + html = shiny::div( + style = "display: flex; + flex-direction: column; + align-items: center; + justify-content:center; + color: white; + opacity: 1 !important;", + shiny::h4("Please wait ..."), + shiny::h6(paste( + "generating", function_name, "function summary ..." + )), + shiny::br(), + shiny::br(), + waiter::spin_wandering_cubes() + ), + hide_on_render = FALSE + ) + waiter$show() + on.exit(waiter$hide()) + + # Set the ID of the new tab to be the current one + currentTabId(tab_name) + + # Query AI: + ai_response <- summarise_function_with_LLM( + foo_name = function_name, + llm_api_url = Sys.getenv("LLM_API_URL"), + llm_api_key = Sys.getenv("LLM_API_KEY"), + envir = pkg_env + ) + + # Create the new tab and output its content + output[[tab_name]] <- shiny::renderUI({ + shiny::HTML(ai_response) + }) + + # Update the number of calls + aiAssit_calls(aiAssit_calls() + 1) + + # Dynamically adjust column widths + shinyjs::runjs( + '$("#mainColumn").removeClass("col-sm-11").addClass("col-sm-6");' + ) + shinyjs::runjs( + '$("#tabColumn").removeClass("col-sm-1").addClass("col-sm-6");' + ) + + # Insert the new tab and set it to the current + shiny::insertTab( + inputId = "fileTabs", + make_closable_tab( + tab_name = paste("AI summary -", tab_name), + content_output_Id = tab_name, + output_type = "HTML" + ), + select = TRUE + ) + + } else { + # Reset the current tab ID + currentTabId(NULL) + # Reset columns width + shinyjs::runjs( + '$("#mainColumn").removeClass("col-sm-6").addClass("col-sm-11");' + ) + shinyjs::runjs( + '$("#tabColumn").removeClass("col-sm-6").addClass("col-sm-1");' + ) + # Notify the user if the file is not found + shiny::showNotification( + ui = "You have exceeded the allotted AI queries.", + type = "warning" + ) + } + } else { + # Notify the user if the file is not found + shiny::showNotification( + ui = paste("Function not found:", function_name), + type = "error" + ) + } + } + } + ) + + # Observer to handle opening files in RStudio + shiny::observeEvent( + ignoreNULL = TRUE, + ignoreInit = TRUE, + eventExpr = input$openInRStudio, + handlerExpr = { + # Open the file in RStudio + file_location <- input$openInRStudio + file_path <- get_function_path( + file_location = file_location, + project_path = project_path + ) + function_line <- get_function_line( + file_location = file_location + ) + + if (file.exists(file_path)) { + rstudioapi::navigateToFile( + file = file_path, + line = function_line + ) + } else { + shiny::showNotification( + paste("File not found:", file_path), + type = "error" + ) + } + } + ) + + # Observer to handle opening files in a new tab within the shiny app + shiny::observeEvent( + ignoreNULL = TRUE, + ignoreInit = TRUE, + eventExpr = input$openInShiny, + handlerExpr = { + if(input$openInShiny != "") { + file_location <- input$openInShiny + file_location <- get_function_path( + file_location = file_location, + project_path = project_path + ) + tab_name <- basename(file_location) + + # If there's a current tab open, remove it + if (!is.null(currentTabId())) { + shiny::removeTab( + inputId = "fileTabs", + target = currentTabId() + ) + } + + # Set the ID of the new tab to be the current one + currentTabId(tab_name) + + # Check if the file path is valid + if (file.exists(file_location)) { + # Read the file content + file_content <- readLines(file_location) + + # Create the new tab and output its content + output[[tab_name]] <- shiny::renderPrint({ + cat(paste(file_content, collapse = "\n")) + }) + + # Dynamically adjust column widths + shinyjs::runjs( + '$("#mainColumn").removeClass("col-sm-11").addClass("col-sm-6");' + ) + shinyjs::runjs( + '$("#tabColumn").removeClass("col-sm-1").addClass("col-sm-6");' + ) + + # Insert the new tab and set it to the current + shiny::insertTab( + inputId = "fileTabs", + make_closable_tab( + tab_name = tab_name, + content_output_Id = tab_name + ), + select = TRUE + ) + } else { + # Notify the user if the file is not found + shiny::showNotification( + ui = paste("File not found:", file_location), + type = "error" + ) + } + } + } + ) + + # Observer to remove the tab opened in the shiny app + shiny::observeEvent( + ignoreNULL = TRUE, + ignoreInit = TRUE, + eventExpr = input$close_tab, + handlerExpr = { + if (!is.null(currentTabId())) { + shiny::removeTab( + inputId = "fileTabs", + target = currentTabId() + ) + # Reset the current tab ID + currentTabId(NULL) + # Reset columns width + shinyjs::runjs( + '$("#mainColumn").removeClass("col-sm-6").addClass("col-sm-11");' + ) + shinyjs::runjs( + '$("#tabColumn").removeClass("col-sm-6").addClass("col-sm-1");' + ) + } + } + ) + } +} diff --git a/R/app_ui.R b/R/app_ui.R new file mode 100644 index 0000000..5da2e1d --- /dev/null +++ b/R/app_ui.R @@ -0,0 +1,92 @@ +#' Define head tags for the Shiny app +#' +#' @importFrom htmltools htmlDependency +#' @importFrom shiny tags +#' @importFrom shinyjs useShinyjs +#' @importFrom waiter use_waiter +#' @return Head tags for the Shiny app +app_ui_head_tags <- function() { + tags$head( + tags$link( + rel = "stylesheet", + type = "text/css", + href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" + ), + htmlDependency( + name = "custom_css", + version = "0.0.1", + src = system.file("assets", package = "assertHE"), + stylesheet = "custom_css.css" + ), + htmlDependency( + name = "custom_js", + version = "0.0.1", + src = system.file("assets", package = "assertHE"), + script = "custom_js.js" + ), + useShinyjs(), + use_waiter() + ) +} + +#' Create Shiny app UI +#' +#' @param network_title Character string representing the title of the network to be displayed above the network. +#' @importFrom bslib bs_theme +#' @return Shiny app user interface +app_ui <- function(network_title) { + # Use bslib theme with flatly preset + theme <- bs_theme(version = 5, bootswatch = "flatly") + + bslib::page_fluid( + class = "p-3", + title = network_title, + theme = theme, + app_ui_head_tags(), + # Define network plot title/subtitle divs + div( + class = "title-container", + span( + class = "title-text", + network_title + ), + span( + id = "question-icon-click", + class = "help-icon-container", + icon( + class = "help-icon text-muted", + name = "question-circle" + ) + ) + ), + # Define main panel + shiny::fluidRow( + shiny::column( + # Prevent another tabs from covering network tooltip/popup + style = "z-index: 10000;", + width = 11, + visNetwork::visNetworkOutput( + outputId = "networkPlot" + ), + id = "mainColumn" + ), + shiny::column( + width = 1, + # Define a tabsetPanel to contain the dynamic tabs showing user code + shiny::tabsetPanel( + id = "fileTabs" + ), + id = "tabColumn" + ) + ), + shiny::HTML( + paste0( + '' + ) + ) + ) +} diff --git a/R/project_visualiser.R b/R/project_visualiser.R index 8ede364..f489b3d 100644 --- a/R/project_visualiser.R +++ b/R/project_visualiser.R @@ -621,8 +621,6 @@ plotNetwork <- function(df_edges, return(g) } - - #' Process Nodes #' #' Process unique nodes from a dataframe of edges. @@ -652,592 +650,6 @@ processNodes <- function(df_edges, return(df_nodes) } -#' Remove artefacts from file path -#' -#' @param file_location Character scalar specifying the path of a file. -#' @param project_path Character scalar specifying the path of the project. -#' -#' @return A character scalar -#' -#' @importFrom here here -#' -#' @examples -#' \dontrun{ -#' cleaned_file_path <- get_function_path( -#' file_location = "tests/testthat/example_project/R/calculate_QALYs.R#L41" -#' ) -#' cleaned_file_path <- get_function_path( -#' file_location = c( -#' "tests/testthat/example_project/R/calculate_QALYs.R#L41", -#' "tests/testthat/example_project/R/calculate_QALYs.R#L49" -#' ) -#' ) -#' } -get_function_path <- function(file_location, project_path) { - - get_function_path <- gsub("#.*", "", file_location) - - full_file_path <- ifelse( - test = is.na(get_function_path), - yes = "", - no = paste0(project_path, "/", get_function_path) - ) - - return(full_file_path) -} - -#' Extract function line in file path -#' -#' @param file_location Character scalar specifying the path of a file. -#' -#' @return A numeric scalar -#' -#' @examples -#' \dontrun{ -#' cleaned_function_line <- get_function_line( -#' file_location = "tests/testthat/example_project/R/calculate_QALYs.R:L41" -#' ) -#' cleaned_function_line <- get_function_line( -#' file_location = c( -#' "tests/testthat/example_project/R/calculate_QALYs.R#L41", -#' "tests/testthat/example_project/R/calculate_QALYs.R#L49" -#' ) -#' ) -#' } -get_function_line <- function(file_location) { - - function_line <- gsub(".*#L", "", file_location) - - function_line <- ifelse( - test = is.na(function_line), - yes = -1L, - no = as.numeric(function_line) - ) - - return(function_line) -} - - -#' Create closable shiny tab -#' -#' @param tab_name Character scalar representing the name or title of the shiny -#' tab. -#' @param content_output_Id Character scalar representing the id of the shiny -#' tab. -#' @param output_type Character scalar specifying the type of rendered output. -#' Default is `"text"` and can also accept `"HTML"`. -#' -#' @return A tab that can be passed to `shiny::tabsetPanel()` -make_closable_tab <- function( - tab_name, - content_output_Id, - output_type = "text") { - shiny::tabPanel( - title = shiny::div( - style = "display: flex; justify-content: space-between; align-items: center;", - shiny::span( - shiny::HTML( - paste0("", tab_name, "") - ), - style = "flex-grow: 1;" - ), - shiny::actionButton( - inputId = "close", - label = shiny::HTML( - '' - ), - class = "close-tab", - onclick = sprintf( - "Shiny.setInputValue('close_tab', '%s');", - content_output_Id - ) - ) - ), - shiny::div( - class = "custom-tab-content", - if(output_type != "HTML") { - shiny::verbatimTextOutput(outputId = content_output_Id) - } else { - shiny::htmlOutput(outputId = content_output_Id) - } - ), - value = content_output_Id - ) -} - -#' Create Shiny app UI -#' -#' @param network_title Character string representing the title of the network to be displayed above the network. -#' -#' @return Shiny app user interface -define_app_ui <- function(network_title) { - - shiny::fluidPage( - # Initialize shinyjs and waiter - shinyjs::useShinyjs(), - waiter::use_waiter(), - # Define javaScript functions - shiny::tags$head( - shiny::tags$link( - rel = "stylesheet", - type = "text/css", - href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" - ), - shiny::tags$script(" - // Initialize a variable to mirror the values of 'aiAssist', - // 'openInShiny' and 'openInRStudio' - var currentAiAssistValue = null; - var currentOpenInShinyValue = null; - var currentOpenInRstudioValue = null; - - function aiAssist(function_name) { - console.log('Executing JavaScript function aiAssist'); - console.log('Function name: ' + function_name); - - // Check if the current value is the same as 'function_name' - if (currentAiAssistValue === function_name) { - - // Temporarily set 'aiAssist' and 'close_tab' to null - // to ensure the change is detected - Shiny.setInputValue('aiAssist', null, {priority: 'event'}); - Shiny.setInputValue('close_tab', null, {priority: 'event'}); - - // Use setTimeout to ensure the null value is processed before setting - // the new value - setTimeout(function() { - Shiny.setInputValue('aiAssist', function_name, {priority: 'event'}); - }, 10); - - } else { - - // Directly set 'aiAssist' to 'function_name' - Shiny.setInputValue('aiAssist', function_name); - } - - // Update the mirror variable to reflect the new value - currentAiAssistValue = function_name; - } - - function openInShiny(file_location) { - console.log('Executing JavaScript function openInShiny'); - console.log('File location: ' + file_location); - - // Check if the current value is the same as 'file_location' - if (currentOpenInShinyValue === file_location) { - - // Temporarily set 'openInShiny' and 'close_tab' to null - // to ensure the change is detected - Shiny.setInputValue('openInShiny', null, {priority: 'event'}); - Shiny.setInputValue('close_tab', null, {priority: 'event'}); - - // Use setTimeout to ensure the null value is processed before setting - // the new value - setTimeout(function() { - Shiny.setInputValue('openInShiny', file_location, {priority: 'event'}); - }, 10); - } else { - - // Directly set 'openInShiny' to 'file_location' if the values are - // different - Shiny.setInputValue('openInShiny', file_location, {priority: 'event'}); - } - - // Update the mirror variable to reflect the new value - currentOpenInShinyValue = file_location; - } - - function openInRStudio(file_location) { - console.log('Executing JavaScript function openInRStudio'); - console.log('File location: ' + file_location); - - // Check if the current value is the same as 'file_location' - if (currentOpenInRstudioValue === file_location) { - - // Temporarily set 'openInRStudio' to null to ensure the change is - // detected - Shiny.setInputValue('openInRStudio', null, {priority: 'event'}); - - // Use setTimeout to ensure the null value is processed before setting - // the new value - setTimeout(function() { - Shiny.setInputValue('openInRStudio', file_location, {priority: 'event'}); - }, 10); - - } else { - - // Directly set 'openInRStudio' to 'file_location' - Shiny.setInputValue('openInRStudio', file_location); - } - - // Update the mirror variable to reflect the new value - currentOpenInRstudioValue = file_location; - } - - $(document).on('shiny:connected', function(event) { - function adjustTabHeight() { - var windowHeight = $(window).height(); - // Distance from the top of the viewport - var offsetTop = $('#fileTabs').offset().top; - // Subtract any additional margin or padding - var tabHeight = windowHeight - offsetTop - 20; - $('.tab-content').css('height', tabHeight + 'px'); - } - - // Adjust the height on window resize - $(window).resize(adjustTabHeight); - - // Initial adjustment - adjustTabHeight(); - }); - "), - # Define CSS - shiny::tags$style(" - /* Make tab content scrollable */ - .tab-content { - /* Enable vertical scrolling */ - overflow-y: auto; - } - - /* Style for custom tab content background */ - .custom-tab-content { - background-color: #f5f5f5; - padding-bottom: 5px; - border-radius: 4px; - margin-bottom: 20px; - } - - /* Reduce padding around close actionButton */ - .close-tab { - border: none; - padding: 5px; - margin: 5px; - color: red; - } - - /* Waiter */ - .waiter-overlay { - position: fixed; - height: 919px; - width: 1375px; - top: 0px; - left: 0px; - background-color: rgba(51, 62, 72, 0.5) !important; - } - ") - ), - # Define network plot title/subtitle divs - shiny::HTML( - paste0( - '
', - network_title, - '
' - ) - ), - shiny::HTML( - paste0( - '
', - 'Functions without a test are red ', - 'and those with a test are green. ', - 'Hover over nodes for more information. Click on ', - "",' to request an AI generated summary ', - "of the corresponding function, ", - "",' to open the file in RStudio, or on ', - "", - ' to load its contents into a new browser tab.
' - ) - ), - # Define main panel - shiny::fluidRow( - shiny::column( - # Prevent another tabs from covering network tooltip/popup - style = "z-index: 10000;", - width = 11, - visNetwork::visNetworkOutput( - outputId = "networkPlot" - ), - id = "mainColumn" - ), - shiny::column( - width = 1, - # Define a tabsetPanel to contain the dynamic tabs showing user code - shiny::tabsetPanel( - id = "fileTabs" - ), - id = "tabColumn" - ) - ), - shiny::HTML( - paste0( - '' - ) - ) - ) -} - -#' Create Shiny app server logic -#' -#' @inheritParams run_shiny_app -#' @param foo_path path to the function folder -#' -#' @return Shiny app server logic -define_app_server <- function(network_object, project_path, foo_path) { - function(input, output, session) { - - # Create a new environment to avoid sourcing scripts into the namespace - pkg_env <- new.env(parent = baseenv()) - - # Load all functions into this environment - load_functions_into_env(path = foo_path, env = pkg_env) - - - # Keep track of the current tab's output ID - currentTabId <- shiny::reactiveVal(NULL) - - # Keep track of the number of AI-generated summary requests - aiAssit_calls <- shiny::reactiveVal(0) - - # Render the network visual - output$networkPlot <- visNetwork::renderVisNetwork(network_object) - - # Observer to handle AI response in a new tab within the shiny app - shiny::observeEvent( - ignoreNULL = TRUE, - ignoreInit = TRUE, - eventExpr = input$aiAssist, - handlerExpr = { - # If there's a current tab open, remove it - if (!is.null(currentTabId())) { - shiny::removeTab( - inputId = "fileTabs", - target = currentTabId() - ) - } - - if(input$aiAssist != "") { - function_name <- input$aiAssist - tab_name <- paste(function_name) - - # Check if the function name refers to an existing function - if (is.function(get(x = function_name, envir = pkg_env))) { - # Check if the number of calls did not exceed a maximum: - if(aiAssit_calls() < 5) { - # Waiter - waiter <- waiter::Waiter$new( - html = shiny::div( - style = "display: flex; - flex-direction: column; - align-items: center; - justify-content:center; - color: white; - opacity: 1 !important;", - shiny::h4("Please wait ..."), - shiny::h6(paste( - "generating", function_name, "function summary ..." - )), - shiny::br(), - shiny::br(), - waiter::spin_wandering_cubes() - ), - hide_on_render = FALSE - ) - waiter$show() - on.exit(waiter$hide()) - - # Set the ID of the new tab to be the current one - currentTabId(tab_name) - - # Query AI: - ai_response <- summarise_function_with_LLM( - foo_name = function_name, - llm_api_url = Sys.getenv("LLM_API_URL"), - llm_api_key = Sys.getenv("LLM_API_KEY"), - envir = pkg_env - ) - - # Create the new tab and output its content - output[[tab_name]] <- shiny::renderUI({ - shiny::HTML(ai_response) - }) - - # Update the number of calls - aiAssit_calls(aiAssit_calls() + 1) - - # Dynamically adjust column widths - shinyjs::runjs( - '$("#mainColumn").removeClass("col-sm-11").addClass("col-sm-6");' - ) - shinyjs::runjs( - '$("#tabColumn").removeClass("col-sm-1").addClass("col-sm-6");' - ) - - # Insert the new tab and set it to the current - shiny::insertTab( - inputId = "fileTabs", - make_closable_tab( - tab_name = paste("AI summary -", tab_name), - content_output_Id = tab_name, - output_type = "HTML" - ), - select = TRUE - ) - - } else { - # Reset the current tab ID - currentTabId(NULL) - # Reset columns width - shinyjs::runjs( - '$("#mainColumn").removeClass("col-sm-6").addClass("col-sm-11");' - ) - shinyjs::runjs( - '$("#tabColumn").removeClass("col-sm-6").addClass("col-sm-1");' - ) - # Notify the user if the file is not found - shiny::showNotification( - ui = "You have exceeded the allotted AI queries.", - type = "warning" - ) - } - } else { - # Notify the user if the file is not found - shiny::showNotification( - ui = paste("Function not found:", function_name), - type = "error" - ) - } - } - } - ) - - # Observer to handle opening files in RStudio - shiny::observeEvent( - ignoreNULL = TRUE, - ignoreInit = TRUE, - eventExpr = input$openInRStudio, - handlerExpr = { - # Open the file in RStudio - file_location <- input$openInRStudio - file_path <- get_function_path( - file_location = file_location, - project_path = project_path - ) - function_line <- get_function_line( - file_location = file_location - ) - - if (file.exists(file_path)) { - rstudioapi::navigateToFile( - file = file_path, - line = function_line - ) - } else { - shiny::showNotification( - paste("File not found:", file_path), - type = "error" - ) - } - } - ) - - # Observer to handle opening files in a new tab within the shiny app - shiny::observeEvent( - ignoreNULL = TRUE, - ignoreInit = TRUE, - eventExpr = input$openInShiny, - handlerExpr = { - if(input$openInShiny != "") { - file_location <- input$openInShiny - file_location <- get_function_path( - file_location = file_location, - project_path = project_path - ) - tab_name <- basename(file_location) - - # If there's a current tab open, remove it - if (!is.null(currentTabId())) { - shiny::removeTab( - inputId = "fileTabs", - target = currentTabId() - ) - } - - # Set the ID of the new tab to be the current one - currentTabId(tab_name) - - # Check if the file path is valid - if (file.exists(file_location)) { - # Read the file content - file_content <- readLines(file_location) - - # Create the new tab and output its content - output[[tab_name]] <- shiny::renderPrint({ - cat(paste(file_content, collapse = "\n")) - }) - - # Dynamically adjust column widths - shinyjs::runjs( - '$("#mainColumn").removeClass("col-sm-11").addClass("col-sm-6");' - ) - shinyjs::runjs( - '$("#tabColumn").removeClass("col-sm-1").addClass("col-sm-6");' - ) - - # Insert the new tab and set it to the current - shiny::insertTab( - inputId = "fileTabs", - make_closable_tab( - tab_name = tab_name, - content_output_Id = tab_name - ), - select = TRUE - ) - } else { - # Notify the user if the file is not found - shiny::showNotification( - ui = paste("File not found:", file_location), - type = "error" - ) - } - } - } - ) - - # Observer to remove the tab opened in the shiny app - shiny::observeEvent( - ignoreNULL = TRUE, - ignoreInit = TRUE, - eventExpr = input$close_tab, - handlerExpr = { - if (!is.null(currentTabId())) { - shiny::removeTab( - inputId = "fileTabs", - target = currentTabId() - ) - # Reset the current tab ID - currentTabId(NULL) - # Reset columns width - shinyjs::runjs( - '$("#mainColumn").removeClass("col-sm-6").addClass("col-sm-11");' - ) - shinyjs::runjs( - '$("#tabColumn").removeClass("col-sm-6").addClass("col-sm-1");' - ) - } - } - ) - } -} - #' Run a Shiny app to host a network visualization #' #' @param uiFunction Function defining shiny user-interface @@ -1263,8 +675,8 @@ define_app_server <- function(network_object, project_path, foo_path) { #' run_shiny_app(network_object = network_object) #' } run_shiny_app <- function( - uiFunction = define_app_ui, - serverFunction = define_app_server, + uiFunction = app_ui, + serverFunction = app_server, network_object, network_title = "Function Network", project_path, From 4af30e85d9c42e9439f39f4490995a014f6e8f75 Mon Sep 17 00:00:00 2001 From: federiva Date: Fri, 29 Nov 2024 05:58:03 -0300 Subject: [PATCH 3/5] docs: Update docs --- NAMESPACE | 6 ++++++ man/{define_app_server.Rd => app_server.Rd} | 8 ++++---- man/{define_app_ui.Rd => app_ui.Rd} | 8 ++++---- man/app_ui_head_tags.Rd | 14 ++++++++++++++ man/get_function_line.Rd | 2 +- man/get_function_path.Rd | 2 +- man/make_closable_tab.Rd | 2 +- man/run_shiny_app.Rd | 4 ++-- 8 files changed, 33 insertions(+), 13 deletions(-) rename man/{define_app_server.Rd => app_server.Rd} (68%) rename man/{define_app_ui.Rd => app_ui.Rd} (68%) create mode 100644 man/app_ui_head_tags.Rd diff --git a/NAMESPACE b/NAMESPACE index c7908a7..aa6c9bd 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -33,6 +33,7 @@ export(visualise_project) export(wrap_string) import(assertthat) import(dplyr) +importFrom(bslib,bs_theme) importFrom(covr,file_coverage) importFrom(data.table,rbindlist) importFrom(dplyr,group_by) @@ -45,6 +46,7 @@ importFrom(flextable,flextable) importFrom(flextable,width) importFrom(here,here) importFrom(htmltools,a) +importFrom(htmltools,htmlDependency) importFrom(igraph,V) importFrom(igraph,degree) importFrom(igraph,graph_from_data_frame) @@ -54,6 +56,9 @@ importFrom(officer,body_add_par) importFrom(officer,read_docx) importFrom(roxygen2,parse_file) importFrom(rstudioapi,navigateToFile) +importFrom(shiny,tags) +importFrom(shinyWidgets,show_toast) +importFrom(shinyjs,useShinyjs) importFrom(stats,na.omit) importFrom(stringr,str_locate_all) importFrom(stringr,str_replace) @@ -64,3 +69,4 @@ importFrom(utils,getParseData) importFrom(visNetwork,visEdges) importFrom(visNetwork,visNetwork) importFrom(visNetwork,visOptions) +importFrom(waiter,use_waiter) diff --git a/man/define_app_server.Rd b/man/app_server.Rd similarity index 68% rename from man/define_app_server.Rd rename to man/app_server.Rd index 0b231ce..d774c98 100644 --- a/man/define_app_server.Rd +++ b/man/app_server.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/project_visualiser.R -\name{define_app_server} -\alias{define_app_server} +% Please edit documentation in R/app_server.R +\name{app_server} +\alias{app_server} \title{Create Shiny app server logic} \usage{ -define_app_server(network_object, project_path, foo_path) +app_server(network_object, project_path, foo_path) } \arguments{ \item{network_object}{visNetwork object to be displayed in the shiny app} diff --git a/man/define_app_ui.Rd b/man/app_ui.Rd similarity index 68% rename from man/define_app_ui.Rd rename to man/app_ui.Rd index 692a2f1..2e8fb3f 100644 --- a/man/define_app_ui.Rd +++ b/man/app_ui.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/project_visualiser.R -\name{define_app_ui} -\alias{define_app_ui} +% Please edit documentation in R/app_ui.R +\name{app_ui} +\alias{app_ui} \title{Create Shiny app UI} \usage{ -define_app_ui(network_title) +app_ui(network_title) } \arguments{ \item{network_title}{Character string representing the title of the network to be displayed above the network.} diff --git a/man/app_ui_head_tags.Rd b/man/app_ui_head_tags.Rd new file mode 100644 index 0000000..5a11fcd --- /dev/null +++ b/man/app_ui_head_tags.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/app_ui.R +\name{app_ui_head_tags} +\alias{app_ui_head_tags} +\title{Define head tags for the Shiny app} +\usage{ +app_ui_head_tags() +} +\value{ +Head tags for the Shiny app +} +\description{ +Define head tags for the Shiny app +} diff --git a/man/get_function_line.Rd b/man/get_function_line.Rd index df161ad..d2bc266 100644 --- a/man/get_function_line.Rd +++ b/man/get_function_line.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/project_visualiser.R +% Please edit documentation in R/app_server.R \name{get_function_line} \alias{get_function_line} \title{Extract function line in file path} diff --git a/man/get_function_path.Rd b/man/get_function_path.Rd index c507ddd..38e60b2 100644 --- a/man/get_function_path.Rd +++ b/man/get_function_path.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/project_visualiser.R +% Please edit documentation in R/app_server.R \name{get_function_path} \alias{get_function_path} \title{Remove artefacts from file path} diff --git a/man/make_closable_tab.Rd b/man/make_closable_tab.Rd index 518387e..ac01191 100644 --- a/man/make_closable_tab.Rd +++ b/man/make_closable_tab.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/project_visualiser.R +% Please edit documentation in R/app_server.R \name{make_closable_tab} \alias{make_closable_tab} \title{Create closable shiny tab} diff --git a/man/run_shiny_app.Rd b/man/run_shiny_app.Rd index 5c3588c..da9863f 100644 --- a/man/run_shiny_app.Rd +++ b/man/run_shiny_app.Rd @@ -5,8 +5,8 @@ \title{Run a Shiny app to host a network visualization} \usage{ run_shiny_app( - uiFunction = define_app_ui, - serverFunction = define_app_server, + uiFunction = app_ui, + serverFunction = app_server, network_object, network_title = "Function Network", project_path, From 5e97871c02930c5d51d6c693079683dfb3781863 Mon Sep 17 00:00:00 2001 From: federiva Date: Tue, 3 Dec 2024 00:36:42 -0300 Subject: [PATCH 4/5] fix: sweetalert css --- NAMESPACE | 5 +++++ R/app_server.R | 11 +++++++---- inst/assets/custom_css.css | 13 +++++++++++++ man/help_text.Rd | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 man/help_text.Rd diff --git a/NAMESPACE b/NAMESPACE index aa6c9bd..937d793 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -56,8 +56,13 @@ importFrom(officer,body_add_par) importFrom(officer,read_docx) importFrom(roxygen2,parse_file) importFrom(rstudioapi,navigateToFile) +importFrom(shiny,br) +importFrom(shiny,div) +importFrom(shiny,icon) +importFrom(shiny,span) importFrom(shiny,tags) importFrom(shinyWidgets,show_toast) +importFrom(shinyjs,onclick) importFrom(shinyjs,useShinyjs) importFrom(stats,na.omit) importFrom(stringr,str_locate_all) diff --git a/R/app_server.R b/R/app_server.R index 1de6165..818c438 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -1,3 +1,6 @@ +#' Help text +#' @importFrom shiny div icon span br +#' @return A shiny HTML element help_text <- div( class = "subtitle-container", span( @@ -6,17 +9,17 @@ help_text <- div( "Functions without a test are ", span(class="text-danger fw-bolder", "red"), "and those with a test are ", span(class="text-success fw-bolder", "green"), ". ", br(), - "Click on", shiny::icon( + "Click on", icon( name = "robot", style = "color: #337ab7; margin-right: 5px; margin-left: 5px;" ), " to request an AI generated summary of the corresponding function.", br(), - shiny::icon( + icon( name = "up-right-from-square", style = "color: #75AADB; margin-right: 5px; margin-left: 5px;" ), " to open the file in RStudio, or", br(), - shiny::icon( + icon( name = "eye", style = "color: #337ab7; margin-right: 5px; margin-left: 5px;" ), @@ -167,7 +170,7 @@ app_server <- function(network_object, project_path, foo_path) { title = "Help", text = help_text, type = "info", - timer = 30000, + timer = 0, width = "500px" ) }) diff --git a/inst/assets/custom_css.css b/inst/assets/custom_css.css index 1e94dec..730fe8c 100644 --- a/inst/assets/custom_css.css +++ b/inst/assets/custom_css.css @@ -50,3 +50,16 @@ font-size: 15px; } } + +/* sweet alert */ +.swal2-container { + z-index: 10001 !important; +} + +.swal2-title { + text-align: center !important; +} + +.swal2-close { + grid-row: 1 !important; +} diff --git a/man/help_text.Rd b/man/help_text.Rd new file mode 100644 index 0000000..f9f1da9 --- /dev/null +++ b/man/help_text.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/app_server.R +\docType{data} +\name{help_text} +\alias{help_text} +\title{Help text} +\format{ +An object of class \code{shiny.tag} of length 3. +} +\usage{ +help_text +} +\value{ +A shiny HTML element +} +\description{ +Help text +} +\keyword{datasets} From b5e38bdb5941f3536ecbdae11b7cbdffd01ffd73 Mon Sep 17 00:00:00 2001 From: federiva Date: Tue, 3 Dec 2024 00:47:42 -0300 Subject: [PATCH 5/5] feat: Prevent LLM running when no API key is set --- NAMESPACE | 1 + R/app_server.R | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 937d793..bb9f54c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -61,6 +61,7 @@ importFrom(shiny,div) importFrom(shiny,icon) importFrom(shiny,span) importFrom(shiny,tags) +importFrom(shinyWidgets,show_alert) importFrom(shinyWidgets,show_toast) importFrom(shinyjs,onclick) importFrom(shinyjs,useShinyjs) diff --git a/R/app_server.R b/R/app_server.R index 818c438..83cecf8 100644 --- a/R/app_server.R +++ b/R/app_server.R @@ -143,7 +143,7 @@ make_closable_tab <- function( #' #' @inheritParams run_shiny_app #' @param foo_path path to the function folder -#' @importFrom shinyWidgets show_toast +#' @importFrom shinyWidgets show_toast show_alert #' @importFrom shinyjs onclick #' #' @return Shiny app server logic @@ -163,7 +163,9 @@ app_server <- function(network_object, project_path, foo_path) { aiAssit_calls <- shiny::reactiveVal(0) # Render the network visual - output$networkPlot <- visNetwork::renderVisNetwork(network_object) + output$networkPlot <- visNetwork::renderVisNetwork({ + network_object + }) onclick("question-icon-click", { show_toast( @@ -190,6 +192,20 @@ app_server <- function(network_object, project_path, foo_path) { } if(input$aiAssist != "") { + LLM_API_URL <- Sys.getenv("LLM_API_URL") + LLM_API_KEY <- Sys.getenv("LLM_API_KEY") + if (LLM_API_URL == "" || LLM_API_KEY == "") { + show_alert( + title = "LLM API not set", + text = "LLM_API_URL and LLM_API_KEY must be set. See README for more details.", + type = "error", + html = FALSE, + closeOnClickOutside = TRUE, + showCloseButton = FALSE, + session = shiny::getDefaultReactiveDomain() + ) + return() + } function_name <- input$aiAssist tab_name <- paste(function_name)