fmegahed commited on
Commit
2701847
·
verified ·
1 Parent(s): 1661b8d

Deploy 7977791 from GitHub

Browse files
Files changed (6) hide show
  1. R/config.R +6 -4
  2. R/help.R +20 -1
  3. R/ui.R +27 -13
  4. README.md +3 -1
  5. www/app.js +12 -8
  6. www/theme.css +23 -3
R/config.R CHANGED
@@ -4,8 +4,8 @@
4
  # behaviour.
5
 
6
  # Version of this app
7
- APP_VERSION <- "1.0.0"
8
- APP_DATE <- "24 October 2026"
9
 
10
  # Model used for every call. This is the ONLY place a model id may be written
11
  # (tests/testthat/test-config.R enforces this).
@@ -60,9 +60,11 @@ USAGE_TOKEN_VAR <- "HF_USAGE_TOKEN"
60
  USAGE_REPO <- "fmegahed/decisions-space-usage"
61
  STATUS_URL <- "https://raw.githubusercontent.com/fmegahed/decisions-space/main/status/status.csv"
62
 
63
- # Address of the Space, and the video on YouTube ("" hides the Video button)
 
64
  SPACE_HOST <- "https://fmegahed-decisions.hf.space"
65
  VIDEO_URL <- "https://youtu.be/4WE-ykXzK1g"
 
66
 
67
  # The values that the help text uses, in one named list
68
  app_config <- function() {
@@ -77,6 +79,6 @@ app_config <- function() {
77
  max_description = MAX_DESCRIPTION, answer_range = ANSWER_RANGE,
78
  max_references = MAX_REFERENCES, chunk_size = CHUNK_SIZE,
79
  image_settings = IMAGE_SETTINGS, space_host = SPACE_HOST,
80
- video_url = VIDEO_URL
81
  )
82
  }
 
4
  # behaviour.
5
 
6
  # Version of this app
7
+ APP_VERSION <- "1.0.1"
8
+ APP_DATE <- "30 October 2026"
9
 
10
  # Model used for every call. This is the ONLY place a model id may be written
11
  # (tests/testthat/test-config.R enforces this).
 
60
  USAGE_REPO <- "fmegahed/decisions-space-usage"
61
  STATUS_URL <- "https://raw.githubusercontent.com/fmegahed/decisions-space/main/status/status.csv"
62
 
63
+ # Address of the Space, and the help video on YouTube ("" hides its buttons)
64
+ # with its length in whole minutes (the video runs 4:59)
65
  SPACE_HOST <- "https://fmegahed-decisions.hf.space"
66
  VIDEO_URL <- "https://youtu.be/4WE-ykXzK1g"
67
+ VIDEO_MINUTES <- 5
68
 
69
  # The values that the help text uses, in one named list
70
  app_config <- function() {
 
79
  max_description = MAX_DESCRIPTION, answer_range = ANSWER_RANGE,
80
  max_references = MAX_REFERENCES, chunk_size = CHUNK_SIZE,
81
  image_settings = IMAGE_SETTINGS, space_host = SPACE_HOST,
82
+ video_url = video_url(), video_minutes = VIDEO_MINUTES
83
  )
84
  }
R/help.R CHANGED
@@ -56,7 +56,10 @@ help_text <- function(key) {
56
  )
57
  }
58
 
59
- # The text of the About tab, as Markdown
 
 
 
60
  about_text <- function(cfg = app_config()) {
61
  usd <- \(x) paste("USD", format(x, big.mark = ","))
62
  num <- \(x) format(x, big.mark = ",")
@@ -65,6 +68,19 @@ about_text <- function(cfg = app_config()) {
65
  "",
66
  "This app asks Jev, a decision model of TypeSafe, a question about each image or text in a set. For each input, it shows the answer and the probability of each answer. It also gives the R code of the run and a bundle with the setup and the answers. You need no key and no account.",
67
  "",
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  "## Package",
69
  "",
70
  sprintf(
@@ -115,6 +131,9 @@ about_text <- function(cfg = app_config()) {
115
  cfg$version, cfg$date, decisions_version(), cfg$model
116
  ),
117
  "",
 
 
 
118
  "## Cite",
119
  "",
120
  sprintf(
 
56
  )
57
  }
58
 
59
+ # The text of the About tab, as Markdown. HELP_VIDEO_MARK marks the place of
60
+ # the button that opens the help video (about_panel() puts it there).
61
+ HELP_VIDEO_MARK <- "<!-- help-video-button -->"
62
+
63
  about_text <- function(cfg = app_config()) {
64
  usd <- \(x) paste("USD", format(x, big.mark = ","))
65
  num <- \(x) format(x, big.mark = ",")
 
68
  "",
69
  "This app asks Jev, a decision model of TypeSafe, a question about each image or text in a set. For each input, it shows the answer and the probability of each answer. It also gives the R code of the run and a bundle with the setup and the answers. You need no key and no account.",
70
  "",
71
+ if (nzchar(cfg$video_url)) {
72
+ c(
73
+ "## Help video",
74
+ "",
75
+ sprintf(
76
+ "A %d-minute video shows how to use the app: a sample of images, your own images, a text sample and your own texts. It explains the three question types (Yes / no, Choice and Score), the answers and the R code. Watch it here, or [on YouTube](%s) with chapters.",
77
+ cfg$video_minutes, cfg$video_url
78
+ ),
79
+ "",
80
+ HELP_VIDEO_MARK,
81
+ ""
82
+ )
83
+ },
84
  "## Package",
85
  "",
86
  sprintf(
 
131
  cfg$version, cfg$date, decisions_version(), cfg$model
132
  ),
133
  "",
134
+ "- 1.0.1: the help video (in the intro, the top bar and this page); the buttons of the top bar on one line.",
135
+ "- 1.0.0: the first public version.",
136
+ "",
137
  "## Cite",
138
  "",
139
  sprintf(
R/ui.R CHANGED
@@ -125,17 +125,18 @@ top_bar <- function(video) {
125
  class = "chip", shiny::span(class = "dot", `aria-hidden` = "true", "\u25cf"), " ",
126
  shiny::textOutput("status_chip", inline = TRUE)
127
  ),
128
- if (nzchar(video)) {
129
- shiny::tags$button(
130
- id = "open_video", type = "button", class = "btn btn-quiet",
131
- `aria-haspopup` = "dialog", "Video"
132
- )
133
- },
134
  shiny::div(
135
  class = "import-btn",
136
  upload_input("import_file", NULL, c(".zip", ".json"), button = "Import")
137
  ),
138
- samples_menu()
 
 
 
 
 
 
 
139
  )
140
  )
141
  }
@@ -205,7 +206,17 @@ sidebar <- function() {
205
 
206
  # --- Build tab: cards -------------------------------------------------------
207
 
208
- intro_card <- function() {
 
 
 
 
 
 
 
 
 
 
209
  step <- \(n, name, text) {
210
  shiny::tags$li(
211
  shiny::span(class = "num", `aria-hidden` = "true", n),
@@ -216,7 +227,8 @@ intro_card <- function() {
216
  id = "intro", class = "card intro", `aria-label` = "Intro",
217
  shiny::div(
218
  shiny::p(class = "intro-lead", "Ask a model a question about each image or text."),
219
- shiny::p(class = "intro-help", help_text("intro"), " ", shiny::actionLink("intro_hide", "Hide"))
 
220
  ),
221
  shiny::tags$ol(
222
  class = "steps",
@@ -318,10 +330,12 @@ build_panel <- function() {
318
  # --- About and Uptime tabs --------------------------------------------------
319
 
320
  about_panel <- function() {
321
- # The About text is Markdown; its links open in a new tab
 
322
  html <- shiny::markdown(help_text("about")) |>
323
  as.character() |>
324
- stringr::str_replace_all("<a href=", '<a target="_blank" rel="noopener" href=')
 
325
  shiny::div(class = "page card", shiny::div(class = "bd prose", htmltools::HTML(html)))
326
  }
327
 
@@ -371,11 +385,11 @@ video_dialog <- function(url) {
371
  id = "video_dialog", class = "video-dialog", `aria-labelledby` = "video_title",
372
  shiny::div(
373
  class = "video-head",
374
- shiny::h2(id = "video_title", "Video"),
375
  shiny::tags$button(id = "close_video", type = "button", class = "btn btn-quiet sm", "Close")
376
  ),
377
  shiny::tags$iframe(
378
- id = "video_frame", class = "video-frame", title = "Video of the decisions app",
379
  `data-src` = video_embed(url), allow = "autoplay; encrypted-media; picture-in-picture; fullscreen",
380
  referrerpolicy = "strict-origin-when-cross-origin"
381
  )
 
125
  class = "chip", shiny::span(class = "dot", `aria-hidden` = "true", "\u25cf"), " ",
126
  shiny::textOutput("status_chip", inline = TRUE)
127
  ),
 
 
 
 
 
 
128
  shiny::div(
129
  class = "import-btn",
130
  upload_input("import_file", NULL, c(".zip", ".json"), button = "Import")
131
  ),
132
+ samples_menu(),
133
+ # The quiet way to the help video: on every tab, and after Hide
134
+ if (nzchar(video)) {
135
+ shiny::tags$button(
136
+ id = "open_video_bar", type = "button", class = "btn btn-quiet help-video-bar open-video",
137
+ `aria-haspopup` = "dialog", shiny::span(class = "tri", `aria-hidden` = "true"), "Help video"
138
+ )
139
+ }
140
  )
141
  )
142
  }
 
206
 
207
  # --- Build tab: cards -------------------------------------------------------
208
 
209
+ # The yellow button that opens the help video (in the intro and in About)
210
+ help_video_button <- function(id) {
211
+ shiny::tags$button(
212
+ id = id, type = "button", class = "btn-help-video open-video", `aria-haspopup` = "dialog",
213
+ shiny::span(class = "play", `aria-hidden` = "true"),
214
+ sprintf("Watch our %d-minute intro video on how to use this app", VIDEO_MINUTES)
215
+ )
216
+ }
217
+
218
+ # The intro, with the help video as the first step of a first visit
219
+ intro_card <- function(video = video_url()) {
220
  step <- \(n, name, text) {
221
  shiny::tags$li(
222
  shiny::span(class = "num", `aria-hidden` = "true", n),
 
227
  id = "intro", class = "card intro", `aria-label` = "Intro",
228
  shiny::div(
229
  shiny::p(class = "intro-lead", "Ask a model a question about each image or text."),
230
+ shiny::p(class = "intro-help", help_text("intro"), " ", shiny::actionLink("intro_hide", "Hide")),
231
+ if (nzchar(video)) shiny::div(class = "help-video-row", help_video_button("open_video"))
232
  ),
233
  shiny::tags$ol(
234
  class = "steps",
 
330
  # --- About and Uptime tabs --------------------------------------------------
331
 
332
  about_panel <- function() {
333
+ # The About text is Markdown; its links open in a new tab. The button of
334
+ # the help video goes at its mark.
335
  html <- shiny::markdown(help_text("about")) |>
336
  as.character() |>
337
+ stringr::str_replace_all("<a href=", '<a target="_blank" rel="noopener" href=') |>
338
+ stringr::str_replace(stringr::fixed(HELP_VIDEO_MARK), as.character(help_video_button("open_video_about")))
339
  shiny::div(class = "page card", shiny::div(class = "bd prose", htmltools::HTML(html)))
340
  }
341
 
 
385
  id = "video_dialog", class = "video-dialog", `aria-labelledby` = "video_title",
386
  shiny::div(
387
  class = "video-head",
388
+ shiny::h2(id = "video_title", "Help video"),
389
  shiny::tags$button(id = "close_video", type = "button", class = "btn btn-quiet sm", "Close")
390
  ),
391
  shiny::tags$iframe(
392
+ id = "video_frame", class = "video-frame", title = "Help video: how to use the decisions app",
393
  `data-src` = video_embed(url), allow = "autoplay; encrypted-media; picture-in-picture; fullscreen",
394
  referrerpolicy = "strict-origin-when-cross-origin"
395
  )
README.md CHANGED
@@ -20,6 +20,8 @@ The app uses the R package [decisions](https://github.com/fmegahed/decisions) (v
20
 
21
  Open the app: <https://huggingface.co/spaces/fmegahed/decisions>
22
 
 
 
23
  ## Privacy
24
 
25
  Your images, texts and references go to TypeSafe (Jev). Do not upload confidential data. The app keeps no input after your session. It stores only the daily totals of calls, tokens and cost. The bundle and the logs hold no key.
@@ -87,4 +89,4 @@ Fadel M. Megahed (Miami University) and Ying-Ju (Tessa) Chen (University of Dayt
87
  If you use the package or the app, cite both:
88
 
89
  - Megahed, F. M., & Chen, Y.-J. (2026). *decisions: Typed decisions from decision models* (R package version 0.1.0). https://github.com/fmegahed/decisions
90
- - Megahed, F. M., & Chen, Y.-J. (2026). *decisions Space* (Version 1.0.0) [Web application]. Hugging Face. https://huggingface.co/spaces/fmegahed/decisions
 
20
 
21
  Open the app: <https://huggingface.co/spaces/fmegahed/decisions>
22
 
23
+ Help video (5 minutes, with chapters): <https://youtu.be/4WE-ykXzK1g>
24
+
25
  ## Privacy
26
 
27
  Your images, texts and references go to TypeSafe (Jev). Do not upload confidential data. The app keeps no input after your session. It stores only the daily totals of calls, tokens and cost. The bundle and the logs hold no key.
 
89
  If you use the package or the app, cite both:
90
 
91
  - Megahed, F. M., & Chen, Y.-J. (2026). *decisions: Typed decisions from decision models* (R package version 0.1.0). https://github.com/fmegahed/decisions
92
+ - Megahed, F. M., & Chen, Y.-J. (2026). *decisions Space* (Version 1.0.1) [Web application]. Hugging Face. https://huggingface.co/spaces/fmegahed/decisions
www/app.js CHANGED
@@ -103,22 +103,26 @@
103
  });
104
 
105
  // ---- Video dialog (only when the page has one) ---------------------------
 
106
  // The embed address is set when the dialog opens and cleared when it
107
  // closes, so nothing loads before a click and the video stops on close.
108
- // Escape closes the dialog; Tab stays in the dialog.
 
109
 
110
  var dialog = byId('video_dialog');
111
- var openVideo = byId('open_video');
112
- if (dialog && openVideo && dialog.showModal) {
113
  var frame = byId('video_frame');
114
  var closeButton = byId('close_video');
115
  var lastFocused = null;
116
 
117
- openVideo.addEventListener('click', function() {
118
- lastFocused = document.activeElement;
119
- frame.setAttribute('src', frame.getAttribute('data-src'));
120
- dialog.showModal();
121
- closeButton.focus();
 
 
122
  });
123
  closeButton.addEventListener('click', function() { dialog.close(); });
124
  dialog.addEventListener('click', function(event) {
 
103
  });
104
 
105
  // ---- Video dialog (only when the page has one) ---------------------------
106
+ // Two buttons open it: the one of the intro and the one of the top bar.
107
  // The embed address is set when the dialog opens and cleared when it
108
  // closes, so nothing loads before a click and the video stops on close.
109
+ // Escape closes the dialog; Tab stays in the dialog. Focus goes back to
110
+ // the button that opened it (some browsers do not focus a clicked button).
111
 
112
  var dialog = byId('video_dialog');
113
+ var openers = document.querySelectorAll('.open-video');
114
+ if (dialog && openers.length && dialog.showModal) {
115
  var frame = byId('video_frame');
116
  var closeButton = byId('close_video');
117
  var lastFocused = null;
118
 
119
+ Array.prototype.forEach.call(openers, function(opener) {
120
+ opener.addEventListener('click', function() {
121
+ lastFocused = opener;
122
+ frame.setAttribute('src', frame.getAttribute('data-src'));
123
+ dialog.showModal();
124
+ closeButton.focus();
125
+ });
126
  });
127
  closeButton.addEventListener('click', function() { dialog.close(); });
128
  dialog.addEventListener('click', function(event) {
www/theme.css CHANGED
@@ -23,7 +23,7 @@
23
 
24
  :root {
25
  --red: #C41230; --tan: #EDECE2; --tan2: #CCC9B8; --tan3: #70685C;
26
- --warm: #FAF9F7; --slate: #3E5468; --ink: #1b1b1b; --muted: #5f5a52;
27
  --code-bg: #fbfaf7; --code-text: #22262b; --code-c: #5f5a52; --code-f: #3E5468; --code-s: #7a3e00;
28
  --serif: "Source Serif 4", Georgia, serif;
29
  --sans: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif;
@@ -97,11 +97,20 @@ select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible, .btn:foc
97
  .chip:has(.shiny-text-output:empty) { visibility: hidden; }
98
 
99
  /* Import: a file input that looks like one button */
 
 
100
  .import-btn .form-group { margin: 0; width: auto !important; }
101
  .import-btn .control-label, .import-btn .form-control, .import-btn .progress { display: none; }
102
- .import-btn .input-group { display: block; }
103
- .import-btn .input-group-btn { display: inline-block; width: auto; font-size: inherit; }
104
  .import-btn .btn-file { border-radius: 6px !important; }
 
 
 
 
 
 
 
105
 
106
  /* Samples menu */
107
  .menu { position: relative; }
@@ -213,6 +222,17 @@ body.is-running .run-row { display: none; }
213
  .num { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
214
  border-radius: 11px; background: var(--red); color: #fff; font-weight: 700; font-size: 12px; }
215
  @media (max-width: 899px) { .intro { grid-template-columns: minmax(0, 1fr); gap: 12px; } }
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  .stats { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 22px; margin: 0 0 10px; }
218
  .stats .v { font-family: var(--serif); font-size: 24px; font-weight: 700; }
 
23
 
24
  :root {
25
  --red: #C41230; --tan: #EDECE2; --tan2: #CCC9B8; --tan3: #70685C;
26
+ --warm: #FAF9F7; --slate: #3E5468; --ink: #1b1b1b; --muted: #5f5a52; --corn: #FDE778;
27
  --code-bg: #fbfaf7; --code-text: #22262b; --code-c: #5f5a52; --code-f: #3E5468; --code-s: #7a3e00;
28
  --serif: "Source Serif 4", Georgia, serif;
29
  --sans: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif;
 
97
  .chip:has(.shiny-text-output:empty) { visibility: hidden; }
98
 
99
  /* Import: a file input that looks like one button */
100
+ /* Its wrappers are flex boxes and its label has no margin (Bootstrap gives
101
+ labels 5px below): the button then sits on the line of the other controls */
102
  .import-btn .form-group { margin: 0; width: auto !important; }
103
  .import-btn .control-label, .import-btn .form-control, .import-btn .progress { display: none; }
104
+ .import-btn, .import-btn .form-group, .import-btn .input-group { display: flex; align-items: center; }
105
+ .import-btn .input-group-btn { display: flex; width: auto; margin: 0; font-size: inherit; }
106
  .import-btn .btn-file { border-radius: 6px !important; }
107
+ /* Every control of the top bar has one height */
108
+ .topbar-right .btn { height: 36px; box-sizing: border-box; }
109
+
110
+ /* Help video: a red play mark on the quiet button of the top bar */
111
+ .help-video-bar { gap: 8px; }
112
+ .tri { flex: none; width: 0; height: 0; border-left: 8px solid var(--red);
113
+ border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
114
 
115
  /* Samples menu */
116
  .menu { position: relative; }
 
222
  .num { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
223
  border-radius: 11px; background: var(--red); color: #fff; font-weight: 700; font-size: 12px; }
224
  @media (max-width: 899px) { .intro { grid-template-columns: minmax(0, 1fr); gap: 12px; } }
225
+ /* The help video of the intro: Corn Yellow with ink text (about 15:1) and a
226
+ Dark Tan edge, so it stands apart from the red and black controls */
227
+ .help-video-row { margin-top: 12px; }
228
+ .btn-help-video { display: inline-flex; align-items: center; gap: 10px; padding: 6px 16px 6px 6px;
229
+ background: var(--corn); border: 1px solid var(--tan3); border-radius: 999px; color: var(--ink);
230
+ font-family: var(--sans); font-weight: 600; font-size: 15px; line-height: 1.25; text-align: left; cursor: pointer; }
231
+ .btn-help-video:hover { text-decoration: underline; }
232
+ .play { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px;
233
+ border-radius: 50%; background: var(--red); }
234
+ .play::before { content: ""; margin-left: 3px; border-left: 10px solid #fff;
235
+ border-top: 6px solid transparent; border-bottom: 6px solid transparent; }
236
 
237
  .stats { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 22px; margin: 0 0 10px; }
238
  .stats .v { font-family: var(--serif); font-size: 24px; font-weight: 700; }