## ========== coxp_ui.R ==========

## 1. 常量 -----------------------------------------------------------------
coxp_show_interactions <- setNames(c("", 2, 3),
                                   c(i18n$t("None"), i18n$t("2-way"), i18n$t("3-way")))

coxp_predict <- setNames(c("none", "data", "cmd", "datacmd"),
                         c(i18n$t("None"), i18n$t("Data"), i18n$t("Command"), i18n$t("Data & Command")))

coxp_check <- setNames(c("robust"), c(i18n$t("Robust")))

coxp_sum_check <- setNames(c("rmse", "confint"),
                           c(i18n$t("RMSE"), i18n$t("Confidence intervals")))

coxp_lines <- setNames(c("line", "loess", "jitter"),
                       c(i18n$t("Line"), i18n$t("Loess"), i18n$t("Jitter")))

coxp_plots <- setNames(
  c("none", "dist", "correlations", "scatter", "vip", "pred_plot", "pdp", "dashboard", "resid_pred", "coef", "influence"),
  c(i18n$t("None"), i18n$t("Distribution"), i18n$t("Correlations"),
    i18n$t("Scatter"), i18n$t("Permutation Importance"), i18n$t("Prediction plots"),
    i18n$t("Partial Dependence"), i18n$t("Dashboard"), i18n$t("Residual vs explanatory"),
    i18n$t("Coefficient plot"), i18n$t("Influential observations"))
)

## 2. 参数收集 -------------------------------------------------------------
coxp_args       <- list()
coxp_sum_args   <- list()
coxp_plot_args  <- list()
coxp_pred_args  <- list()
coxp_pred_plot_args <- list()

##  输入收集 reactive
coxp_inputs <- reactive({
  args <- list()
  args$data_filter <- if (input$show_filter) input$data_filter else ""
  args$arr         <- if (input$show_filter) input$data_arrange else ""
  args$rows        <- if (input$show_filter) input$data_rows   else ""
  args$dataset     <- input$dataset
  ## 其余参数手动映射
  for (i in r_drop(names(args))) {
    args[[i]] <- input[[paste0("coxp_", i)]]
  }
  args
})

coxp_sum_inputs <- reactive({
  args <- coxp_sum_args
  for (i in names(args)) args[[i]] <- input[[paste0("coxp_", i)]]
  args
})

coxp_plot_inputs <- reactive({ list() })

coxp_pred_inputs <- reactive({
  args <- coxp_pred_args
  for (i in names(args)) args[[i]] <- input[[paste0("coxp_", i)]]
  args$pred_cmd  <- ""
  args$pred_data <- ""
  if (input$coxp_predict == "cmd") {
    args$pred_cmd <- gsub("\\s{2,}", " ", input$coxp_pred_cmd) %>%
      gsub(";\\s+", ";", .) %>% gsub("\"", "\'", .)
  } else if (input$coxp_predict == "data") {
    args$pred_data <- input$coxp_pred_data
  } else if (input$coxp_predict == "datacmd") {
    args$pred_cmd  <- gsub("\\s{2,}", " ", input$coxp_pred_cmd) %>%
      gsub(";\\s+", ";", .) %>% gsub("\"", "\'", .)
    args$pred_data <- input$coxp_pred_data
  }
  args
})

coxp_pred_plot_inputs <- reactive({ list() })

## 3. 变量选择 UI ----------------------------------------------------------
output$ui_coxp_time <- renderUI({
  isNum <- .get_class() %in% c("integer", "numeric", "ts")
  vars <- varnames()[isNum]
  selectInput("coxp_time", i18n$t("Time variable:"), vars,
              selected = state_single("coxp_time", vars))
})

output$ui_coxp_status <- renderUI({
  vars <- varnames()
  selectInput("coxp_status", i18n$t("Status variable:"), vars,
              selected = state_single("coxp_status", vars))
})

output$ui_coxp_evar <- renderUI({
  req(available(input$coxp_time), available(input$coxp_status))
  vars <- setdiff(varnames(), c(input$coxp_time, input$coxp_status))
  selectInput("coxp_evar", i18n$t("Explanatory variables:"), vars,
              selected = state_multiple("coxp_evar", vars),
              multiple = TRUE, size = min(10, length(vars)), selectize = FALSE)
})

## 4. 交互 / 包含 / 测试变量 ----------------------------------------------
output$ui_coxp_incl <- renderUI({
  req(available(input$coxp_evar))
  vars <- input$coxp_evar
  if (input[["coxp_plots"]] == "coef") {
    vars_init <- vars
  } else {
    vars_init <- c()
  }
  selectInput(
    "coxp_incl", i18n$t("Explanatory variables to include:"), choices = vars,
    selected = state_multiple("coxp_incl", vars, vars_init),
    multiple = TRUE, size = min(10, length(vars)), selectize = FALSE
  )
})

output$ui_coxp_incl_int <- renderUI({
  req(available(input$coxp_evar))
  choices <- character(0)
  vars <- input$coxp_evar
  if (length(vars) > 1) {
    choices <- iterms(vars, 2)
  } else {
    updateSelectInput(session, "coxp_incl_int", choices = choices, selected = choices)
    return()
  }
  selectInput(
    "coxp_incl_int",
    label = i18n$t("2-way interactions to explore:"),
    choices = choices,
    selected = state_multiple("coxp_incl_int", choices),
    multiple = TRUE,
    size = min(8, length(choices)),
    selectize = FALSE
  )
})

output$ui_coxp_test_var <- renderUI({
  req(available(input$coxp_evar))
  vars <- input$coxp_evar
  if (!is.null(input$coxp_int)) vars <- c(vars, input$coxp_int)
  selectizeInput("coxp_test_var", i18n$t("Variables to test:"),
                 choices = vars,
                 selected = state_multiple("coxp_test_var", vars),
                 multiple = TRUE,
                 options = list(placeholder = i18n$t("None"), plugins = list("remove_button")))
})

## 5. 交互选择 ------------------------------------------------------------
output$ui_coxp_show_interactions <- renderUI({
  vars <- input$coxp_evar
  isNum  <- .get_class() %in% c("integer", "numeric", "ts")
  if (any(vars %in% varnames()[isNum])) {
    choices <- coxp_show_interactions[1:3]
  } else {
    choices <- coxp_show_interactions[1:max(min(3, length(input$coxp_evar)), 1)]
  }
  radioButtons("coxp_show_interactions", i18n$t("Interactions:"),
               choices = choices, selected = state_init("coxp_show_interactions"),
               inline = TRUE)
})

output$ui_coxp_int <- renderUI({
  choices <- character(0)
  if (is.empty(input$coxp_show_interactions)) return()
  vars <- input$coxp_evar
  if (not_available(vars)) return()
  isNum <- intersect(vars, varnames()[.get_class() %in% c("integer", "numeric", "ts")])
  if (length(isNum) > 0) choices <- qterms(isNum, input$coxp_show_interactions)
  if (length(vars) > 1) choices <- c(choices, iterms(vars, input$coxp_show_interactions))
  if (length(choices) == 0) return()
  selectInput("coxp_int", label = NULL, choices = choices,
              selected = state_init("coxp_int"),
              multiple = TRUE, size = min(8, length(choices)), selectize = FALSE)
})

## 6. 预测 / 绘图 / 刷新按钮 ----------------------------------------------
observeEvent(input$dataset, {
  updateSelectInput(session, "coxp_predict", selected = "none")
  updateSelectInput(session, "coxp_plots",   selected = "none")
})

output$ui_coxp_predict_plot <- renderUI({ predict_plot_controls("coxp") })
output$ui_coxp_nrobs <- renderUI({
  nrobs <- nrow(.get_data())
  choices <- c("1,000" = 1000, "5,000" = 5000, "10,000" = 10000, "All" = -1) %>% .[. < nrobs]
  selectInput("coxp_nrobs", i18n$t("Number of data points plotted:"),
              choices = choices, selected = state_single("coxp_nrobs", choices, 1000))
})

output$ui_coxp_store_res_name <- renderUI({
  req(input$dataset)
  textInput("coxp_store_res_name", i18n$t("Store residuals:"), "", placeholder = i18n$t("Provide variable name"))
})

run_refresh(coxp_args, "coxp", tabs = "tabs_coxp", label = i18n$t("Estimate model"), relabel = i18n$t("Re-estimate model"))

## 7. 主 UI ----------------------------------------------------------------
output$ui_coxp <- renderUI({
  req(input$dataset)
  tagList(
    conditionalPanel(
      condition = "input.tabs_coxp == 'Summary'",
      wellPanel(
        actionButton("coxp_run", i18n$t("Estimate model"), width = "100%", icon = icon("play", verify_fa = FALSE), class = "btn-success")
      )
    ),
    wellPanel(
      conditionalPanel(
        condition = "input.tabs_coxp == 'Summary'",
        uiOutput("ui_coxp_time"),
        uiOutput("ui_coxp_status"),
        uiOutput("ui_coxp_evar"),
        conditionalPanel(
          condition = "input.coxp_evar != null",
          uiOutput("ui_coxp_show_interactions"),
          conditionalPanel(
            condition = "input.coxp_show_interactions != ''",
            uiOutput("ui_coxp_int")
          ),
          uiOutput("ui_coxp_test_var"),
          checkboxGroupInput("coxp_check", NULL, coxp_check,
                             selected = state_group("coxp_check"), inline = TRUE),
          checkboxGroupInput("coxp_sum_check", NULL, coxp_sum_check,
                             selected = state_group("coxp_sum_check"), inline = TRUE)
        )
      ),
      conditionalPanel(
        condition = "input.tabs_coxp == 'Predict'",
        selectInput("coxp_predict", i18n$t("Prediction input type:"), coxp_predict,
                    selected = state_single("coxp_predict", coxp_predict, "none")),
        conditionalPanel(
          "input.coxp_predict == 'data' | input.coxp_predict == 'datacmd'",
          selectizeInput("coxp_pred_data", i18n$t("Prediction data:"),
                         choices = c("None" = "", r_info[["datasetlist"]]),
                         selected = state_single("coxp_pred_data", c("None" = "", r_info[["datasetlist"]])))
        ),
        conditionalPanel(
          "input.coxp_predict == 'cmd' | input.coxp_predict == 'datacmd'",
          returnTextAreaInput("coxp_pred_cmd", i18n$t("Prediction command:"),
                              value = state_init("coxp_pred_cmd", ""), rows = 3,
                              placeholder = i18n$t("Type a formula to set values for model variables (e.g., age = 60; sex = 'Male') and press return"))
        ),
        conditionalPanel(
          condition = "input.coxp_predict != 'none'",
          checkboxInput("coxp_pred_plot", i18n$t("Plot predictions"), state_init("coxp_pred_plot", FALSE)),
          conditionalPanel(
            "input.coxp_pred_plot == true",
            uiOutput("ui_coxp_predict_plot")
          )
        ),
        conditionalPanel(
          "input.coxp_predict == 'data' | input.coxp_predict == 'datacmd'",
          tags$table(
            tags$td(textInput("coxp_store_pred_name", i18n$t("Store predictions:"), state_init("coxp_store_pred_name", "pred_coxp"))),
            tags$td(actionButton("coxp_store_pred", i18n$t("Store"), icon = icon("plus", verify_fa = FALSE)), class = "top")
          )
        )
      ),
      conditionalPanel(
        condition = "input.tabs_coxp == 'Plot'",
        selectInput("coxp_plots", i18n$t("Plots:"), choices = coxp_plots,
                    selected = state_single("coxp_plots", coxp_plots)),
        conditionalPanel(
          condition = "input.coxp_plots == 'coef' || input.coxp_plots == 'pdp' || input.coxp_plots == 'pred_plot'",
          uiOutput("ui_coxp_incl"),
          conditionalPanel(
            condition = "input.coxp_plots == 'coef'",
            checkboxInput("coxp_intercept", i18n$t("Include intercept"), state_init("coxp_intercept", FALSE))
          ),
          conditionalPanel(
            condition = "input.coxp_plots == 'pdp' | input.coxp_plots == 'pred_plot'",
            uiOutput("ui_coxp_incl_int")
          )
        ),
        conditionalPanel(
          condition = "['correlations', 'scatter', 'dashboard', 'resid_pred'].indexOf(input.coxp_plots) !== -1",
          uiOutput("ui_coxp_nrobs"),
          conditionalPanel(
            condition = "input.coxp_plots != 'correlations'",
            checkboxGroupInput("coxp_lines", NULL, coxp_lines,
                               selected = state_group("coxp_lines"), inline = TRUE)
          )
        )
      ),
      conditionalPanel(
        condition = "(input.tabs_coxp == 'Summary' && input.coxp_sum_check != undefined && input.coxp_sum_check.indexOf('confint') >= 0) ||
               (input.tabs_coxp == 'Predict' && input.coxp_predict != 'none') ||
               (input.tabs_coxp == 'Plot' && input.coxp_plots == 'coef')",
        sliderInput("coxp_conf_lev", i18n$t("Confidence level:"),
                    min = 0.80, max = 0.99, value = state_init("coxp_conf_lev", .95), step = 0.01)
      ),
      conditionalPanel(
        condition = "input.tabs_coxp == 'Summary'",
        tags$table(
          tags$td(uiOutput("ui_coxp_store_res_name")),
          tags$td(actionButton("coxp_store_res", i18n$t("Store"), icon = icon("plus", verify_fa = FALSE)), class = "top")
        )
      )
    ),
    help_and_report(
      modal_title = i18n$t("Cox Proportional Hazards Regression"),
      fun_name = "coxp",
      help_file = inclMD(file.path(getOption("radiant.path.model"), "app/tools/help/cox.md"))
    )
  )
})

## 8. 绘图尺寸 ------------------------------------------------------------
coxp_plot <- reactive({
  if (coxp_available() != "available") return()
  if (is.empty(input$coxp_plots, "none")) return()
  plot_width <- 650
  plot_height <- 500
  nr_vars <- length(input$coxp_evar) + 1
  
  if (input$coxp_plots == "dist") {
    plot_height <- (plot_height / 2) * ceiling(nr_vars / 2)
  } else if (input$coxp_plots == "dashboard") {
    plot_height <- 1.5 * plot_height
  } else if (input$coxp_plots == "correlations") {
    plot_height <- 150 * nr_vars
    plot_width <- 150 * nr_vars
  } else if (input$coxp_plots == "coef") {
    incl <- paste0("^(", paste0(input$coxp_incl, "[|]*", collapse = "|"), ")")
    nr_coeff <- sum(grepl(incl, .coxp()$coeff$label))
    plot_height <- 300 + 20 * nr_coeff
  } else if (input$coxp_plots %in% c("scatter", "resid_pred")) {
    plot_height <- (plot_height / 2) * ceiling((nr_vars - 1) / 2)
  } else if (input$coxp_plots == "vip") {
    plot_height <- max(500, 30 * nr_vars)
  } else if (input$coxp_plots %in% c("pdp", "pred_plot")) {
    nr_vars <- length(input$coxp_incl) + length(input$coxp_incl_int)
    plot_height <- max(250, ceiling(nr_vars / 2) * 250)
    if (length(input$coxp_incl_int) > 0) {
      plot_width <- plot_width + min(2, length(input$coxp_incl_int)) * 90
    }
  }
  list(plot_width = plot_width, plot_height = plot_height)
})

coxp_plot_width  <- function() coxp_plot() %>% (function(x) if (is.list(x)) x$plot_width else 650)
coxp_plot_height <- function() coxp_plot() %>% (function(x) if (is.list(x)) x$plot_height else 500)
coxp_pred_plot_height <- function() if (input$coxp_pred_plot) 500 else 1

## 9. 输出注册 -------------------------------------------------------------
output$coxp <- renderUI({
  register_print_output("summary_coxp", ".summary_coxp")
  register_print_output("predict_coxp", ".predict_print_coxp")
  register_plot_output("predict_plot_coxp", ".predict_plot_coxp", height_fun = "coxp_pred_plot_height")
  register_plot_output("plot_coxp", ".plot_coxp", height_fun = "coxp_plot_height", width_fun = "coxp_plot_width")
  
  coxp_output_panels <- tabsetPanel(
    id = "tabs_coxp",
    tabPanel(i18n$t("Summary"), value = "Summary",
             download_link("dl_coxp_coef"), br(),
             verbatimTextOutput("summary_coxp")),
    tabPanel(i18n$t("Predict"), value = "Predict",
             conditionalPanel("input.coxp_pred_plot == true",
                              download_link("dlp_coxp_pred"),
                              plotOutput("predict_plot_coxp", width = "100%", height = "100%")),
             download_link("dl_coxp_pred"), br(),
             verbatimTextOutput("predict_coxp")),
    tabPanel(i18n$t("Plot"), value = "Plot",
             download_link("dlp_coxp"),
             plotOutput("plot_coxp", width = "100%", height = "100%"))
  )
  
  stat_tab_panel(
    menu = i18n$t("Model > Estimate"),
    tool = i18n$t("Cox Proportional Hazards Regression"),
    tool_ui = "ui_coxp",
    output_panels = coxp_output_panels
  )
})

## 10. 可用性检查 ----------------------------------------------------------
coxp_available <- reactive({
  if (!input$dataset %in% names(r_data)) {
    return(i18n$t("数据集不存在：请先加载有效数据集"))
  }
  # 检查时间变量
  if (is.null(input$coxp_time) || input$coxp_time == "" || !input$coxp_time %in% colnames(r_data[[input$dataset]])) {
    return(i18n$t("时间变量无效：请选择数据集中存在的数值型变量"))
  }
  # 检查状态变量
  if (is.null(input$coxp_status) || input$coxp_status == "" || !input$coxp_status %in% colnames(r_data[[input$dataset]])) {
    return(i18n$t("状态变量无效：请选择数据集中存在的变量"))
  }
  # 检查解释变量
  if (is.null(input$coxp_evar) || length(input$coxp_evar) == 0 || length(setdiff(input$coxp_evar, colnames(r_data[[input$dataset]]))) > 0) {
    return(i18n$t("解释变量无效：请选择至少一个数据集中存在的变量"))
  }
  return("available")
})


## 11. 模型估计
.coxp <- eventReactive(input$coxp_run, {
  cat("---->coxp reactive entered")
  # 严格校验变量
  ds <- tryCatch({
    get_data(input$dataset, vars = c(), envir = r_data)  # 先获取完整数据集
  }, error = function(e) return(paste("数据集获取失败：", e$message)))
  
  if (is.character(ds)) return(ds)  # 数据集不存在，返回错误
  
  # 校验时间变量
  if (!input$coxp_time %in% colnames(ds)) {
    return(paste("时间变量不存在：数据集中无「", input$coxp_time, "」列", sep = ""))
  }
  if (!is.numeric(ds[[input$coxp_time]])) {
    return(paste("时间变量类型错误：「", input$coxp_time, "」需为数值型（整数/小数）", sep = ""))
  }
  
  # 校验状态变量
  if (!input$coxp_status %in% colnames(ds)) {
    return(paste("状态变量不存在：数据集中无「", input$coxp_status, "」列", sep = ""))
  }
  sv <- ds[[input$coxp_status]]
  sv <- if (is.factor(sv)) as.numeric(sv) - 1 else sv  # 因子转0/1
  sv <- ifelse(sv %in% c(0, 1), sv, 0)  # 非0/1强制为0
  n_event <- sum(sv)
  if (n_event < 1) {
    return(paste("事件数不足：状态变量转换后仅", n_event, "个事件（需至少1个），请检查状态变量编码"))
  }
  ds[[input$coxp_status]] <- sv
  
  # 校验解释变量（存在且非空）
  evar_missing <- setdiff(input$coxp_evar, colnames(ds))
  if (length(evar_missing) > 0) {
    return(paste("解释变量不存在：数据集中无「", paste(evar_missing, collapse = "、"), "」列", sep = ""))
  }
  
  # 构建模型并运行
  form <- as.formula(paste0("Surv(", input$coxp_time, ", ", input$coxp_status, ") ~ ", paste(input$coxp_evar, collapse = " + ")))
  model <- tryCatch({
    survival::coxph(form, data = ds)
  }, error = function(e) return(paste("coxph模型失败：", gsub("\n", " ", e$message)))) 
  
  return(model)
})

## 12. summary / predict / plot --------------------------------------------
.summary_coxp <- reactive({
  if (not_pressed(input$coxp_run)) {
    return(i18n$t("** 请点击「估计模型」按钮运行分析 **")) 
  }
  
  # 先检查可用性（提前拦截无效操作）
  avail_msg <- coxp_available()
  if (avail_msg != "available") {
    return(paste0("** 前置检查失败：", avail_msg, " **"))
  }
  
  # 获取模型结果（可能是coxph对象或错误文本）
  model_result <- .coxp()
  
  # 处理错误文本
  if (is.character(model_result)) {
    return(paste0("** 模型运行失败：", model_result, " **"))
  }
  
  # 处理有效模型
  if (inherits(model_result, "coxph")) {
    # 检查是否有系数（避免无系数的空模型）
    if (length(coef(model_result)) == 0) {
      return(i18n$t("** 未估计出系数：可能存在完全共线性、事件数不足或变量无效 **"))
    }
    # 输出标准summary
    return(summary(model_result))
  }
  
  # 其他未知错误
  return(i18n$t("** 未知错误：请检查数据集和变量设置 **"))
})

## 确保UI输出绑定正确
output$summary_coxp <- renderPrint({
  res <- .summary_coxp()
  if (is.character(res)) {
    cat(res, "\n")  
  } else {
    print(res)      
  }
})


.predict_coxp <- reactive({
  if (not_pressed(input$coxp_run)) return(i18n$t("** Press the Estimate button to estimate the model **"))
  if (coxp_available() != "available") return(coxp_available())
  if (is.empty(input$coxp_predict, "none")) return(i18n$t("** Select prediction input **"))
  if ((input$coxp_predict == "data" || input$coxp_predict == "datacmd") && is.empty(input$coxp_pred_data))
    return(i18n$t("** Select data for prediction **"))
  if (input$coxp_predict == "cmd" && is.empty(input$coxp_pred_cmd))
    return(i18n$t("** Enter prediction commands **"))
  
  withProgress(message = i18n$t("Generating predictions"), value = 1, {
    pi <- coxp_pred_inputs()
    pi$object <- .coxp()
    pi$envir  <- r_data
    do.call(predict, pi)
  })
})

.predict_print_coxp <- reactive({
  .predict_coxp() %>% { if (is.character(.)) cat(., "\n") else print(.) }
})

.predict_plot_coxp <- reactive({
  req(pressed(input$coxp_run), input$coxp_pred_plot, available(input$coxp_xvar), !is.empty(input$coxp_predict, "none"))
  withProgress(message = i18n$t("Generating prediction plot"), value = 1,
               do.call(plot, c(list(x = .predict_coxp()), coxp_pred_plot_inputs())))
})

.plot_coxp <- reactive({
  if (not_pressed(input$coxp_run)) return(i18n$t("** Press the Estimate button to estimate the model **"))
  if (is.empty(input$coxp_plots, "none")) return(i18n$t("Please select a plot from the drop-down menu"))
  if (coxp_available() != "available") return(coxp_available())
  if (!input$coxp_plots %in% c("coef", "dist", "influence", "vip", "pdp", "pred_plot")) req(input$coxp_nrobs)
  check_for_pdp_pred_plots("coxp")
  withProgress(message = i18n$t("Generating plots"), value = 1, {
    if (input$coxp_plots == "correlations") {
      capture_plot(do.call(plot, c(list(x = .coxp()), coxp_plot_inputs())))
    } else {
      do.call(plot, c(list(x = .coxp()), coxp_plot_inputs(), shiny = TRUE))
    }
  })
})

## 13. 报告 / 下载 / 存储  -------------------------------------------------
coxp_report <- function() {
  if (is.empty(input$coxp_evar)) return(invisible())
  outputs <- c("summary")
  inp_out <- list(list(prn = TRUE), "")
  figs <- FALSE
  if (!is.empty(input$coxp_plots, "none")) {
    inp <- check_plot_inputs(coxp_plot_inputs())
    inp_out[[2]] <- clean_args(inp, list())   # coxp_plot_args 已空
    inp_out[[2]]$custom <- FALSE
    outputs <- c(outputs, "plot")
    figs <- TRUE
  }
  if (!is.empty(input$coxp_store_res_name)) {
    fixed <- fix_names(input$coxp_store_res_name)
    updateTextInput(session, "coxp_store_res_name", value = fixed)
    xcmd <- paste0(input$dataset, " <- store(", input$dataset, ", result, name = \"", fixed, "\")\n")
  } else {
    xcmd <- ""
  }
  if (!is.empty(input$coxp_predict, "none") &&
      (!is.empty(input$coxp_pred_data) || !is.empty(input$coxp_pred_cmd))) {
    pred_args <- clean_args(coxp_pred_inputs(), list())
    if (!is.empty(pred_args$pred_cmd)) pred_args$pred_cmd <- strsplit(pred_args$pred_cmd, ";\\s*")[[1]] else pred_args$pred_cmd <- NULL
    if (!is.empty(pred_args$pred_data)) pred_args$pred_data <- as.symbol(pred_args$pred_data) else pred_args$pred_data <- NULL
    inp_out[[2 + figs]] <- pred_args
    outputs <- c(outputs, "pred <- predict")
    xcmd <- paste0(xcmd, "print(pred, n = 10)")
    if (input$coxp_predict %in% c("data", "datacmd")) {
      fixed <- unlist(strsplit(input$coxp_store_pred_name, "(\\s*,\\s*|\\s*;\\s*)")) %>%
        fix_names() %>% deparse(., control = getOption("dctrl"), width.cutoff = 500L)
      xcmd <- paste0(xcmd, "\n", input$coxp_pred_data, " <- store(", input$coxp_pred_data, ", pred, name = ", fixed, ")")
    }
    if (input$coxp_pred_plot && !is.empty(input$coxp_xvar)) {
      inp_out[[3 + figs]] <- clean_args(coxp_pred_plot_inputs(), list())
      inp_out[[3 + figs]]$result <- "pred"
      outputs <- c(outputs, "plot")
      figs <- TRUE
    }
  }
  update_report(
    inp_main = clean_args(coxp_inputs(), coxp_args),
    fun_name = "coxp",
    inp_out = inp_out,
    outputs = outputs,
    figs = figs,
    fig.width = coxp_plot_width(),
    fig.height = coxp_plot_height(),
    xcmd = xcmd
  )
}

observeEvent(input$coxp_store_res, {
  req(pressed(input$coxp_run))
  robj <- .coxp()
  if (!is.list(robj)) return()
  fixed <- fix_names(input$coxp_store_res_name)
  updateTextInput(session, "coxp_store_res_name", value = fixed)
  withProgress(message = i18n$t("Storing residuals"), value = 1,
               r_data[[input$dataset]] <- store(r_data[[input$dataset]], robj, name = fixed))
})

observeEvent(input$coxp_store_pred, {
  req(!is.empty(input$coxp_pred_data), pressed(input$coxp_run))
  pred <- .predict_coxp()
  if (is.null(pred)) return()
  fixed <- fix_names(input$coxp_store_pred_name)
  updateTextInput(session, "coxp_store_pred_name", value = fixed)
  withProgress(message = i18n$t("Storing predictions"), value = 1,
               r_data[[input$coxp_pred_data]] <- store(r_data[[input$coxp_pred_data]], pred, name = fixed))
})

## 14. 下载 ----------------------------------------------------------------
dl_coxp_coef <- function(path) {
  if (pressed(input$coxp_run)) {
    write.coeff(.coxp(), file = path)
  } else {
    cat(i18n$t("No output available. Press the Estimate button to generate results"), file = path)
  }
}

download_handler(
  id = "dl_coxp_coef",
  fun = dl_coxp_coef,
  fn = function() paste0(input$dataset, "_coxp_coef"),
  type = "csv",
  caption = i18n$t("Save Cox coefficients")
)

dl_coxp_pred <- function(path) {
  if (pressed(input$coxp_run)) {
    write.csv(.predict_coxp(), file = path, row.names = FALSE)
  } else {
    cat(i18n$t("No output available. Press the Estimate button to generate results"), file = path)
  }
}

download_handler(
  id = "dl_coxp_pred",
  fun = dl_coxp_pred,
  fn = function() paste0(input$dataset, "_coxp_pred"),
  type = "csv",
  caption = i18n$t("Save Cox predictions")
)

download_handler(
  id = "dlp_coxp_pred",
  fun = download_handler_plot,
  fn = paste0(input$dataset, "_coxp_pred"),
  type = "png",
  caption = i18n$t("Save Cox prediction plot"),
  plot = .predict_plot_coxp,
  width = plot_width,
  height = coxp_pred_plot_height
)

download_handler(
  id = "dlp_coxp",
  fun = download_handler_plot,
  fn = function() paste0(input$dataset, "_", input$coxp_plots, "_coxp"),
  type = "png",
  caption = i18n$t("Save Cox plot"),
  plot = .plot_coxp,
  width = coxp_plot_width,
  height = coxp_plot_height
)

## 15. 报告 / 截图 ---------------------------------------------------------
observeEvent(input$coxp_report, {
  r_info[["latest_screenshot"]] <- NULL
  coxp_report()
})

observeEvent(input$coxp_screenshot, {
  r_info[["latest_screenshot"]] <- NULL
  radiant_screenshot_modal("modal_coxp_screenshot")
})

observeEvent(input$modal_coxp_screenshot, {
  coxp_report()
  removeModal()
})