From 639ec30773f8aa7fdeed44c6dd99e09c3966a349 Mon Sep 17 00:00:00 2001 From: Ivo Kwee Date: Tue, 1 Sep 2026 20:12:07 +0200 Subject: [PATCH 1/6] feat: wire up IDconvert and SmartPrism as dynamically-launched mini apps Home page tiles for IDconvert and SmartPrism previously either did nothing in production (server gated on DEVMODE) or fell back to selecting a pre-rendered hidden nav panel. Both are now launched on demand via launchModule()/nav_insert(), matching the pattern already used for Qsee and Across: - server.R: add launch_idconvert()/launch_prism(), register them in app_launchers - launcher_server.R: launch_idconvert/launch_prism handlers now call into app_launchers instead of bslib::nav_select() on a static panel - ui.R: drop the now-redundant static nav_panel_hidden() blocks for IDconvert and Prism Also, while wiring this up: - prism_ui.R, board_mofa_ui.R: add padding = 0 to their bslib::page_fillable() roots (idconvert_ui.R/qsee_ui.R already did this) so bslib's default 1rem page padding doesn't show as a thick white border - prism_server.R: fix a crash on first launch -- get_plotcode()'s eager observe() ran before the client had sent chartbot_user_input, so isolate(input$chartbot_user_input) == "" compared NULL == "" and threw "argument is of length zero"; this module's server was never actually wired up before now, so the bug was latent --- components/app/R/server.R | 24 ++++++++++++++++----- components/app/R/ui.R | 17 --------------- components/app_launcher/R/launcher_server.R | 12 ++++++++--- components/app_prism/R/prism_server.R | 3 ++- components/app_prism/R/prism_ui.R | 1 + components/board.mofa/R/board_mofa_ui.R | 1 + 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/components/app/R/server.R b/components/app/R/server.R index 5db804c9f..8d6416597 100644 --- a/components/app/R/server.R +++ b/components/app/R/server.R @@ -792,7 +792,7 @@ output$current_user <- shiny::renderText({ raw_dir <- shiny::isolate(raw_dir()) if (!is.null(PGX) && !is.null(shiny::isolate(PGX$name))) { - pgx_name <- PGX$name + pgx_name <- shiny::isolate(PGX$name) } else { pgx_name <- "No PGX loaded when error occurred" } @@ -1080,10 +1080,22 @@ output$current_user <- shiny::renderText({ ) } - ## THESE STILL NEED TO BE WRAPPED in a launchModule() + launch_idconvert <- function() { + launchModule("idconvert", + ui = omicspanel(idconvert_ui("idconvert")), + server = function() idconvert_server("idconvert") + ) + } + + launch_prism <- function() { + launchModule("prism", + ui = omicspanel(prism_ui("prism")), + server = function() prism_server("prism") + ) + } + + ## THIS STILL NEEDS TO BE WRAPPED in a launchModule() ## RunMonitorServer("runmonitor") - ## idconvert_server("idconvert") - ## prism_server("prism") launcher_server( "apps", @@ -1091,7 +1103,9 @@ output$current_user <- shiny::renderText({ load_example = load_example, app_launchers = list( "qsee" = launch_qsee, - "across" = launch_across + "across" = launch_across, + "idconvert" = launch_idconvert, + "prism" = launch_prism ), pgx = PGX ) diff --git a/components/app/R/ui.R b/components/app/R/ui.R index 795f62d8b..36aba9b96 100644 --- a/components/app/R/ui.R +++ b/components/app/R/ui.R @@ -182,23 +182,6 @@ app_ui <- function(x) { omicspanel(AdminPanelUI("admin_panel")) ) }, - ## Tools - if(isTRUE(opt$DEVMODE)) { - bslib::nav_panel_hidden("Prism", - omicspanel(prism_ui("prism")) - ) - }, - if(isTRUE(opt$DEVMODE)) { - bslib::nav_panel_hidden("IDconvert", - omicspanel(idconvert_ui("idconvert")) - ) - }, - ## if(isTRUE(opt$DEVMODE)) { - ## bslib::nav_panel_hidden( - ## value = "AcrossDatasets", - ## omicspanel(AcrossUI("across")) - ## ) - ## }, ## lower settings buttons bslib::nav_spacer(), bslib::nav_panel("Settings", icon=icon("cog"), diff --git a/components/app_launcher/R/launcher_server.R b/components/app_launcher/R/launcher_server.R index 611c72b64..d266aa30d 100644 --- a/components/app_launcher/R/launcher_server.R +++ b/components/app_launcher/R/launcher_server.R @@ -100,15 +100,21 @@ launcher_server <- function(id, parent, load_example = NULL, if(!isTRUE(opt$DEVMODE)) { return(NULL) } - bslib::nav_select("app-sidebar", "Prism", session=parent) + run_app <- app_launchers[["prism"]] + if (!is.null(run_app) && is.function(run_app)) { + run_app() + } }) observeEvent(input$launch_idconvert, { - dev_alert() + dev_alert() if(!isTRUE(opt$DEVMODE)) { return(NULL) } - bslib::nav_select("app-sidebar", "IDconvert", session=parent) + run_app <- app_launchers[["idconvert"]] + if (!is.null(run_app) && is.function(run_app)) { + run_app() + } }) ## Quick action: load the example dataset and jump to the Dashboard diff --git a/components/app_prism/R/prism_server.R b/components/app_prism/R/prism_server.R index d282b2c7d..06ced38cd 100644 --- a/components/app_prism/R/prism_server.R +++ b/components/app_prism/R/prism_server.R @@ -171,7 +171,8 @@ prism_server <- function(id) { dbg("input$chartbot_send = ", input$chartbot_send) dbg("input$chartbot_user_input = ", isolate(input$chartbot_user_input)) - if(isolate(input$chartbot_user_input) == "") { + user_input <- isolate(input$chartbot_user_input) + if(is.null(user_input) || user_input == "") { dbg("updating plotcode") code <- update_plotcode(last_plotcode, pointsize, fontsize, theme, dataset) } else { diff --git a/components/app_prism/R/prism_ui.R b/components/app_prism/R/prism_ui.R index 5588ef58a..b4ea74007 100644 --- a/components/app_prism/R/prism_ui.R +++ b/components/app_prism/R/prism_ui.R @@ -21,6 +21,7 @@ prism_ui <- function(id) { title <- div("SmartPrism", style="font-size: 18px;") ui <- bslib::page_fillable( + padding = 0, tags$head( tags$script(type = "module", src = "static/prism-webr.js"), tags$style(HTML(" diff --git a/components/board.mofa/R/board_mofa_ui.R b/components/board.mofa/R/board_mofa_ui.R index 39bdc3f79..279f9a79f 100644 --- a/components/board.mofa/R/board_mofa_ui.R +++ b/components/board.mofa/R/board_mofa_ui.R @@ -275,6 +275,7 @@ MofaUI <- function(id) { # shiny::div( bslib::page_fillable( + padding = 0, fillable_mobile = FALSE, # not working here... OmicsBoardUI( ns = ns, From 07deb85b18217a1226c55f46bff5ab2aed356fa1 Mon Sep 17 00:00:00 2001 From: Ivo Kwee Date: Tue, 1 Sep 2026 23:17:21 +0200 Subject: [PATCH 2/6] wip --- components/app_idconvert/R/idconvert_server.R | 29 +++- components/app_idconvert/R/idconvert_ui.R | 124 ++++++++++-------- components/app_launcher/R/launcher_ui.R | 2 +- components/app_prism/R/prism_ui.R | 23 +++- 4 files changed, 115 insertions(+), 63 deletions(-) diff --git a/components/app_idconvert/R/idconvert_server.R b/components/app_idconvert/R/idconvert_server.R index a45aec81c..8230c2f96 100644 --- a/components/app_idconvert/R/idconvert_server.R +++ b/components/app_idconvert/R/idconvert_server.R @@ -47,17 +47,30 @@ convert_datatype_choices <- function() { idconvert_server <- function(id) { shiny::moduleServer(id, function(input, output, session) { + + known.organisms <- convert_organism_choices() + BRIDGE_SPECIES = c("dmelanogaster","drerio","celegans","scerevisiae","athaliana") + shiny::updateSelectizeInput(session, "organism", - choices = convert_organism_choices(), selected = "Human", server = TRUE + choices = known.organisms, + selected = "Human", + server = TRUE ) + shiny::updateSelectizeInput(session, "bridge_organism", + choices = setdiff(known.organisms, "Human"), + selected = 1, + server = TRUE + ) + # Holds the current annotation result (NULL when cleared or not yet converted) result <- shiny::reactiveVal(NULL) shiny::observeEvent(input$example, { + n <- 100 features <- shiny::withProgress( message = "Loading example features...", - playbase::getExampleFeatures( organism=input$organism, n=25) + playbase::getExampleFeatures( organism=input$organism, n=n) ) if (length(features) == 0) { shiny::showNotification( @@ -90,11 +103,20 @@ idconvert_server <- function(id) { "Please paste at least one gene/feature ID." )) + bridge <- FALSE + bridge_species <- NULL + if( input$use_bridge ) { + bridge <- TRUE + bridge_species <- input$bridge_species + } + annot <- shiny::withProgress( message = "Converting gene/feature IDs...", playbase::getProbeAnnotation( organism = input$organism, probes = probes, + bridge = bridge, + bridge_species = bridge_species, datatype = input$datatype ) ) @@ -104,6 +126,9 @@ idconvert_server <- function(id) { "Could not annotate these IDs for the selected organism." )) + + + result(annot) # store successful result }) diff --git a/components/app_idconvert/R/idconvert_ui.R b/components/app_idconvert/R/idconvert_ui.R index 494ecd78d..91bd094be 100644 --- a/components/app_idconvert/R/idconvert_ui.R +++ b/components/app_idconvert/R/idconvert_ui.R @@ -6,72 +6,90 @@ idconvert_ui <- function(id) { ns <- shiny::NS(id) ## namespace - title <- div("ID Converter", style = "font-size: 18px;") - + organism_choices <- convert_organism_choices() - ui <- bslib::page_fillable( - padding = 0, - div(class = "navbar navbar-static-top", div(title, class = "container-fluid"), - style = "margin-top: 24px;"), + ui <- bslib::layout_columns( + col_widths = c(2, 10), + class = "p-3", + gap = "2rem", + height = "calc(100vh - 72px)", bslib::layout_columns( - col_widths = c(2, 10), - class = "p-3", - gap = "2rem", - height = "calc(100vh - 72px)", - bslib::layout_columns( - col_widths = 12, - fill = FALSE, - shiny::selectizeInput(ns("organism"), "Organism:", - choices = c("Human" = "Human"), - selected = "Human" - ), - shiny::div( - paste(length(organism_choices), "species available"), - style = "font-size: 11px; color: #888; margin-top: -12px;" - ), - shiny::selectInput(ns("datatype"), "Datatype:", - choices = convert_datatype_choices(), - selected = "proteomics" - ), + col_widths = 12, + gap = "14px", + fill = FALSE, + shiny::selectizeInput(ns("organism"), "Organism:", + choices = c("Human" = "Human"), + selected = "Human" + ), + shiny::div( + paste(length(organism_choices), "species available"), + style = "font-size: 12px; color: #888; margin: -12px 0 0 0;" + ), + shiny::selectInput(ns("datatype"), "Datatype:", + choices = convert_datatype_choices(), + selected = "proteomics" + ), + div( + style = "margin: -15px 0 0px 0;", shiny::textAreaInput(ns("features"), "Gene/feature IDs (one per line):", - rows = 20, + rows = 16, placeholder = "e.g.\nTrp53\nENSMUSG00000059552\n..." ), - div( - shiny::actionLink(ns("example"), "Load example features", - style = paste( - "font-size: 12px; color: #888; margin-top: -12px;", - "margin-bottom: 12px; display: inline-block;", - "text-decoration: underline; padding-right: 10px;" - ) - ), - shiny::actionLink(ns("clear"), "Clear", - style = paste( - "font-size: 12px; color: #888; margin-top: -12px;", - "margin-bottom: 12px; display: inline-block;", - "text-decoration: underline;" - ) + shiny::actionLink(ns("example"), "Load example features", + style = paste( + "font-size: 12px; color: #888; margin-top: -15px;", + "margin-bottom: 8px; display: inline-block;", + "text-decoration: underline; padding-right: 10px;" ) ), - div( - style = "display: flex; flex-direction: column; gap: 0;", - shiny::actionButton(ns("convert"), "Convert", - icon = icon("arrows-rotate"), class = "btn-primary mb-2", - width = "100%" - ), - shiny::uiOutput(ns("download_ui")) + shiny::actionLink(ns("clear"), "Clear", + style = paste( + "font-size: 12px; color: #888; margin-top: -12px;", + "margin-bottom: 8px; display: inline-block;", + "text-decoration: underline;" + ) ) ), - bslib::layout_columns( - col_widths = 12, - div( - style = "padding-left: 30px; height: 100%;", - shiny::uiOutput(ns("table_area"), style = "height: 100%;") + div( + style = "margin: -5px 0 10px 0;", + shiny::checkboxInput( ns("use_bridge"), "Use bridge organism", FALSE), + shiny::conditionalPanel( + condition = "input.use_bridge", + ns = ns, + shiny::selectizeInput(ns("bridge_organism(s)"), "Bridge organism:", + choices = NULL, multiple = TRUE + ) ) + ), + div( + style = "display: flex; flex-direction: column; gap: 0;", + shiny::actionButton(ns("convert"), "Convert", + icon = icon("arrows-rotate"), class = "btn-primary mb-2", + width = "100%" + ), + shiny::uiOutput(ns("download_ui")) + ) + ), + bslib::layout_columns( + col_widths = 12, + div( + style = "padding-left: 30px; height: 100%;", + shiny::uiOutput(ns("table_area"), style = "height: 100%;") ) ) ) - return(ui) + title <- HTML("ID Converter — convert and annotate your features") + + board <- OmicsBoardUI( + ns = ns, + #title = "AI Copilot", + title = div(title, style="margin-left: 14px;"), + info = FALSE, + header_margin = "0px", + ui + ) + + return(board) } diff --git a/components/app_launcher/R/launcher_ui.R b/components/app_launcher/R/launcher_ui.R index cf79f9ce4..6d8e63fd7 100644 --- a/components/app_launcher/R/launcher_ui.R +++ b/components/app_launcher/R/launcher_ui.R @@ -63,7 +63,7 @@ launcher_ui <- function(id) { input = "launch_prism", icon = "wand-magic-sparkles", label = "SmartPrism", - description = "AI-generated figures", + description = "AI-assisted plotting", rgb = c(190,120,50), group = "Apps", badge = "dev" diff --git a/components/app_prism/R/prism_ui.R b/components/app_prism/R/prism_ui.R index b4ea74007..d4bee31e8 100644 --- a/components/app_prism/R/prism_ui.R +++ b/components/app_prism/R/prism_ui.R @@ -17,8 +17,6 @@ prism_ui <- function(id) { actionButton(ns("button4"), "button4", icon=icon("eye-dropper")) ) ) - - title <- div("SmartPrism", style="font-size: 18px;") ui <- bslib::page_fillable( padding = 0, @@ -48,18 +46,18 @@ prism_ui <- function(id) { } ")) ), - div(class = "navbar navbar-static-top", div(title, class = "container-fluid"), - style="margin-top: 24px;"), +# div(class = "navbar navbar-static-top", div(title, class = "container-fluid"), +# style="margin-top: 24px;"), bslib::layout_columns( col_widths = c(3,6,2), class = "p-3", - + gap = "2rem", bslib::layout_columns( col_widths = 12, fill = TRUE, row_heights = list("auto","auto",1), shiny::selectInput(ns("dataset"), "Dataset:", c("mtcars","iris","geiger"), - selected="mtcars"), + selected="geiger"), buttons, div( style = "height: 100%;", @@ -128,5 +126,16 @@ prism_ui <- function(id) { ) ) - return(ui) + title = HTML("SmartPrism — AI-assisted plotting") + + board <- OmicsBoardUI( + ns = ns, + #title = "AI Copilot", + title = div(title, style="margin-left: 14px;"), + info = FALSE, + header_margin = "0px", + ui + ) + + return(board) } From 478411d4d5f9c4c409735ff40de439a409ed69b1 Mon Sep 17 00:00:00 2001 From: Ivo Kwee Date: Wed, 2 Sep 2026 16:36:37 +0200 Subject: [PATCH 3/6] fix input name --- components/app_idconvert/R/idconvert_ui.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/app_idconvert/R/idconvert_ui.R b/components/app_idconvert/R/idconvert_ui.R index 91bd094be..b7c687efc 100644 --- a/components/app_idconvert/R/idconvert_ui.R +++ b/components/app_idconvert/R/idconvert_ui.R @@ -57,7 +57,7 @@ idconvert_ui <- function(id) { shiny::conditionalPanel( condition = "input.use_bridge", ns = ns, - shiny::selectizeInput(ns("bridge_organism(s)"), "Bridge organism:", + shiny::selectizeInput(ns("bridge_organism"), "Bridge organism(s):", choices = NULL, multiple = TRUE ) ) From 26b80d59728fedc77eb952d6c7247b9608d39271 Mon Sep 17 00:00:00 2001 From: Ivo Kwee Date: Wed, 2 Sep 2026 17:37:03 +0200 Subject: [PATCH 4/6] remove empty probes --- components/app_idconvert/R/idconvert_server.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/app_idconvert/R/idconvert_server.R b/components/app_idconvert/R/idconvert_server.R index 8230c2f96..39a660266 100644 --- a/components/app_idconvert/R/idconvert_server.R +++ b/components/app_idconvert/R/idconvert_server.R @@ -109,6 +109,9 @@ idconvert_server <- function(id) { bridge <- TRUE bridge_species <- input$bridge_species } + + probes <- probes[!probes %in% c("",NA)] + probes <- unique(probes) annot <- shiny::withProgress( message = "Converting gene/feature IDs...", @@ -125,9 +128,6 @@ idconvert_server <- function(id) { !is.null(annot), "Could not annotate these IDs for the selected organism." )) - - - result(annot) # store successful result }) From dd511962a159b48382fe8f9bb6b917e8121cd53c Mon Sep 17 00:00:00 2001 From: Ivo Kwee Date: Wed, 16 Sep 2026 18:55:34 +0200 Subject: [PATCH 5/6] merge devel --- components/app_idconvert/R/idconvert_ui.R | 12 ++++++------ components/app_launcher/R/launcher_ui.R | 2 +- components/app_prism/R/prism_ui.R | 7 +++++-- scss/components/_all.scss | 3 ++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/components/app_idconvert/R/idconvert_ui.R b/components/app_idconvert/R/idconvert_ui.R index 89cc14c8e..c2fd5715d 100644 --- a/components/app_idconvert/R/idconvert_ui.R +++ b/components/app_idconvert/R/idconvert_ui.R @@ -11,8 +11,9 @@ idconvert_ui <- function(id) { ui <- bslib::layout_columns( col_widths = c(2, 10), class = "p-3", - gap = "2rem", - height = "calc(100vh - 72px)", + gap = "0.7rem", + height = "100%", + row_heights = "auto", bslib::layout_columns( col_widths = 12, gap = "14px", @@ -71,16 +72,15 @@ idconvert_ui <- function(id) { ) ) - title <- HTML("ID Converter — convert and annotate your features") + title <- HTML("ID Annotator — annotate your features") board <- OmicsBoardUI( ns = ns, - #title = "AI Copilot", - title = div(title, style="margin-left: 14px;"), + title = title, info = FALSE, header_margin = "0px", ui ) return(board) -} \ No newline at end of file +} diff --git a/components/app_launcher/R/launcher_ui.R b/components/app_launcher/R/launcher_ui.R index 6fafeb1e5..c0e441046 100644 --- a/components/app_launcher/R/launcher_ui.R +++ b/components/app_launcher/R/launcher_ui.R @@ -62,7 +62,7 @@ launcher_ui <- function(id) { list( input = "launch_idconvert", icon = "shuffle", - label = "ID Converter", + label = "ID Annotator", description = "Annotate features", rgb = c(140,35,175), group = "Apps", diff --git a/components/app_prism/R/prism_ui.R b/components/app_prism/R/prism_ui.R index adbca8b46..19bc8e4dc 100644 --- a/components/app_prism/R/prism_ui.R +++ b/components/app_prism/R/prism_ui.R @@ -121,10 +121,13 @@ prism_ui <- function(id) { ) ) + title <- HTML("SmartPrism — chat and plot") + ui <- OmicsBoardUI( ns = ns, - title = "SmartPrism", - header_margin="0px", + title = title, + header_margin = "0px", + info = FALSE, tags.head, board ) diff --git a/scss/components/_all.scss b/scss/components/_all.scss index c5a86e3be..a81592494 100644 --- a/scss/components/_all.scss +++ b/scss/components/_all.scss @@ -353,12 +353,13 @@ html { } div#navheader-current-section { + height: 33px; font-size: 28px; font-weight: 600; // 17px tighter than it used to be (was margin-top 15 + padding-top 10): // this puts the title's baseline on the same line as the sidebar's "Menu" // heading instead of floating well below it. - margin: 8px 0 5px 0px; + margin: 8px 0 5px 15px; } div#navheader-dataset-stats { From 85c200d4753bbb5a2cb3520b8aaf6e887b3459b2 Mon Sep 17 00:00:00 2001 From: Ivo Kwee Date: Wed, 16 Sep 2026 19:07:42 +0200 Subject: [PATCH 6/6] fix: restore maxOptions on idconvert organism selectizeInput Server-side selectize defaults to maxOptions=1000, silently truncating the ~1581 species list without this option. Co-Authored-By: Claude Sonnet 5 --- components/app_idconvert/R/idconvert_ui.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/app_idconvert/R/idconvert_ui.R b/components/app_idconvert/R/idconvert_ui.R index c2fd5715d..4af475406 100644 --- a/components/app_idconvert/R/idconvert_ui.R +++ b/components/app_idconvert/R/idconvert_ui.R @@ -20,7 +20,8 @@ idconvert_ui <- function(id) { fill = FALSE, shiny::selectizeInput(ns("organism"), "Organism:", choices = c("Human" = "Human"), - selected = "Human" + selected = "Human", + options = list(maxOptions = length(organism_choices)) ), shiny::div( paste(length(organism_choices), "species available"),