Say in the app that it has been updated since the paper
Browse filesThe header links to a 'What changed' note built from the configuration: this
version and date, the version and model the paper describes, and why the two
differ. The README gains a version table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- R/config.R +15 -1
- R/help.R +22 -1
- README.md +14 -1
- app.R +2 -2
- tests/testthat/test-config-help.R +16 -3
- www/miami-theme.css +5 -0
R/config.R
CHANGED
|
@@ -13,6 +13,16 @@ get_extraction_model = function() {
|
|
| 13 |
if (nchar(model) == 0) DEFAULT_EXTRACTION_MODEL else model
|
| 14 |
}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Name of the provider behind ellmer::chat_openai(), used in UI text
|
| 17 |
MODEL_PROVIDER = "OpenAI"
|
| 18 |
|
|
@@ -50,6 +60,10 @@ app_config = function() {
|
|
| 50 |
max_pages_or_images = MAX_PAGES_OR_IMAGES,
|
| 51 |
max_upload_mb = MAX_UPLOAD_MB,
|
| 52 |
max_fields = MAX_FIELDS,
|
| 53 |
-
list_separator = LIST_SEPARATOR
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
)
|
| 55 |
}
|
|
|
|
| 13 |
if (nchar(model) == 0) DEFAULT_EXTRACTION_MODEL else model
|
| 14 |
}
|
| 15 |
|
| 16 |
+
# Version of this app, shown in the header and in the "What changed" help
|
| 17 |
+
APP_VERSION_NUMBER = "3.0.0"
|
| 18 |
+
APP_VERSION_DATE = "9 October 2026"
|
| 19 |
+
|
| 20 |
+
# The version that the paper describes. The app has been updated since the
|
| 21 |
+
# paper was drafted, and the "What changed" help says so. PAPER_MODEL is a
|
| 22 |
+
# record of the past, not a setting: nothing is sent to it.
|
| 23 |
+
PAPER_VERSION_NUMBER = "2.0.1"
|
| 24 |
+
PAPER_MODEL = "gpt-5-mini"
|
| 25 |
+
|
| 26 |
# Name of the provider behind ellmer::chat_openai(), used in UI text
|
| 27 |
MODEL_PROVIDER = "OpenAI"
|
| 28 |
|
|
|
|
| 60 |
max_pages_or_images = MAX_PAGES_OR_IMAGES,
|
| 61 |
max_upload_mb = MAX_UPLOAD_MB,
|
| 62 |
max_fields = MAX_FIELDS,
|
| 63 |
+
list_separator = LIST_SEPARATOR,
|
| 64 |
+
version = APP_VERSION_NUMBER,
|
| 65 |
+
version_date = APP_VERSION_DATE,
|
| 66 |
+
paper_version = PAPER_VERSION_NUMBER,
|
| 67 |
+
paper_model = PAPER_MODEL
|
| 68 |
)
|
| 69 |
}
|
R/help.R
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Help text for the "?" buttons. Every number and name in the text comes from
|
| 2 |
# app_config(), the same values that drive the app's behaviour.
|
| 3 |
|
| 4 |
-
HELP_TOPICS = c("source", "fields", "extract", "results")
|
| 5 |
|
| 6 |
# Returns list(title, paragraphs) for one help topic
|
| 7 |
help_content = function(topic, cfg = app_config()) {
|
|
@@ -73,6 +73,21 @@ help_content = function(topic, cfg = app_config()) {
|
|
| 73 |
"Download as CSV saves the table with the source_id, status and status_message columns. Reproduce in code shows R and Python code that runs the same extraction, with your fields and the same model, on your own data."
|
| 74 |
)
|
| 75 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
stop("Unknown help topic: ", topic)
|
| 77 |
)
|
| 78 |
}
|
|
@@ -90,6 +105,12 @@ help_button = function(topic) {
|
|
| 90 |
)
|
| 91 |
}
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
# Modal dialog with the help for a topic
|
| 94 |
help_modal = function(topic, cfg = app_config()) {
|
| 95 |
content = help_content(topic, cfg)
|
|
|
|
| 1 |
# Help text for the "?" buttons. Every number and name in the text comes from
|
| 2 |
# app_config(), the same values that drive the app's behaviour.
|
| 3 |
|
| 4 |
+
HELP_TOPICS = c("source", "fields", "extract", "results", "changes")
|
| 5 |
|
| 6 |
# Returns list(title, paragraphs) for one help topic
|
| 7 |
help_content = function(topic, cfg = app_config()) {
|
|
|
|
| 73 |
"Download as CSV saves the table with the source_id, status and status_message columns. Reproduce in code shows R and Python code that runs the same extraction, with your fields and the same model, on your own data."
|
| 74 |
)
|
| 75 |
),
|
| 76 |
+
changes = list(
|
| 77 |
+
title = "What changed since the paper",
|
| 78 |
+
paragraphs = c(
|
| 79 |
+
sprintf(
|
| 80 |
+
"This is version %s of the app, released on %s. The paper describes version %s, which used the %s model %s.",
|
| 81 |
+
cfg$version, cfg$version_date, cfg$paper_version, cfg$provider, cfg$paper_model
|
| 82 |
+
),
|
| 83 |
+
sprintf(
|
| 84 |
+
"The model is now %s. If the screen or the model you see here differs from the paper, that is why.",
|
| 85 |
+
cfg$model
|
| 86 |
+
),
|
| 87 |
+
"Other changes: ready-made field templates for common quality documents; field definitions that can be saved and loaded; a progress bar, with one failed item no longer discarding the rest; R and Python code that reproduces an extraction; and a '?' beside each step that explains how it works.",
|
| 88 |
+
"The difference is intentional. Language models are replaced often, and users find problems that only appear in use. An app built on a language model needs regular updating, and a paper can only describe it at one point in time."
|
| 89 |
+
)
|
| 90 |
+
),
|
| 91 |
stop("Unknown help topic: ", topic)
|
| 92 |
)
|
| 93 |
}
|
|
|
|
| 105 |
)
|
| 106 |
}
|
| 107 |
|
| 108 |
+
# Link in the header that opens the "What changed" help
|
| 109 |
+
changes_link = function() {
|
| 110 |
+
shiny::actionLink("help_changes", "Updated since the paper. What changed?",
|
| 111 |
+
class = "changes-link", `aria-haspopup` = "dialog")
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
# Modal dialog with the help for a topic
|
| 115 |
help_modal = function(topic, cfg = app_config()) {
|
| 116 |
content = help_content(topic, cfg)
|
README.md
CHANGED
|
@@ -74,7 +74,20 @@ one item per row of its first column, and its first row is treated as a header.
|
|
| 74 |
The model id is set in one place, `DEFAULT_EXTRACTION_MODEL` in `R/config.R`. To use another
|
| 75 |
OpenAI model without editing code, set the environment variable `EXTRACTION_MODEL`. The
|
| 76 |
call, the help text and the generated code all read this value. A test fails if a model id
|
| 77 |
-
is written anywhere else in the app code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
## Run it yourself
|
| 80 |
|
|
|
|
| 74 |
The model id is set in one place, `DEFAULT_EXTRACTION_MODEL` in `R/config.R`. To use another
|
| 75 |
OpenAI model without editing code, set the environment variable `EXTRACTION_MODEL`. The
|
| 76 |
call, the help text and the generated code all read this value. A test fails if a model id
|
| 77 |
+
is written anywhere else in the app code, apart from the record of the model that the
|
| 78 |
+
paper's version used.
|
| 79 |
+
|
| 80 |
+
## Versions
|
| 81 |
+
|
| 82 |
+
| Version | Date | Model | Notes |
|
| 83 |
+
|---|---|---|---|
|
| 84 |
+
| 3.0.0 | 9 October 2026 | `gpt-6-luna` | Released after the paper was drafted. Field templates, saved field definitions, per-item progress and failure handling, code that reproduces an extraction, and in-app help. |
|
| 85 |
+
| 2.0.1 | 2026 | `gpt-5-mini` | The version the paper describes. |
|
| 86 |
+
|
| 87 |
+
The app has been updated since the paper was drafted, and this is intentional. Language
|
| 88 |
+
models are replaced often, and users find problems that only appear in use, so an app built
|
| 89 |
+
on a language model needs regular updating. The header of the app links to a short "What
|
| 90 |
+
changed" note that is generated from the values in `R/config.R`.
|
| 91 |
|
| 92 |
## Run it yourself
|
| 93 |
|
app.R
CHANGED
|
@@ -17,7 +17,7 @@ options(shiny.maxRequestSize = MAX_UPLOAD_MB * 1024^2)
|
|
| 17 |
# Model used for every call and named in every piece of UI text
|
| 18 |
EXTRACTION_MODEL = get_extraction_model()
|
| 19 |
|
| 20 |
-
APP_VERSION = "Version
|
| 21 |
|
| 22 |
PAPER_TITLE = "What Quality Engineers Need to Know About Generative AI: Structured Data Extraction"
|
| 23 |
|
|
@@ -83,7 +83,7 @@ ui = shiny::fluidPage(
|
|
| 83 |
),
|
| 84 |
shiny::div(
|
| 85 |
class = "header-right",
|
| 86 |
-
shiny::p(APP_VERSION),
|
| 87 |
shiny::p(
|
| 88 |
shiny::tags$strong("Authors: "),
|
| 89 |
"Fadel M. Megahed, Ying-Ju Chen, Yamin Dahwich, Arthur Carvalho, L. Allison Jones-Farmer, Ibrahim Yousif, and Inez M. Zwetsloot"
|
|
|
|
| 17 |
# Model used for every call and named in every piece of UI text
|
| 18 |
EXTRACTION_MODEL = get_extraction_model()
|
| 19 |
|
| 20 |
+
APP_VERSION = paste0("Version ", APP_VERSION_NUMBER, " | ", APP_VERSION_DATE)
|
| 21 |
|
| 22 |
PAPER_TITLE = "What Quality Engineers Need to Know About Generative AI: Structured Data Extraction"
|
| 23 |
|
|
|
|
| 83 |
),
|
| 84 |
shiny::div(
|
| 85 |
class = "header-right",
|
| 86 |
+
shiny::p(APP_VERSION, " | ", changes_link()),
|
| 87 |
shiny::p(
|
| 88 |
shiny::tags$strong("Authors: "),
|
| 89 |
"Fadel M. Megahed, Ying-Ju Chen, Yamin Dahwich, Arthur Carvalho, L. Allison Jones-Farmer, Ibrahim Yousif, and Inez M. Zwetsloot"
|
tests/testthat/test-config-help.R
CHANGED
|
@@ -48,11 +48,12 @@ test_that("no model id is written outside R/config.R", {
|
|
| 48 |
)
|
| 49 |
}
|
| 50 |
|
| 51 |
-
# In the config file, the only model
|
|
|
|
| 52 |
config_lines = readLines(config_file, warn = FALSE)
|
| 53 |
hits = grep(model_pattern, config_lines, ignore.case = TRUE)
|
| 54 |
-
expect_length(hits,
|
| 55 |
-
expect_match(config_lines[hits], "^DEFAULT_EXTRACTION_MODEL = ")
|
| 56 |
})
|
| 57 |
|
| 58 |
test_that("help text is generated from the configuration values", {
|
|
@@ -88,6 +89,18 @@ test_that("help text follows the configured model at run time", {
|
|
| 88 |
expect_match(paste(help_content("source")$paragraphs, collapse = " "), "model-from-env")
|
| 89 |
})
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
test_that("every help topic has content, a button and a dialog, without em dashes", {
|
| 92 |
for (topic in HELP_TOPICS) {
|
| 93 |
content = help_content(topic)
|
|
|
|
| 48 |
)
|
| 49 |
}
|
| 50 |
|
| 51 |
+
# In the config file, the only model ids are the default and the record of
|
| 52 |
+
# the model that the paper's version used
|
| 53 |
config_lines = readLines(config_file, warn = FALSE)
|
| 54 |
hits = grep(model_pattern, config_lines, ignore.case = TRUE)
|
| 55 |
+
expect_length(hits, 2)
|
| 56 |
+
expect_match(config_lines[hits], "^(DEFAULT_EXTRACTION_MODEL|PAPER_MODEL) = ")
|
| 57 |
})
|
| 58 |
|
| 59 |
test_that("help text is generated from the configuration values", {
|
|
|
|
| 89 |
expect_match(paste(help_content("source")$paragraphs, collapse = " "), "model-from-env")
|
| 90 |
})
|
| 91 |
|
| 92 |
+
test_that("the header says the app was updated since the paper, from the configuration", {
|
| 93 |
+
text = paste(help_content("changes")$paragraphs, collapse = " ")
|
| 94 |
+
for (value in c(APP_VERSION_NUMBER, APP_VERSION_DATE, PAPER_VERSION_NUMBER, PAPER_MODEL, get_extraction_model())) {
|
| 95 |
+
expect_match(text, value, fixed = TRUE)
|
| 96 |
+
}
|
| 97 |
+
withr::local_envvar(EXTRACTION_MODEL = "model-from-env")
|
| 98 |
+
expect_match(paste(help_content("changes")$paragraphs, collapse = " "), "model-from-env")
|
| 99 |
+
link = as.character(changes_link())
|
| 100 |
+
expect_match(link, "id=\"help_changes\"", fixed = TRUE)
|
| 101 |
+
expect_match(link, "What changed?", fixed = TRUE)
|
| 102 |
+
})
|
| 103 |
+
|
| 104 |
test_that("every help topic has content, a button and a dialog, without em dashes", {
|
| 105 |
for (topic in HELP_TOPICS) {
|
| 106 |
content = help_content(topic)
|
www/miami-theme.css
CHANGED
|
@@ -65,6 +65,11 @@ body {
|
|
| 65 |
text-align: right;
|
| 66 |
}
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
.header-right p {
|
| 69 |
margin: 3px 0;
|
| 70 |
font-size: 0.85em;
|
|
|
|
| 65 |
text-align: right;
|
| 66 |
}
|
| 67 |
|
| 68 |
+
.header-right .changes-link {
|
| 69 |
+
color: #EFDB72;
|
| 70 |
+
text-decoration: underline;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
.header-right p {
|
| 74 |
margin: 3px 0;
|
| 75 |
font-size: 0.85em;
|