Download R/server.R from fmegahed/decisions: direct link, hf CLI and curl.
- Browser
- Download file 26.2 kB
-
https://huggingface.co/spaces/fmegahed/decisions/resolve/main/R/server.R
- Command line
-
hf download hf://spaces/fmegahed/decisions/R/server.R
-
curl -L -o server.R https://huggingface.co/spaces/fmegahed/decisions/resolve/main/R/server.R
26.2 kB
| # Server: the wiring of the page. The logic is in the other files of R/; this | |
| # file keeps the state of a session (the question, the data, the answers and | |
| # the spend) and runs the calls to Jev in chunks, as a background task. | |
| # | |
| # app_deps() holds what a test replaces: decide(), the usage store, the status | |
| # reader, the clock, and `async` (FALSE runs the chunks in this R process). | |
| # app.R makes it once for all sessions, so its `state` (the stop for the UTC | |
| # day after a key or credit error) holds for every visitor. | |
| # The dependencies of the server. With SPACE_MOCK=1 (local checks only; the | |
| # Dockerfile never sets it), the mock of decide() and a store in memory | |
| # replace the defaults. | |
| app_deps <- function(decide_fn = decisions::decide, store = usage_store_hf(), | |
| status_fn = read_status, now = Sys.time, async = TRUE) { | |
| if (Sys.getenv("SPACE_MOCK") == "1" && (missing(decide_fn) || missing(store))) { | |
| mock <- mock_deps() | |
| if (missing(decide_fn)) decide_fn <- mock$decide_fn | |
| if (missing(store)) store <- mock$store | |
| } | |
| list( | |
| decide_fn = decide_fn, store = store, status_fn = status_fn, now = now, | |
| async = async, state = new.env(parent = emptyenv()) | |
| ) | |
| } | |
| # The mock of the tests (with the saved answers of both samples) and a store in | |
| # memory. The mock stays in tests/: the app code holds no mock. APP_ROOT is the | |
| # app folder (set by app.R, and by the helper of the tests). | |
| mock_deps <- function(path = file.path(APP_ROOT, "tests", "testthat", "helper-mock.R")) { | |
| if (!file.exists(path)) { | |
| stop( | |
| "SPACE_MOCK=1 needs ", path, | |
| ". Start the app from the repository folder, or unset SPACE_MOCK.", | |
| call. = FALSE | |
| ) | |
| } | |
| env <- new.env(parent = environment(mock_deps)) | |
| sys.source(path, envir = env) | |
| lookup <- dplyr::bind_rows(env$sample_lookup("textiles"), env$sample_lookup("injuries")) | |
| list(decide_fn = env$mock_decide(lookup), store = usage_store_memory()) | |
| } | |
| # The category that stops the runs today, or NULL | |
| stop_reason <- function(deps) { | |
| if (identical(deps$state$stop_day, utc_date(deps$now()))) deps$state$stop_reason | |
| } | |
| # The accounts of a chunk, in the main R process (never in a daemon): add the | |
| # usage to the store, stop the day after a key or credit error, and write the | |
| # log line. Rows that failed before any request ("config") cost nothing. | |
| account_chunk <- function(answers, deps) { | |
| categories <- classify_error(answers$.error) | |
| use <- chunk_usage(answers[!categories %in% "config", ]) | |
| now <- deps$now() | |
| usage <- if (use$calls > 0) usage_record(deps$store, now, use$calls, use$tokens, use$cost) | |
| stop <- dplyr::first(categories[stops_the_day(categories)]) | |
| if (!is.na(stop)) { | |
| deps$state$stop_day <- utc_date(now) | |
| deps$state$stop_reason <- stop | |
| } | |
| log_chunk(now, nrow(answers), use, categories) | |
| list(answers = answers, cost = use$cost, usage = usage) | |
| } | |
| app_server <- function(input, output, session, deps = app_deps()) { | |
| # --- The folder of the session: uploads, imports and thumbnails ----------- | |
| dir <- tempfile("session") | |
| dir.create(file.path(dir, "thumbs"), recursive = TRUE) | |
| # The thumbnails are served under a prefix that no other visitor can guess | |
| prefix <- paste(openssl::rand_bytes(16), collapse = "") | |
| shiny::addResourcePath(prefix, file.path(dir, "thumbs")) | |
| uploads <- character() | |
| folders <- 0L | |
| new_dir <- function(kind) { | |
| folders <<- folders + 1L | |
| file.path(dir, kind, folders) | |
| } | |
| keep_uploads <- function(files) uploads <<- c(uploads, files$datapath) | |
| job <- new.env(parent = emptyenv()) | |
| # Files are deleted only when no chunk is in flight: a chunk reads its images | |
| # and references until it ends, and a missing file would stop the day | |
| after_chunk <- function(delete) { | |
| if (isTRUE(job$in_flight)) job$deletes <- c(job$deletes, delete) else delete() | |
| } | |
| session$onSessionEnded(\() { | |
| shiny::removeResourcePath(prefix) | |
| remove_uploads(uploads) | |
| after_chunk(\() unlink(dir, recursive = TRUE)) | |
| }) | |
| # The thumbnail of an image, and its address ("" for an image that cannot be read) | |
| thumb_file <- function(path) file.path(dir, "thumbs", paste0(rlang::hash(path), ".jpg")) | |
| thumb <- function(paths) { | |
| purrr::map_chr(paths, \(path) { | |
| dest <- thumb_file(path) | |
| if (file.exists(dest) || make_thumb(path, dest)) paste0(prefix, "/", basename(dest)) else "" | |
| }) | |
| } | |
| # --- State -------------------------------------------------------------------- | |
| question <- shiny::reactiveVal(START_QUESTION) | |
| refs <- shiny::reactiveVal(list()) # reference paths, one element for each answer | |
| data_mode <- shiny::reactiveVal("image") | |
| data_source <- shiny::reactiveVal("upload") # the page opens empty | |
| image_upload <- shiny::reactiveVal(NULL) | |
| csv_file <- shiny::reactiveVal(NULL) | |
| text_kind <- shiny::reactiveVal("paste") | |
| # list(answers, key, saved, token of the run, rows of Try on 5 or NULL for all) | |
| results <- shiny::reactiveVal(NULL) | |
| mean_tokens <- shiny::reactiveVal(NULL) # list(fingerprint, mean) | |
| session_cost <- shiny::reactiveVal(0) | |
| usage <- shiny::reactiveVal(deps$store$read()) | |
| notice <- shiny::reactiveVal(character()) | |
| running <- shiny::reactiveVal(FALSE) | |
| rows_version <- shiny::reactiveVal(0) # renders the answer rows again | |
| # --- The spec of the sidebar -------------------------------------------------- | |
| text_upload <- shiny::reactive({ | |
| csv <- csv_file() | |
| if (text_kind() == "paste" || is.null(csv)) { | |
| return(c(read_text_paste(input$paste_text), list(name = "texts"))) | |
| } | |
| if (length(csv$columns) == 0) { | |
| return(list( | |
| inputs = empty_inputs(), problems = "The CSV file could not be read.", name = csv$name | |
| )) | |
| } | |
| column <- if (isTRUE(input$csv_text_col %in% csv$columns)) input$csv_text_col else csv$text_col | |
| label <- if (isTRUE(input$csv_label_col %in% csv$columns)) input$csv_label_col | |
| c(read_text_csv(csv$path, column, label, csv$file), list(name = csv$name, text_column = column)) | |
| }) | |
| data <- shiny::reactive({ | |
| id <- data_source() | |
| if (id != "upload") return(list(inputs = sample_data(id)$spec$inputs, name = id)) | |
| upload <- if (data_mode() == "image") image_upload() else text_upload() | |
| upload %||% list(inputs = empty_inputs(), name = "upload") | |
| }) | |
| matched <- shiny::reactive(match_labels(data()$inputs, question()$answers)) | |
| spec <- shiny::reactive({ | |
| q <- question() | |
| references <- if (data_mode() == "image" && q$type != "score") { | |
| stats::setNames(refs()[seq_along(q$answers)], names(q$answers)) |> purrr::compact() | |
| } | |
| list( | |
| mode = data_mode(), question = q, | |
| references = if (length(references) > 0) references, | |
| inputs = matched()$inputs, source = data_source(), name = data()$name, | |
| text_column = data()$text_column | |
| ) | |
| }) | |
| key <- shiny::reactive(answers_key(spec())) | |
| # The type and the names of the answers: the reference rows and the target follow them | |
| layout <- shiny::reactiveVal() | |
| shiny::observe(layout(list(type = question()$type, answers = names(question()$answers)))) | |
| # Send the setup of the server to the inputs of the page. The page answers | |
| # each update with the new value, so an observer never sends its own input | |
| # (or one above it: mode, then source, then the question) back: two crossing | |
| # updates would flip the input forever. `send` names the inputs of the data. | |
| show_setup <- function(send = c("mode", "source")) { | |
| q <- question() | |
| if ("mode" %in% send) shiny::updateRadioButtons(session, "mode", selected = data_mode()) | |
| if ("source" %in% send) { | |
| shiny::updateSelectInput( | |
| session, "source", choices = source_choices(data_mode()), selected = data_source() | |
| ) | |
| } | |
| shiny::updateRadioButtons(session, "qtype", selected = q$type) | |
| shiny::updateTextAreaInput(session, "instructions", value = q$instructions) | |
| shiny::updateRadioButtons( | |
| session, "view", selected = if (data_mode() == "image") "gallery" else "table" | |
| ) | |
| rows_version(rows_version() + 1) | |
| } | |
| # A sample opens with no answers and makes no call. A run of the unchanged | |
| # sample then shows its saved run (see start_run()). | |
| load_sample <- function(id, send = c("mode", "source")) { | |
| sample <- sample_data(id) | |
| question(sample$spec$question) | |
| refs(unname(sample$spec$references) %||% list()) | |
| data_mode(sample$spec$mode) | |
| data_source(id) | |
| notice(character()) | |
| show_setup(send) | |
| } | |
| # The page opens empty: Images, your own data, no sample. The first HTML of | |
| # the page holds the same values, so the server sends no update at the start. | |
| # --- Data ------------------------------------------------------------------- | |
| shiny::observeEvent(input$sample_textiles, load_sample("textiles")) | |
| shiny::observeEvent(input$sample_injuries, load_sample("injuries")) | |
| shiny::observeEvent(input$mode, { | |
| if (input$mode == data_mode()) return() | |
| if (data_source() != "upload") { | |
| return(load_sample(if (input$mode == "image") "textiles" else "injuries", send = "source")) | |
| } | |
| data_mode(input$mode) | |
| refs(list()) | |
| notice(character()) | |
| show_setup(send = "source") | |
| }) | |
| shiny::observeEvent(input$source, { | |
| # A source of the other mode is the late answer to an older update | |
| if (input$source == data_source() || !input$source %in% source_choices(data_mode())) return() | |
| if (input$source != "upload") return(load_sample(input$source, send = character())) | |
| data_source("upload") | |
| refs(list()) | |
| notice(character()) | |
| }) | |
| shiny::observeEvent(input$upload_images, { | |
| files <- input$upload_images | |
| keep_uploads(files) | |
| notice(character()) | |
| old <- image_upload() | |
| if (!is.null(old)) { | |
| after_chunk(\() unlink(c(old$dir, thumb_file(old$inputs$path)), recursive = TRUE)) | |
| } | |
| read <- read_images(files, dir = new_dir("images")) | |
| bad <- thumb(read$inputs$path) == "" | |
| image_upload(list( | |
| inputs = read$inputs[!bad, ], dir = read$dir, | |
| problems = c( | |
| read$problems, sprintf("%s could not be read and was skipped.", read$inputs$file[bad]) | |
| ), | |
| name = if (nrow(files) == 1) tools::file_path_sans_ext(files$name) else "images" | |
| )) | |
| }) | |
| shiny::observeEvent(input$paste_text, text_kind("paste"), ignoreInit = TRUE) | |
| shiny::observeEvent(input$upload_csv, { | |
| file <- input$upload_csv | |
| keep_uploads(file) | |
| notice(character()) | |
| path <- file.path(new_dir("csv"), "data.csv") | |
| dir.create(dirname(path), recursive = TRUE) | |
| file.copy(file$datapath, path) | |
| columns <- csv_columns(path) | |
| text_col <- if ("text" %in% columns) "text" else columns[1] | |
| csv_file(list( | |
| path = path, file = file$name, name = tools::file_path_sans_ext(file$name), | |
| columns = columns, text_col = text_col | |
| )) | |
| text_kind("csv") | |
| shiny::updateSelectInput(session, "csv_text_col", choices = columns, selected = text_col) | |
| shiny::updateSelectInput( | |
| session, "csv_label_col", choices = c(None = "", columns), selected = "" | |
| ) | |
| }) | |
| # --- Question --------------------------------------------------------------- | |
| # Each input changes one part of the question. A change that the server made | |
| # comes back from the page with the same value, so it changes nothing. | |
| shiny::observeEvent(input$instructions, { | |
| q <- question() | |
| q$instructions <- input$instructions | |
| question(q) | |
| }) | |
| edit_answer <- function(i, part, value) { | |
| q <- question() | |
| if (i > length(q$answers) || (part == "name" && q$type == "noul")) return() | |
| if (part == "name") names(q$answers)[i] <- value else q$answers[[i]] <- value | |
| question(q) | |
| } | |
| purrr::walk(seq_len(max(unlist(ANSWER_RANGE))), \(i) { | |
| shiny::observeEvent(input[[paste0("answer_name_", i)]], { | |
| edit_answer(i, "name", input[[paste0("answer_name_", i)]]) | |
| }) | |
| shiny::observeEvent(input[[paste0("answer_desc_", i)]], { | |
| edit_answer(i, "desc", input[[paste0("answer_desc_", i)]]) | |
| }) | |
| shiny::observeEvent(input[[paste0("answer_remove_", i)]], { | |
| q <- question() | |
| if (i > length(q$answers) || length(q$answers) <= answer_range(q$type)[1]) return() | |
| q$answers <- q$answers[-i] | |
| question(q) | |
| refs(refs()[-i]) | |
| rows_version(rows_version() + 1) | |
| }) | |
| }) | |
| shiny::observeEvent(input$answer_add, { | |
| q <- question() | |
| n <- length(q$answers) | |
| if (n >= answer_range(q$type)[2]) return() | |
| name <- utils::tail(make.unique(c(names(q$answers), paste("answer", n + 1)), sep = " "), 1) | |
| q$answers <- c(q$answers, stats::setNames("", name)) | |
| question(q) | |
| rows_version(rows_version() + 1) | |
| }) | |
| shiny::observeEvent(input$qtype, { | |
| q <- question() | |
| if (input$qtype == q$type) return() | |
| q$answers <- if (input$qtype == "noul") { | |
| c(true = "", false = "") | |
| } else { | |
| utils::head(q$answers, answer_range(input$qtype)[2]) | |
| } | |
| q$type <- input$qtype | |
| question(q) | |
| rows_version(rows_version() + 1) | |
| }) | |
| # The reference inputs: a new upload replaces the references of its answer | |
| seen <- character() | |
| shiny::observe({ | |
| ids <- ref_input_id(shiny::req(layout())$answers) | |
| for (i in seq_along(ids)) { | |
| files <- input[[ids[i]]] | |
| if (is.null(files) || all(files$datapath %in% seen)) next | |
| seen <<- c(seen, files$datapath) | |
| keep_uploads(files) | |
| read <- read_references(files, dir = new_dir("references")) | |
| shiny::isolate({ | |
| r <- refs() | |
| r[[i]] <- read$paths | |
| refs(r) | |
| notice(read$problems) | |
| }) | |
| } | |
| }) | |
| # Import restores the question and the references; it makes no call | |
| shiny::observeEvent(input$import_file, { | |
| file <- input$import_file | |
| keep_uploads(file) | |
| read <- read_import(file$datapath, dir = new_dir("import")) | |
| if (is.null(read$question)) return(notice(read$problems)) | |
| question(read$question) | |
| refs(purrr::map(names(read$question$answers), \(a) read$references[[a]] %||% character())) | |
| if (length(read$references) > 0 && data_mode() == "text") { | |
| data_mode("image") | |
| data_source("upload") | |
| } | |
| notice(c("The setup is imported.", read$problems)) | |
| show_setup() | |
| }) | |
| shiny::observe({ | |
| l <- shiny::req(layout()) | |
| shiny::updateSelectInput( | |
| session, "target", choices = if (l$type == "noul") "true" else l$answers, | |
| selected = target_answer(shiny::isolate(question()), shiny::isolate(input$target)) | |
| ) | |
| }) | |
| # The answers show only while the setup and the inputs are those of the | |
| # answers (table() checks the key). They are not deleted: the page can answer | |
| # an older update late, and the setup then comes back. A change stops a run. | |
| shiny::observe({ | |
| k <- key() | |
| if (shiny::isolate(running()) && !identical(job$key, k)) job$stop <- TRUE | |
| }) | |
| # --- Runs ------------------------------------------------------------------- | |
| mean_for <- function(s) { | |
| m <- mean_tokens() | |
| if (!is.null(m) && identical(m$fingerprint, spec_fingerprint(s))) m$mean | |
| } | |
| # A run of an unchanged sample shows its saved run: it costs nothing, so no | |
| # limit stops it | |
| checks <- shiny::reactive({ | |
| s <- spec() | |
| saved <- sample_unchanged(s) | |
| status <- spend_status(session_cost(), usage(), deps$now()) | |
| purrr::map(c(try5 = 5, run_all = Inf), \(k) { | |
| n <- min(k, nrow(s$inputs)) | |
| cost <- if (saved) 0 else estimate_cost(s, n, mean_for(s)) | |
| allowed <- if (saved) list(ok = TRUE, reason = NA_character_) else run_allowed(cost, status) | |
| # No inputs: the buttons stay off, and no limit is the reason | |
| list(ok = allowed$ok && n > 0, reason = allowed$reason, n = n, cost = cost, saved = saved) | |
| }) | |
| }) | |
| shiny::observe({ | |
| shiny::updateActionButton(session, "try5", disabled = !checks()$try5$ok) | |
| shiny::updateActionButton(session, "run_all", disabled = !checks()$run_all$ok) | |
| }) | |
| # Each chunk runs in a mirai daemon (or here, when async is FALSE); its | |
| # accounts are kept in this process, also when the session has ended. The | |
| # files that waited for the chunk are deleted when it ends. | |
| task <- shiny::ExtendedTask$new(function(spec, rows) { | |
| job$in_flight <- TRUE | |
| chunk <- tryCatch( | |
| if (deps$async) { | |
| mirai::mirai( | |
| run_chunk(spec, rows, decide_fn), | |
| .args = list(spec = spec, rows = rows, decide_fn = deps$decide_fn) | |
| ) | |
| } else { | |
| run_chunk(spec, rows, deps$decide_fn) | |
| }, | |
| error = \(e) promises::promise_reject(e) | |
| ) | |
| promises::then(promises::promise_resolve(chunk), \(answers) account_chunk(answers, deps)) |> | |
| promises::finally(\() { | |
| job$in_flight <- FALSE | |
| deletes <- job$deletes | |
| job$deletes <- NULL | |
| purrr::walk(deletes, \(delete) delete()) | |
| }) |> | |
| # A closed session has no task to finish (it would fail on its destroyed | |
| # values): its chunk was counted above, and the promise stays open | |
| promises::then( | |
| onFulfilled = \(done) if (session$isClosed()) never_settles() else done, | |
| onRejected = \(e) if (session$isClosed()) never_settles() else stop(e) | |
| ) | |
| }) | |
| never_settles <- function() promises::promise(\(resolve, reject) NULL) | |
| # Before each chunk, its images and references must exist. A missing file | |
| # ends the run; decide() would abort, and that would stop the day. | |
| run_next <- function() { | |
| rows <- job$chunks[[job$done + 1L]] | |
| if (!all(file.exists(chunk_files(job$spec, rows)))) { | |
| notice("The files of this run are not available. Upload them again.") | |
| return(end_run()) | |
| } | |
| task$invoke(job$spec, rows) | |
| } | |
| start_run <- function(n) { | |
| if (running()) return() | |
| s <- spec() | |
| n <- min(n, nrow(s$inputs)) | |
| # Try on 5 takes the rows of try_rows(); Run all takes all rows (try_rows() | |
| # gives all rows when n is the number of inputs). The R code repeats the | |
| # rows of Try on 5 (`subset`). | |
| rows <- try_rows(s$inputs, n) | |
| subset <- if (n < nrow(s$inputs)) rows | |
| # An unchanged sample shows the saved answers of these rows at once: no | |
| # call, no cost and no write to the usage store | |
| if (sample_unchanged(s)) { | |
| notice(character()) | |
| saved <- sample_data(s$source)$answers[rows, ] | |
| return(results(list(answers = saved, key = key(), saved = TRUE, rows = subset))) | |
| } | |
| problems <- run_problems(s) | |
| if (length(problems) > 0) return(notice(problems)) | |
| if (!is.null(stop_reason(deps))) return(notice(error_message(stop_reason(deps)))) | |
| # The latest totals of all sessions decide, not the copy of this session | |
| latest <- deps$store$read() | |
| usage(latest) | |
| allowed <- run_allowed( | |
| estimate_cost(s, n, mean_for(s)), spend_status(session_cost(), latest, deps$now()) | |
| ) | |
| if (!allowed$ok) return(notice(limit_message(allowed$reason))) | |
| notice(character()) | |
| job$spec <- s | |
| job$key <- key() | |
| job$chunks <- purrr::map(chunk_ids(n), \(i) rows[i]) | |
| job$rows <- subset | |
| job$done <- 0L | |
| job$stop <- FALSE | |
| job$answers <- NULL | |
| # The run writes its answers only while the card holds the answers that it | |
| # found or wrote | |
| job$token <- (job$token %||% 0L) + 1L | |
| results(utils::modifyList(results() %||% list(), list(token = job$token))) | |
| running(TRUE) | |
| session$sendCustomMessage("run-state", TRUE) | |
| run_next() | |
| } | |
| end_run <- function() { | |
| running(FALSE) | |
| session$sendCustomMessage("run-state", FALSE) | |
| } | |
| # After each chunk: show its answers, then run the next chunk or stop | |
| finish_chunk <- function(done) { | |
| if (!running()) return() | |
| job$done <- job$done + 1L | |
| if (is.null(done)) { | |
| notice("The run stopped. Try again.") | |
| return(end_run()) | |
| } | |
| session_cost(session_cost() + done$cost) | |
| if (!is.null(done$usage)) usage(done$usage) | |
| job$answers <- dplyr::bind_rows(job$answers, done$answers) | |
| shown <- identical(results()$token, job$token) | |
| if (shown) { | |
| results(list( | |
| answers = job$answers, key = job$key, saved = FALSE, token = job$token, rows = job$rows | |
| )) | |
| } | |
| # Only calls with tokens count: a mean of 0 would make the next run look free | |
| tokens <- job$answers$.input_tokens | |
| tokens <- tokens[!is.na(tokens) & tokens > 0] | |
| if (length(tokens) > 0) { | |
| mean_tokens(list(fingerprint = spec_fingerprint(job$spec), mean = mean(tokens))) | |
| } | |
| reason <- stop_reason(deps) | |
| if (!is.null(reason)) notice(error_message(reason)) | |
| # A run whose answers nobody sees stops (its chunk is counted all the same) | |
| if (job$stop || !shown || !is.null(reason) || job$done >= length(job$chunks)) return(end_run()) | |
| run_next() | |
| } | |
| shiny::observe({ | |
| if (!task$status() %in% c("success", "error")) return() | |
| done <- tryCatch(task$result(), error = \(e) NULL) | |
| shiny::isolate(finish_chunk(done)) | |
| }) | |
| shiny::observeEvent(input$try5, start_run(5)) | |
| shiny::observeEvent(input$run_all, start_run(Inf)) | |
| # Stop takes effect at the end of the current chunk | |
| shiny::observeEvent(input$stop, job$stop <- TRUE, priority = 10) | |
| # --- Outputs: sidebar ------------------------------------------------------- | |
| output$answers_ui <- shiny::renderUI({ | |
| rows_version() | |
| answer_rows(shiny::isolate(question())) | |
| }) | |
| output$references_ui <- shiny::renderUI({ | |
| l <- shiny::req(layout()) | |
| r <- refs() | |
| srcs <- purrr::map(seq_along(l$answers), \(i) { | |
| if (i <= length(r)) thumb(r[[i]]) else character() | |
| }) | |
| reference_rows( | |
| list(type = l$type, answers = stats::setNames(character(length(l$answers)), l$answers)), | |
| stats::setNames(srcs, l$answers) | |
| ) | |
| }) | |
| # A cost below one cent reads "< $0.01", not "$0.00": a run is never free. | |
| # Only the saved run of an unchanged sample costs nothing. | |
| output$run_estimate <- shiny::renderText({ | |
| all <- checks()$run_all | |
| if (all$n == 0) return("") | |
| if (all$saved) return(paste(all$n, "\u00b7 saved run, no cost")) | |
| cost <- if (all$cost > 0 && all$cost < 0.005) "< $0.01" else sprintf("\u2248 $%.2f", all$cost) | |
| paste(all$n, "\u00b7", cost) | |
| }) | |
| output$spend_meter <- shiny::renderUI(spend_meter_ui(session_cost())) | |
| output$limits_text <- shiny::renderText({ | |
| all <- checks()$run_all | |
| refused <- if (!all$ok && !is.na(all$reason)) limit_message(all$reason) | |
| sentences <- unique(c(notice(), data()$problems, matched()$problems, refused)) | |
| paste(stringr::str_replace(sentences, "([^.!?])$", "\\1."), collapse = " ") | |
| }) | |
| # --- Outputs: answers ------------------------------------------------------- | |
| target <- shiny::reactive(target_answer(question(), input$target)) | |
| table <- shiny::reactive({ | |
| r <- results() | |
| if (is.null(r$answers) || !identical(r$key, key())) return(NULL) | |
| answers_table(spec(), r$answers, target()) | |
| }) | |
| # The table of the downloads: no rows while the card shows no answers | |
| download_table <- shiny::reactive(table() %||% empty_table(spec())) | |
| stats <- shiny::reactive(if (!is.null(table())) answer_stats(table(), spec(), target())) | |
| output$stats <- shiny::renderUI({ | |
| if (is.null(stats())) return(NULL) | |
| r <- results() | |
| note <- if (r$saved) { | |
| sprintf("Saved run of %s, %s", SAVED_RUN_DATE, MODEL) | |
| } else if (running()) { | |
| sprintf("%d of %d", nrow(r$answers), length(unlist(job$chunks))) | |
| } | |
| stats_ui(stats(), spec()$mode, note) | |
| }) | |
| output$empty_answers <- shiny::renderUI({ | |
| if (is.null(table())) { | |
| shiny::p( | |
| class = "empty", | |
| "No answers yet. Pick a sample from Samples, or add your own images or texts." | |
| ) | |
| } | |
| }) | |
| # The facts of a sample describe its full saved run, not Try on 5 | |
| output$help_answers <- shiny::renderText({ | |
| if (is.null(stats())) return(help_text("answers")) | |
| r <- results() | |
| help_note(spec(), stats(), target(), saved = r$saved, full = is.null(r$rows)) | |
| }) | |
| output$gallery <- shiny::renderUI({ | |
| t <- table() | |
| if (is.null(t) || spec()$mode != "image") return(NULL) | |
| inputs <- spec()$inputs | |
| gallery_ui(t, target(), thumb(inputs$path[match(t$id, inputs$id)])) | |
| }) | |
| output$table <- shiny::renderTable( | |
| if (!is.null(table())) table_view(table(), spec()), | |
| digits = 2, na = "" | |
| ) | |
| output$download_answers <- shiny::downloadHandler( | |
| filename = \() paste0(run_name(spec()), "-answers.csv"), | |
| content = \(file) readr::write_file(answers_csv(download_table()), file) | |
| ) | |
| # --- Outputs: R code and bundle --------------------------------------------- | |
| run_date <- function() as.Date(deps$now()) | |
| zip_name <- function() sprintf("%s-%s.zip", run_name(spec()), format(run_date())) | |
| # The code repeats the run on show: after Try on 5, the same rows; with no | |
| # answers on show, or after Run all, all inputs | |
| code_rows <- shiny::reactive(if (!is.null(table())) results()$rows) | |
| code <- shiny::reactive(run_code(spec(), run_date(), code_rows())) | |
| output$code_summary <- shiny::renderText( | |
| sprintf("R code of this run \u00b7 %d lines of code", code_lines(code())) | |
| ) | |
| output$code <- shiny::renderText(code()) | |
| output$bundle_files <- shiny::renderUI(bundle_files_ui(bundle_names(spec()), zip_name())) | |
| for (id in c("code", "bundle_files", "code_summary")) { | |
| shiny::outputOptions(output, id, suspendWhenHidden = FALSE) | |
| } | |
| output$download_code <- shiny::downloadHandler( | |
| filename = \() paste0(run_name(spec()), ".R"), | |
| content = \(file) readr::write_file(paste0(code(), "\n"), file) | |
| ) | |
| output$download_bundle <- shiny::downloadHandler( | |
| filename = zip_name, | |
| content = \(file) write_bundle(spec(), download_table(), file, run_date(), code_rows()) | |
| ) | |
| # --- Outputs: status ---------------------------------------------------------- | |
| # The status file is read at the start and every hour | |
| status <- shiny::reactive({ | |
| shiny::invalidateLater(3600000) | |
| status_summary(deps$status_fn(), deps$now()) | |
| }) | |
| output$status_chip <- shiny::renderText(status_chip(status())) | |
| output$record_table <- shiny::renderTable( | |
| { | |
| s <- status() | |
| s$last |> | |
| dplyr::arrange(dplyr::desc(time_utc)) |> | |
| dplyr::transmute( | |
| `Time (UTC)` = time_utc, HTTP = suppressWarnings(as.integer(http_status)), | |
| Seconds = suppressWarnings(as.numeric(seconds)), | |
| !!sprintf("Answered (%d of %d)", s$answered, s$checks) := answered, | |
| Version = app_version | |
| ) | |
| }, | |
| digits = 1, na = "" | |
| ) | |
| } | |