Deploy 71faef1 from GitHub
Browse files- R/answers.R +10 -8
- R/bundle.R +4 -3
- R/code.R +45 -17
- R/config.R +3 -0
- R/help.R +2 -2
- R/runs.R +11 -0
- R/samples.R +18 -0
- R/server.R +44 -16
- R/status.R +1 -1
- R/ui.R +4 -4
- README.md +1 -1
- www/app.js +1 -1
- www/theme.css +3 -1
R/answers.R
CHANGED
|
@@ -20,7 +20,6 @@ answers_table <- function(spec, answers, target) {
|
|
| 20 |
spec$inputs |> dplyr::select(id, file, text, label),
|
| 21 |
by = "id"
|
| 22 |
) |>
|
| 23 |
-
dplyr::mutate(text = stringr::str_trunc(text, 150, ellipsis = "...")) |>
|
| 24 |
dplyr::select(
|
| 25 |
id, file, text, answer, p_target, dplyr::starts_with("p_"),
|
| 26 |
label, tokens, seconds, problem
|
|
@@ -64,17 +63,20 @@ alt_text <- function(row, target) {
|
|
| 64 |
)
|
| 65 |
}
|
| 66 |
|
| 67 |
-
# What the probability is.
|
| 68 |
-
#
|
| 69 |
-
|
|
|
|
| 70 |
generic <- sprintf(
|
| 71 |
"The probability is the raw probability of Jev for %s. It is not calibrated.",
|
| 72 |
target
|
| 73 |
)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
if (
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
# The CSV text of the table for the download
|
|
|
|
| 20 |
spec$inputs |> dplyr::select(id, file, text, label),
|
| 21 |
by = "id"
|
| 22 |
) |>
|
|
|
|
| 23 |
dplyr::select(
|
| 24 |
id, file, text, answer, p_target, dplyr::starts_with("p_"),
|
| 25 |
label, tokens, seconds, problem
|
|
|
|
| 63 |
)
|
| 64 |
}
|
| 65 |
|
| 66 |
+
# What the probability is. A saved run of a sample adds that the app reuses it.
|
| 67 |
+
# The facts of a sample describe its full saved run: they follow only for all
|
| 68 |
+
# its saved answers (`full`, not Try on 5) and its default target.
|
| 69 |
+
help_note <- function(spec, stats, target, saved = FALSE, full = TRUE) {
|
| 70 |
generic <- sprintf(
|
| 71 |
"The probability is the raw probability of Jev for %s. It is not calibrated.",
|
| 72 |
target
|
| 73 |
)
|
| 74 |
+
saved <- spec$source %in% names(SAMPLE_FACTS_TEXT) && saved
|
| 75 |
+
if (!saved) return(generic)
|
| 76 |
+
facts <- if (full && identical(target, target_answer(spec$question))) {
|
| 77 |
+
SAMPLE_FACTS_TEXT[[spec$source]]
|
| 78 |
+
}
|
| 79 |
+
paste(c(generic, facts, SAVED_RUN_TEXT), collapse = " ")
|
| 80 |
}
|
| 81 |
|
| 82 |
# The CSV text of the table for the download
|
R/bundle.R
CHANGED
|
@@ -70,14 +70,15 @@ bundle_readme <- function(spec, refs, name, has_data, date) {
|
|
| 70 |
}
|
| 71 |
|
| 72 |
# Write the bundle of a run to `zipfile` and return its path. `table` is the
|
| 73 |
-
# answers table of answers_table().
|
| 74 |
-
|
|
|
|
| 75 |
name <- run_name(spec)
|
| 76 |
refs <- reference_files(spec)
|
| 77 |
dir <- tempfile("bundle")
|
| 78 |
dir.create(dir)
|
| 79 |
on.exit(unlink(dir, recursive = TRUE))
|
| 80 |
-
readr::write_file(paste0(run_code(spec, date), "\n"), file.path(dir, paste0(name, ".R")))
|
| 81 |
readr::write_file(answers_csv(table), file.path(dir, paste0(name, "-answers.csv")))
|
| 82 |
# The setup holds the references as the prepared images; the key stays out
|
| 83 |
references <- if (nrow(refs) > 0) spec$references[unique(refs$answer)]
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
# Write the bundle of a run to `zipfile` and return its path. `table` is the
|
| 73 |
+
# answers table of answers_table(). `rows` are the rows of Try on 5 for the
|
| 74 |
+
# code (NULL for all inputs).
|
| 75 |
+
write_bundle <- function(spec, table, zipfile, date = Sys.Date(), rows = NULL) {
|
| 76 |
name <- run_name(spec)
|
| 77 |
refs <- reference_files(spec)
|
| 78 |
dir <- tempfile("bundle")
|
| 79 |
dir.create(dir)
|
| 80 |
on.exit(unlink(dir, recursive = TRUE))
|
| 81 |
+
readr::write_file(paste0(run_code(spec, date, rows), "\n"), file.path(dir, paste0(name, ".R")))
|
| 82 |
readr::write_file(answers_csv(table), file.path(dir, paste0(name, "-answers.csv")))
|
| 83 |
# The setup holds the references as the prepared images; the key stays out
|
| 84 |
references <- if (nrow(refs) > 0) spec$references[unique(refs$answer)]
|
R/code.R
CHANGED
|
@@ -146,19 +146,33 @@ references_code <- function(refs) {
|
|
| 146 |
paste0("\nreferences <- list(\n", paste(by_answer$code, collapse = ",\n"), "\n)")
|
| 147 |
}
|
| 148 |
|
| 149 |
-
# The
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
if (spec$source == "injuries") {
|
| 152 |
-
return(paste(
|
| 153 |
"set.seed(2026)",
|
| 154 |
"# This line needs the package dplyr.",
|
| 155 |
"injuries <- ohio_injuries |>",
|
| 156 |
" dplyr::slice_sample(n = 20, by = collapsed_nature_title)",
|
| 157 |
sep = "\n"
|
| 158 |
-
))
|
| 159 |
}
|
| 160 |
if (is.null(spec$text_column)) {
|
| 161 |
-
return('texts <- read.csv("data.csv")$text')
|
| 162 |
}
|
| 163 |
# read.csv() changes odd column names unless check.names is FALSE
|
| 164 |
read <- if (make.names(spec$text_column) == spec$text_column) {
|
|
@@ -166,12 +180,15 @@ text_code <- function(spec) {
|
|
| 166 |
} else {
|
| 167 |
'read.csv("data.csv", check.names = FALSE)'
|
| 168 |
}
|
| 169 |
-
|
|
|
|
|
|
|
| 170 |
}
|
| 171 |
|
| 172 |
-
# The lines that define `images` and `references`
|
| 173 |
-
|
| 174 |
-
|
|
|
|
| 175 |
if (spec$source == "textiles") {
|
| 176 |
by_answer <- refs |>
|
| 177 |
dplyr::summarise(
|
|
@@ -181,13 +198,18 @@ inputs_code <- function(spec, refs) {
|
|
| 181 |
),
|
| 182 |
.by = answer
|
| 183 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
return(paste0(
|
| 185 |
'folder <- system.file("extdata", "textiles", package = "decisions")\n',
|
| 186 |
-
|
| 187 |
"references <- list(\n", paste(by_answer$code, collapse = ",\n"), "\n)"
|
| 188 |
))
|
| 189 |
}
|
| 190 |
-
inputs <- spec$inputs
|
| 191 |
images <- if (nrow(inputs) == 0) {
|
| 192 |
'images <- list.files("images", full.names = TRUE)'
|
| 193 |
} else {
|
|
@@ -220,8 +242,9 @@ question_code <- function(question, var) {
|
|
| 220 |
)
|
| 221 |
}
|
| 222 |
|
| 223 |
-
# The code of a run, as one string. `date` is the day of the run.
|
| 224 |
-
|
|
|
|
| 225 |
refs <- reference_files(spec)
|
| 226 |
var <- question_var(spec)
|
| 227 |
data_arg <- if (spec$mode == "text") {
|
|
@@ -232,10 +255,10 @@ run_code <- function(spec, date = Sys.Date()) {
|
|
| 232 |
if (nrow(refs) > 0) data_arg <- paste0(data_arg, ", references = references")
|
| 233 |
paste(
|
| 234 |
sprintf("# The run of the decisions Space on %s, in R. Run it with your own key.", format(date)),
|
| 235 |
-
|
| 236 |
"",
|
| 237 |
paths_comment(spec, refs),
|
| 238 |
-
inputs_code(spec, refs),
|
| 239 |
"",
|
| 240 |
question_code(spec$question, var),
|
| 241 |
"",
|
|
@@ -252,7 +275,12 @@ run_code <- function(spec, date = Sys.Date()) {
|
|
| 252 |
)
|
| 253 |
}
|
| 254 |
|
| 255 |
-
# The number of lines of
|
|
|
|
|
|
|
| 256 |
code_lines <- function(code) {
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
| 258 |
}
|
|
|
|
| 146 |
paste0("\nreferences <- list(\n", paste(by_answer$code, collapse = ",\n"), "\n)")
|
| 147 |
}
|
| 148 |
|
| 149 |
+
# The comment above the inputs of Try on 5
|
| 150 |
+
TRY_COMMENT <- "# The inputs of Try on 5 in the app"
|
| 151 |
+
|
| 152 |
+
# The lines that keep the rows of Try on 5: x[c(3, 17), ] for a data frame,
|
| 153 |
+
# x[c(3, 17)] for a vector. Empty with no rows.
|
| 154 |
+
keep_code <- function(var, rows, frame = FALSE) {
|
| 155 |
+
if (length(rows) == 0) return("")
|
| 156 |
+
sprintf(
|
| 157 |
+
"\n%s\n%s <- %s[c(%s)%s]",
|
| 158 |
+
TRY_COMMENT, var, var, paste(rows, collapse = ", "), if (frame) ", " else ""
|
| 159 |
+
)
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
# The lines that define `texts` (or `injuries`). With `rows` (Try on 5), one
|
| 163 |
+
# more line keeps those rows.
|
| 164 |
+
text_code <- function(spec, rows = NULL) {
|
| 165 |
if (spec$source == "injuries") {
|
| 166 |
+
return(paste0(paste(
|
| 167 |
"set.seed(2026)",
|
| 168 |
"# This line needs the package dplyr.",
|
| 169 |
"injuries <- ohio_injuries |>",
|
| 170 |
" dplyr::slice_sample(n = 20, by = collapsed_nature_title)",
|
| 171 |
sep = "\n"
|
| 172 |
+
), keep_code("injuries", rows, frame = TRUE)))
|
| 173 |
}
|
| 174 |
if (is.null(spec$text_column)) {
|
| 175 |
+
return(paste0('texts <- read.csv("data.csv")$text', keep_code("texts", rows)))
|
| 176 |
}
|
| 177 |
# read.csv() changes odd column names unless check.names is FALSE
|
| 178 |
read <- if (make.names(spec$text_column) == spec$text_column) {
|
|
|
|
| 180 |
} else {
|
| 181 |
'read.csv("data.csv", check.names = FALSE)'
|
| 182 |
}
|
| 183 |
+
# A CSV file keeps its own row numbers ("Row 3"): the app drops empty texts
|
| 184 |
+
file_rows <- as.integer(stringr::str_extract(spec$inputs$file[rows], "[0-9]+$"))
|
| 185 |
+
paste0("texts <- ", read, "[[", r_string(spec$text_column), "]]", keep_code("texts", file_rows))
|
| 186 |
}
|
| 187 |
|
| 188 |
+
# The lines that define `images` and `references`. With `rows` (Try on 5),
|
| 189 |
+
# the code names only the images of those rows.
|
| 190 |
+
inputs_code <- function(spec, refs, rows = NULL) {
|
| 191 |
+
if (spec$mode == "text") return(text_code(spec, rows))
|
| 192 |
if (spec$source == "textiles") {
|
| 193 |
by_answer <- refs |>
|
| 194 |
dplyr::summarise(
|
|
|
|
| 198 |
),
|
| 199 |
.by = answer
|
| 200 |
)
|
| 201 |
+
images <- if (is.null(rows)) {
|
| 202 |
+
'images <- file.path(folder, textiles$file[textiles$role == "input"])'
|
| 203 |
+
} else {
|
| 204 |
+
paste0(TRY_COMMENT, "\nimages <- file.path(folder, ", r_vector(spec$inputs$file[rows]), ")")
|
| 205 |
+
}
|
| 206 |
return(paste0(
|
| 207 |
'folder <- system.file("extdata", "textiles", package = "decisions")\n',
|
| 208 |
+
images, "\n",
|
| 209 |
"references <- list(\n", paste(by_answer$code, collapse = ",\n"), "\n)"
|
| 210 |
))
|
| 211 |
}
|
| 212 |
+
inputs <- if (is.null(rows)) spec$inputs else spec$inputs[rows, ]
|
| 213 |
images <- if (nrow(inputs) == 0) {
|
| 214 |
'images <- list.files("images", full.names = TRUE)'
|
| 215 |
} else {
|
|
|
|
| 242 |
)
|
| 243 |
}
|
| 244 |
|
| 245 |
+
# The code of a run, as one string. `date` is the day of the run. `rows` are
|
| 246 |
+
# the rows of Try on 5 (NULL for all inputs).
|
| 247 |
+
run_code <- function(spec, date = Sys.Date(), rows = NULL) {
|
| 248 |
refs <- reference_files(spec)
|
| 249 |
var <- question_var(spec)
|
| 250 |
data_arg <- if (spec$mode == "text") {
|
|
|
|
| 255 |
if (nrow(refs) > 0) data_arg <- paste0(data_arg, ", references = references")
|
| 256 |
paste(
|
| 257 |
sprintf("# The run of the decisions Space on %s, in R. Run it with your own key.", format(date)),
|
| 258 |
+
'library(decisions) # install once: pak::pak("fmegahed/decisions")',
|
| 259 |
"",
|
| 260 |
paths_comment(spec, refs),
|
| 261 |
+
inputs_code(spec, refs, rows),
|
| 262 |
"",
|
| 263 |
question_code(spec$question, var),
|
| 264 |
"",
|
|
|
|
| 275 |
)
|
| 276 |
}
|
| 277 |
|
| 278 |
+
# The number of lines of code, for the closed card: the top-level calls, so
|
| 279 |
+
# comments and blank lines do not count. Code that does not parse counts the
|
| 280 |
+
# lines that are not blank and not only a comment.
|
| 281 |
code_lines <- function(code) {
|
| 282 |
+
calls <- tryCatch(length(parse(text = code, keep.source = FALSE)), error = \(e) NULL)
|
| 283 |
+
if (!is.null(calls)) return(calls)
|
| 284 |
+
lines <- stringr::str_trim(stringr::str_split_1(code, "\n"))
|
| 285 |
+
sum(lines != "" & !startsWith(lines, "#"))
|
| 286 |
}
|
R/config.R
CHANGED
|
@@ -11,6 +11,9 @@ APP_DATE <- "24 October 2026"
|
|
| 11 |
# (tests/testthat/test-config.R enforces this).
|
| 12 |
MODEL <- "jev-1.14.0"
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
# Commit of the decisions package that the app uses
|
| 15 |
DECISIONS_SHA <- "b25fc9ffa7908e9a6a2d9833f080de1456c9d50a"
|
| 16 |
|
|
|
|
| 11 |
# (tests/testthat/test-config.R enforces this).
|
| 12 |
MODEL <- "jev-1.14.0"
|
| 13 |
|
| 14 |
+
# Day of the saved runs of the two samples (their answers record MODEL)
|
| 15 |
+
SAVED_RUN_DATE <- "2026-10-23"
|
| 16 |
+
|
| 17 |
# Commit of the decisions package that the app uses
|
| 18 |
DECISIONS_SHA <- "b25fc9ffa7908e9a6a2d9833f080de1456c9d50a"
|
| 19 |
|
R/help.R
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# Help: the text of the intro, the help lines, the notes, the
|
| 2 |
# About. All text is in 80% ASD-STE100: short sentences, active voice, no
|
| 3 |
# contractions. Numbers and versions come from app_config(), so the text
|
| 4 |
# cannot drift from the behaviour.
|
|
@@ -48,7 +48,7 @@ help_text <- function(key) {
|
|
| 48 |
cfg$max_references
|
| 49 |
),
|
| 50 |
references_score = "A score question takes no references. References attach only to Yes / no and Choice questions.",
|
| 51 |
-
answers = "The answers show here after a run. A sample shows its saved
|
| 52 |
code = "The code repeats this run: the question, the references, the model and one decide() call. Run it in your own R with your TypeSafe key in TYPESAFE_API_KEY. Install the package with pak::pak(\"fmegahed/decisions\"). For your own images, read the comment on paths at the top of the code.",
|
| 53 |
privacy = "Your images and texts go to TypeSafe (Jev). Do not upload confidential data.",
|
| 54 |
record = "A GitHub workflow opens this app twice a day, as a visitor does. Each visit adds a row to a public file. The visit also keeps the app awake. The table shows the last 60 visits.",
|
|
|
|
| 1 |
+
# Help: the text of the intro, the help lines, the notes, the Uptime tab and
|
| 2 |
# About. All text is in 80% ASD-STE100: short sentences, active voice, no
|
| 3 |
# contractions. Numbers and versions come from app_config(), so the text
|
| 4 |
# cannot drift from the behaviour.
|
|
|
|
| 48 |
cfg$max_references
|
| 49 |
),
|
| 50 |
references_score = "A score question takes no references. References attach only to Yes / no and Choice questions.",
|
| 51 |
+
answers = "The answers show here after a run. A run of an unchanged sample shows its saved run, at no cost.",
|
| 52 |
code = "The code repeats this run: the question, the references, the model and one decide() call. Run it in your own R with your TypeSafe key in TYPESAFE_API_KEY. Install the package with pak::pak(\"fmegahed/decisions\"). For your own images, read the comment on paths at the top of the code.",
|
| 53 |
privacy = "Your images and texts go to TypeSafe (Jev). Do not upload confidential data.",
|
| 54 |
record = "A GitHub workflow opens this app twice a day, as a visitor does. Each visit adds a row to a public file. The visit also keeps the app awake. The table shows the last 60 visits.",
|
R/runs.R
CHANGED
|
@@ -12,6 +12,17 @@ chunk_ids <- function(n, size = CHUNK_SIZE) {
|
|
| 12 |
split(seq_len(n), ceiling(seq_len(n) / size)) |> unname()
|
| 13 |
}
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Names of the probability columns of decide(), one for each answer name
|
| 16 |
pred_column <- function(answers) {
|
| 17 |
paste0(".pred_", janitor::make_clean_names(answers, case = "none"))
|
|
|
|
| 12 |
split(seq_len(n), ceiling(seq_len(n) / size)) |> unname()
|
| 13 |
}
|
| 14 |
|
| 15 |
+
# The rows of Try on 5, sorted: the same rows each time. With labels, the
|
| 16 |
+
# first row of each label in a shuffle comes first, then the other rows of the
|
| 17 |
+
# shuffle. The seed does not change the random state of the session.
|
| 18 |
+
try_rows <- function(inputs, n = 5, seed = 2026) {
|
| 19 |
+
if (nrow(inputs) <= n) return(seq_len(nrow(inputs)))
|
| 20 |
+
shuffled <- withr::with_seed(seed, sample(nrow(inputs)))
|
| 21 |
+
label <- inputs$label[shuffled]
|
| 22 |
+
first <- utils::head(shuffled[!is.na(label) & !duplicated(label)], n)
|
| 23 |
+
sort(c(first, setdiff(shuffled, first))[seq_len(n)])
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
# Names of the probability columns of decide(), one for each answer name
|
| 27 |
pred_column <- function(answers) {
|
| 28 |
paste0(".pred_", janitor::make_clean_names(answers, case = "none"))
|
R/samples.R
CHANGED
|
@@ -22,6 +22,9 @@ SAMPLE_FACTS_TEXT <- list(
|
|
| 22 |
)
|
| 23 |
)
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
# The questions, copied from the vignettes images.Rmd and choosing-a-model.Rmd
|
| 26 |
sample_question <- function(id) {
|
| 27 |
switch(id,
|
|
@@ -107,3 +110,18 @@ sample_data <- function(id) {
|
|
| 107 |
list(spec = sample_spec(id), answers = sample_answers(id))
|
| 108 |
SAMPLE_CACHE[[id]]
|
| 109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
)
|
| 23 |
)
|
| 24 |
|
| 25 |
+
# The sentence of the Help note when the answers on show are a saved run
|
| 26 |
+
SAVED_RUN_TEXT <- "The app reuses this run to save cost. Change the question or a description to ask Jev again."
|
| 27 |
+
|
| 28 |
# The questions, copied from the vignettes images.Rmd and choosing-a-model.Rmd
|
| 29 |
sample_question <- function(id) {
|
| 30 |
switch(id,
|
|
|
|
| 110 |
list(spec = sample_spec(id), answers = sample_answers(id))
|
| 111 |
SAMPLE_CACHE[[id]]
|
| 112 |
}
|
| 113 |
+
|
| 114 |
+
# Is the spec the setup of its sample? Then a run shows the saved run and makes
|
| 115 |
+
# no call. Edits of spaces only in the instructions and the descriptions do not
|
| 116 |
+
# count; every other part must be the same.
|
| 117 |
+
sample_unchanged <- function(spec) {
|
| 118 |
+
if (!isTRUE(spec$source %in% c("textiles", "injuries"))) return(FALSE)
|
| 119 |
+
sample <- sample_data(spec$source)$spec
|
| 120 |
+
setup <- \(s) {
|
| 121 |
+
list(
|
| 122 |
+
s$mode, s$source, s$inputs, s$references, s$question$type, names(s$question$answers),
|
| 123 |
+
stringr::str_squish(s$question$instructions), stringr::str_squish(unname(s$question$answers))
|
| 124 |
+
)
|
| 125 |
+
}
|
| 126 |
+
identical(setup(spec), setup(sample))
|
| 127 |
+
}
|
R/server.R
CHANGED
|
@@ -105,7 +105,8 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 105 |
image_upload <- shiny::reactiveVal(NULL)
|
| 106 |
csv_file <- shiny::reactiveVal(NULL)
|
| 107 |
text_kind <- shiny::reactiveVal("paste")
|
| 108 |
-
|
|
|
|
| 109 |
mean_tokens <- shiny::reactiveVal(NULL) # list(fingerprint, mean)
|
| 110 |
session_cost <- shiny::reactiveVal(0)
|
| 111 |
usage <- shiny::reactiveVal(deps$store$read())
|
|
@@ -172,14 +173,14 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 172 |
rows_version(rows_version() + 1)
|
| 173 |
}
|
| 174 |
|
| 175 |
-
# A sample opens with
|
|
|
|
| 176 |
load_sample <- function(id, send = c("mode", "source")) {
|
| 177 |
sample <- sample_data(id)
|
| 178 |
question(sample$spec$question)
|
| 179 |
refs(unname(sample$spec$references) %||% list())
|
| 180 |
data_mode(sample$spec$mode)
|
| 181 |
data_source(id)
|
| 182 |
-
results(list(answers = sample$answers, key = answers_key(sample$spec), saved = TRUE))
|
| 183 |
notice(character())
|
| 184 |
show_setup(send)
|
| 185 |
}
|
|
@@ -351,15 +352,18 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 351 |
m <- mean_tokens()
|
| 352 |
if (!is.null(m) && identical(m$fingerprint, spec_fingerprint(s))) m$mean
|
| 353 |
}
|
|
|
|
|
|
|
| 354 |
checks <- shiny::reactive({
|
| 355 |
s <- spec()
|
|
|
|
| 356 |
status <- spend_status(session_cost(), usage(), deps$now())
|
| 357 |
purrr::map(c(try5 = 5, run_all = Inf), \(k) {
|
| 358 |
n <- min(k, nrow(s$inputs))
|
| 359 |
-
cost <- estimate_cost(s, n, mean_for(s))
|
| 360 |
-
allowed <- run_allowed(cost, status)
|
| 361 |
# No inputs: the buttons stay off, and no limit is the reason
|
| 362 |
-
list(ok = allowed$ok && n > 0, reason = allowed$reason, n = n, cost = cost)
|
| 363 |
})
|
| 364 |
})
|
| 365 |
shiny::observe({
|
|
@@ -412,13 +416,25 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 412 |
start_run <- function(n) {
|
| 413 |
if (running()) return()
|
| 414 |
s <- spec()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
problems <- run_problems(s)
|
| 416 |
if (length(problems) > 0) return(notice(problems))
|
| 417 |
if (!is.null(stop_reason(deps))) return(notice(error_message(stop_reason(deps))))
|
| 418 |
# The latest totals of all sessions decide, not the copy of this session
|
| 419 |
latest <- deps$store$read()
|
| 420 |
usage(latest)
|
| 421 |
-
n <- min(n, nrow(s$inputs))
|
| 422 |
allowed <- run_allowed(
|
| 423 |
estimate_cost(s, n, mean_for(s)), spend_status(session_cost(), latest, deps$now())
|
| 424 |
)
|
|
@@ -426,12 +442,13 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 426 |
notice(character())
|
| 427 |
job$spec <- s
|
| 428 |
job$key <- key()
|
| 429 |
-
job$chunks <- chunk_ids(n)
|
|
|
|
| 430 |
job$done <- 0L
|
| 431 |
job$stop <- FALSE
|
| 432 |
job$answers <- NULL
|
| 433 |
# The run writes its answers only while the card holds the answers that it
|
| 434 |
-
# found or wrote
|
| 435 |
job$token <- (job$token %||% 0L) + 1L
|
| 436 |
results(utils::modifyList(results() %||% list(), list(token = job$token)))
|
| 437 |
running(TRUE)
|
|
@@ -454,7 +471,11 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 454 |
if (!is.null(done$usage)) usage(done$usage)
|
| 455 |
job$answers <- dplyr::bind_rows(job$answers, done$answers)
|
| 456 |
shown <- identical(results()$token, job$token)
|
| 457 |
-
if (shown)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
# Only calls with tokens count: a mean of 0 would make the next run look free
|
| 459 |
tokens <- job$answers$.input_tokens
|
| 460 |
tokens <- tokens[!is.na(tokens) & tokens > 0]
|
|
@@ -493,10 +514,12 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 493 |
stats::setNames(srcs, l$answers)
|
| 494 |
)
|
| 495 |
})
|
| 496 |
-
# A cost below one cent reads "< $0.01", not "$0.00": a run is never free
|
|
|
|
| 497 |
output$run_estimate <- shiny::renderText({
|
| 498 |
all <- checks()$run_all
|
| 499 |
if (all$n == 0) return("")
|
|
|
|
| 500 |
cost <- if (all$cost > 0 && all$cost < 0.005) "< $0.01" else sprintf("\u2248 $%.2f", all$cost)
|
| 501 |
paste(all$n, "\u00b7", cost)
|
| 502 |
})
|
|
@@ -522,7 +545,7 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 522 |
if (is.null(stats())) return(NULL)
|
| 523 |
r <- results()
|
| 524 |
note <- if (r$saved) {
|
| 525 |
-
|
| 526 |
} else if (running()) {
|
| 527 |
sprintf("%d of %d", nrow(r$answers), length(unlist(job$chunks)))
|
| 528 |
}
|
|
@@ -536,9 +559,11 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 536 |
)
|
| 537 |
}
|
| 538 |
})
|
|
|
|
| 539 |
output$help_answers <- shiny::renderText({
|
| 540 |
if (is.null(stats())) return(help_text("answers"))
|
| 541 |
-
|
|
|
|
| 542 |
})
|
| 543 |
output$gallery <- shiny::renderUI({
|
| 544 |
t <- table()
|
|
@@ -558,9 +583,12 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 558 |
# --- Outputs: R code and bundle ---------------------------------------------
|
| 559 |
run_date <- function() as.Date(deps$now())
|
| 560 |
zip_name <- function() sprintf("%s-%s.zip", run_name(spec()), format(run_date()))
|
| 561 |
-
code
|
|
|
|
|
|
|
|
|
|
| 562 |
output$code_summary <- shiny::renderText(
|
| 563 |
-
sprintf("R code of this run \u00b7 %d lines", code_lines(code()))
|
| 564 |
)
|
| 565 |
output$code <- shiny::renderText(code())
|
| 566 |
output$bundle_files <- shiny::renderUI(bundle_files_ui(bundle_names(spec()), zip_name()))
|
|
@@ -573,7 +601,7 @@ app_server <- function(input, output, session, deps = app_deps()) {
|
|
| 573 |
)
|
| 574 |
output$download_bundle <- shiny::downloadHandler(
|
| 575 |
filename = zip_name,
|
| 576 |
-
content = \(file) write_bundle(spec(), download_table(), file, run_date())
|
| 577 |
)
|
| 578 |
|
| 579 |
# --- Outputs: status ----------------------------------------------------------
|
|
|
|
| 105 |
image_upload <- shiny::reactiveVal(NULL)
|
| 106 |
csv_file <- shiny::reactiveVal(NULL)
|
| 107 |
text_kind <- shiny::reactiveVal("paste")
|
| 108 |
+
# list(answers, key, saved, token of the run, rows of Try on 5 or NULL for all)
|
| 109 |
+
results <- shiny::reactiveVal(NULL)
|
| 110 |
mean_tokens <- shiny::reactiveVal(NULL) # list(fingerprint, mean)
|
| 111 |
session_cost <- shiny::reactiveVal(0)
|
| 112 |
usage <- shiny::reactiveVal(deps$store$read())
|
|
|
|
| 173 |
rows_version(rows_version() + 1)
|
| 174 |
}
|
| 175 |
|
| 176 |
+
# A sample opens with no answers and makes no call. A run of the unchanged
|
| 177 |
+
# sample then shows its saved run (see start_run()).
|
| 178 |
load_sample <- function(id, send = c("mode", "source")) {
|
| 179 |
sample <- sample_data(id)
|
| 180 |
question(sample$spec$question)
|
| 181 |
refs(unname(sample$spec$references) %||% list())
|
| 182 |
data_mode(sample$spec$mode)
|
| 183 |
data_source(id)
|
|
|
|
| 184 |
notice(character())
|
| 185 |
show_setup(send)
|
| 186 |
}
|
|
|
|
| 352 |
m <- mean_tokens()
|
| 353 |
if (!is.null(m) && identical(m$fingerprint, spec_fingerprint(s))) m$mean
|
| 354 |
}
|
| 355 |
+
# A run of an unchanged sample shows its saved run: it costs nothing, so no
|
| 356 |
+
# limit stops it
|
| 357 |
checks <- shiny::reactive({
|
| 358 |
s <- spec()
|
| 359 |
+
saved <- sample_unchanged(s)
|
| 360 |
status <- spend_status(session_cost(), usage(), deps$now())
|
| 361 |
purrr::map(c(try5 = 5, run_all = Inf), \(k) {
|
| 362 |
n <- min(k, nrow(s$inputs))
|
| 363 |
+
cost <- if (saved) 0 else estimate_cost(s, n, mean_for(s))
|
| 364 |
+
allowed <- if (saved) list(ok = TRUE, reason = NA_character_) else run_allowed(cost, status)
|
| 365 |
# No inputs: the buttons stay off, and no limit is the reason
|
| 366 |
+
list(ok = allowed$ok && n > 0, reason = allowed$reason, n = n, cost = cost, saved = saved)
|
| 367 |
})
|
| 368 |
})
|
| 369 |
shiny::observe({
|
|
|
|
| 416 |
start_run <- function(n) {
|
| 417 |
if (running()) return()
|
| 418 |
s <- spec()
|
| 419 |
+
n <- min(n, nrow(s$inputs))
|
| 420 |
+
# Try on 5 takes the rows of try_rows(); Run all takes all rows (try_rows()
|
| 421 |
+
# gives all rows when n is the number of inputs). The R code repeats the
|
| 422 |
+
# rows of Try on 5 (`subset`).
|
| 423 |
+
rows <- try_rows(s$inputs, n)
|
| 424 |
+
subset <- if (n < nrow(s$inputs)) rows
|
| 425 |
+
# An unchanged sample shows the saved answers of these rows at once: no
|
| 426 |
+
# call, no cost and no write to the usage store
|
| 427 |
+
if (sample_unchanged(s)) {
|
| 428 |
+
notice(character())
|
| 429 |
+
saved <- sample_data(s$source)$answers[rows, ]
|
| 430 |
+
return(results(list(answers = saved, key = key(), saved = TRUE, rows = subset)))
|
| 431 |
+
}
|
| 432 |
problems <- run_problems(s)
|
| 433 |
if (length(problems) > 0) return(notice(problems))
|
| 434 |
if (!is.null(stop_reason(deps))) return(notice(error_message(stop_reason(deps))))
|
| 435 |
# The latest totals of all sessions decide, not the copy of this session
|
| 436 |
latest <- deps$store$read()
|
| 437 |
usage(latest)
|
|
|
|
| 438 |
allowed <- run_allowed(
|
| 439 |
estimate_cost(s, n, mean_for(s)), spend_status(session_cost(), latest, deps$now())
|
| 440 |
)
|
|
|
|
| 442 |
notice(character())
|
| 443 |
job$spec <- s
|
| 444 |
job$key <- key()
|
| 445 |
+
job$chunks <- purrr::map(chunk_ids(n), \(i) rows[i])
|
| 446 |
+
job$rows <- subset
|
| 447 |
job$done <- 0L
|
| 448 |
job$stop <- FALSE
|
| 449 |
job$answers <- NULL
|
| 450 |
# The run writes its answers only while the card holds the answers that it
|
| 451 |
+
# found or wrote
|
| 452 |
job$token <- (job$token %||% 0L) + 1L
|
| 453 |
results(utils::modifyList(results() %||% list(), list(token = job$token)))
|
| 454 |
running(TRUE)
|
|
|
|
| 471 |
if (!is.null(done$usage)) usage(done$usage)
|
| 472 |
job$answers <- dplyr::bind_rows(job$answers, done$answers)
|
| 473 |
shown <- identical(results()$token, job$token)
|
| 474 |
+
if (shown) {
|
| 475 |
+
results(list(
|
| 476 |
+
answers = job$answers, key = job$key, saved = FALSE, token = job$token, rows = job$rows
|
| 477 |
+
))
|
| 478 |
+
}
|
| 479 |
# Only calls with tokens count: a mean of 0 would make the next run look free
|
| 480 |
tokens <- job$answers$.input_tokens
|
| 481 |
tokens <- tokens[!is.na(tokens) & tokens > 0]
|
|
|
|
| 514 |
stats::setNames(srcs, l$answers)
|
| 515 |
)
|
| 516 |
})
|
| 517 |
+
# A cost below one cent reads "< $0.01", not "$0.00": a run is never free.
|
| 518 |
+
# Only the saved run of an unchanged sample costs nothing.
|
| 519 |
output$run_estimate <- shiny::renderText({
|
| 520 |
all <- checks()$run_all
|
| 521 |
if (all$n == 0) return("")
|
| 522 |
+
if (all$saved) return(paste(all$n, "\u00b7 saved run, no cost"))
|
| 523 |
cost <- if (all$cost > 0 && all$cost < 0.005) "< $0.01" else sprintf("\u2248 $%.2f", all$cost)
|
| 524 |
paste(all$n, "\u00b7", cost)
|
| 525 |
})
|
|
|
|
| 545 |
if (is.null(stats())) return(NULL)
|
| 546 |
r <- results()
|
| 547 |
note <- if (r$saved) {
|
| 548 |
+
sprintf("Saved run of %s, %s", SAVED_RUN_DATE, MODEL)
|
| 549 |
} else if (running()) {
|
| 550 |
sprintf("%d of %d", nrow(r$answers), length(unlist(job$chunks)))
|
| 551 |
}
|
|
|
|
| 559 |
)
|
| 560 |
}
|
| 561 |
})
|
| 562 |
+
# The facts of a sample describe its full saved run, not Try on 5
|
| 563 |
output$help_answers <- shiny::renderText({
|
| 564 |
if (is.null(stats())) return(help_text("answers"))
|
| 565 |
+
r <- results()
|
| 566 |
+
help_note(spec(), stats(), target(), saved = r$saved, full = is.null(r$rows))
|
| 567 |
})
|
| 568 |
output$gallery <- shiny::renderUI({
|
| 569 |
t <- table()
|
|
|
|
| 583 |
# --- Outputs: R code and bundle ---------------------------------------------
|
| 584 |
run_date <- function() as.Date(deps$now())
|
| 585 |
zip_name <- function() sprintf("%s-%s.zip", run_name(spec()), format(run_date()))
|
| 586 |
+
# The code repeats the run on show: after Try on 5, the same rows; with no
|
| 587 |
+
# answers on show, or after Run all, all inputs
|
| 588 |
+
code_rows <- shiny::reactive(if (!is.null(table())) results()$rows)
|
| 589 |
+
code <- shiny::reactive(run_code(spec(), run_date(), code_rows()))
|
| 590 |
output$code_summary <- shiny::renderText(
|
| 591 |
+
sprintf("R code of this run \u00b7 %d lines of code", code_lines(code()))
|
| 592 |
)
|
| 593 |
output$code <- shiny::renderText(code())
|
| 594 |
output$bundle_files <- shiny::renderUI(bundle_files_ui(bundle_names(spec()), zip_name()))
|
|
|
|
| 601 |
)
|
| 602 |
output$download_bundle <- shiny::downloadHandler(
|
| 603 |
filename = zip_name,
|
| 604 |
+
content = \(file) write_bundle(spec(), download_table(), file, run_date(), code_rows())
|
| 605 |
)
|
| 606 |
|
| 607 |
# --- Outputs: status ----------------------------------------------------------
|
R/status.R
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Uptime record. A GitHub workflow adds a row to status/status.csv twice a day.
|
| 2 |
# The app reads this file from GitHub and shows the days in service (chip) and
|
| 3 |
-
# the last checks (
|
| 4 |
|
| 5 |
# The columns of status.csv
|
| 6 |
STATUS_COLUMNS <- c("time_utc", "http_status", "seconds", "answered", "app_version")
|
|
|
|
| 1 |
# Uptime record. A GitHub workflow adds a row to status/status.csv twice a day.
|
| 2 |
# The app reads this file from GitHub and shows the days in service (chip) and
|
| 3 |
+
# the last checks (Uptime tab).
|
| 4 |
|
| 5 |
# The columns of status.csv
|
| 6 |
STATUS_COLUMNS <- c("time_utc", "http_status", "seconds", "answered", "app_version")
|
R/ui.R
CHANGED
|
@@ -115,7 +115,7 @@ top_bar <- function(video) {
|
|
| 115 |
shiny::div(
|
| 116 |
role = "tablist",
|
| 117 |
tab_button("build", "Build", TRUE), tab_button("about", "About"),
|
| 118 |
-
tab_button("record", "
|
| 119 |
)
|
| 120 |
),
|
| 121 |
shiny::div(
|
|
@@ -315,7 +315,7 @@ build_panel <- function() {
|
|
| 315 |
)
|
| 316 |
}
|
| 317 |
|
| 318 |
-
# --- About and
|
| 319 |
|
| 320 |
about_panel <- function() {
|
| 321 |
# The About text is Markdown; its links open in a new tab
|
|
@@ -330,9 +330,9 @@ record_panel <- function() {
|
|
| 330 |
class = "page card",
|
| 331 |
shiny::div(
|
| 332 |
class = "bd prose",
|
| 333 |
-
shiny::h2("
|
| 334 |
shiny::p(help_text("record")),
|
| 335 |
-
scroll_region("
|
| 336 |
shiny::p(
|
| 337 |
ext_link("Status file", LINKS$status), " \u00b7 ",
|
| 338 |
ext_link("Package record", LINKS$package_record), " \u00b7 ",
|
|
|
|
| 115 |
shiny::div(
|
| 116 |
role = "tablist",
|
| 117 |
tab_button("build", "Build", TRUE), tab_button("about", "About"),
|
| 118 |
+
tab_button("record", "Uptime")
|
| 119 |
)
|
| 120 |
),
|
| 121 |
shiny::div(
|
|
|
|
| 315 |
)
|
| 316 |
}
|
| 317 |
|
| 318 |
+
# --- About and Uptime tabs --------------------------------------------------
|
| 319 |
|
| 320 |
about_panel <- function() {
|
| 321 |
# The About text is Markdown; its links open in a new tab
|
|
|
|
| 330 |
class = "page card",
|
| 331 |
shiny::div(
|
| 332 |
class = "bd prose",
|
| 333 |
+
shiny::h2("Uptime"),
|
| 334 |
shiny::p(help_text("record")),
|
| 335 |
+
scroll_region("Uptime table", shiny::tableOutput("record_table")),
|
| 336 |
shiny::p(
|
| 337 |
ext_link("Status file", LINKS$status), " \u00b7 ",
|
| 338 |
ext_link("Package record", LINKS$package_record), " \u00b7 ",
|
README.md
CHANGED
|
@@ -47,7 +47,7 @@ docker run -p 7860:7860 -e HF_JEV_API=... decisions-space
|
|
| 47 |
|
| 48 |
Then open <http://localhost:7860>. Replace `...` with a TypeSafe key.
|
| 49 |
|
| 50 |
-
- `HF_JEV_API` is the TypeSafe key. Without it, the app opens, but a run shows "The demo is not available right now."
|
| 51 |
- `HF_USAGE_TOKEN` is a Hugging Face token that can write to the private dataset `fmegahed/decisions-space-usage`. The app keeps its daily totals there. Without it, the app cannot reach the dataset. It then keeps the totals in memory, writes a warning to the log, and loses the totals when the app restarts.
|
| 52 |
|
| 53 |
The app never writes a key to a file or to the log.
|
|
|
|
| 47 |
|
| 48 |
Then open <http://localhost:7860>. Replace `...` with a TypeSafe key.
|
| 49 |
|
| 50 |
+
- `HF_JEV_API` is the TypeSafe key. Without it, the app opens, but a run shows "The demo is not available right now." A run of an unchanged sample still shows its saved answers.
|
| 51 |
- `HF_USAGE_TOKEN` is a Hugging Face token that can write to the private dataset `fmegahed/decisions-space-usage`. The app keeps its daily totals there. Without it, the app cannot reach the dataset. It then keeps the totals in memory, writes a warning to the log, and loses the totals when the app restarts.
|
| 52 |
|
| 53 |
The app never writes a key to a file or to the log.
|
www/app.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
| 14 |
}
|
| 15 |
function byId(id) { return document.getElementById(id); }
|
| 16 |
|
| 17 |
-
// ---- Tabs: Build, About,
|
| 18 |
|
| 19 |
var TABS = ['build', 'about', 'record'];
|
| 20 |
|
|
|
|
| 14 |
}
|
| 15 |
function byId(id) { return document.getElementById(id); }
|
| 16 |
|
| 17 |
+
// ---- Tabs: Build, About, Uptime ----------------------------------------
|
| 18 |
|
| 19 |
var TABS = ['build', 'about', 'record'];
|
| 20 |
|
www/theme.css
CHANGED
|
@@ -231,6 +231,8 @@ body.is-running .run-row { display: none; }
|
|
| 231 |
.table-wrap { max-width: 100%; overflow-x: auto; }
|
| 232 |
.table-wrap table { margin: 0; font-size: 13px; }
|
| 233 |
.table-wrap th { font-family: var(--mono); font-size: 11.5px; font-weight: 600; white-space: nowrap; }
|
|
|
|
|
|
|
| 234 |
|
| 235 |
/* R code card: the summary line and the buttons share the first row */
|
| 236 |
.code-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
|
|
@@ -258,7 +260,7 @@ pre.code { margin: 0 0 12px; padding: 10px 12px; background: var(--code-bg); bor
|
|
| 258 |
.file .n { font-family: var(--mono); font-size: 12px; font-weight: 600; word-break: break-all; }
|
| 259 |
.file .d { margin-top: 3px; font-size: 12.5px; color: var(--muted); }
|
| 260 |
|
| 261 |
-
/* --- About and
|
| 262 |
.prose h2 { margin: 18px 0 6px; font-family: var(--serif); font-weight: 700; font-size: 18px; }
|
| 263 |
.prose h2:first-child { margin-top: 4px; }
|
| 264 |
.prose p, .prose li { max-width: 72ch; }
|
|
|
|
| 231 |
.table-wrap { max-width: 100%; overflow-x: auto; }
|
| 232 |
.table-wrap table { margin: 0; font-size: 13px; }
|
| 233 |
.table-wrap th { font-family: var(--mono); font-size: 11.5px; font-weight: 600; white-space: nowrap; }
|
| 234 |
+
/* The full text of an input wraps in a column that is wide enough to read */
|
| 235 |
+
#table td:first-child { min-width: 16em; }
|
| 236 |
|
| 237 |
/* R code card: the summary line and the buttons share the first row */
|
| 238 |
.code-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
|
|
|
|
| 260 |
.file .n { font-family: var(--mono); font-size: 12px; font-weight: 600; word-break: break-all; }
|
| 261 |
.file .d { margin-top: 3px; font-size: 12.5px; color: var(--muted); }
|
| 262 |
|
| 263 |
+
/* --- About and Uptime ----------------------------------------------------------- */
|
| 264 |
.prose h2 { margin: 18px 0 6px; font-family: var(--serif); font-weight: 700; font-size: 18px; }
|
| 265 |
.prose h2:first-child { margin-top: 4px; }
|
| 266 |
.prose p, .prose li { max-width: 72ch; }
|