Skip to content
47 changes: 45 additions & 2 deletions R/admin_utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ build_base_url <- function(session) {
#' Resets a user's annotation file by keeping the header row and the first
#' three columns (coordinates, REF, ALT) but clearing all other data columns.
#' Also updates the database by decrementing vote counts for all votes that
#' the user had cast. This allows a user to start voting from scratch while
#' preserving the randomized order of variants.
#' the user had cast. Additionally, resets the vote_input_methods counts in the
#' user's info.json file to 0. This allows a user to start voting from scratch
#' while preserving the randomized order of variants.
#'
#' @param annotation_file_path Character. Full path to the user's annotation TSV file
#' @param user_annotations_colnames Character vector. Column names for the annotation file
Expand Down Expand Up @@ -182,6 +183,48 @@ reset_user_annotations <- function(annotation_file_path, user_annotations_colnam
)

message("Successfully reset annotations for file: ", annotation_file_path)

# Reset vote_input_methods counts in the user info JSON file
# The info.json file is in the same directory as the annotations file,
# with a similar naming pattern (replacing _annotations.tsv with _info.json)
user_dir <- dirname(annotation_file_path)
base_name <- sub("_annotations\\.tsv$", "", basename(annotation_file_path))
user_info_file <- file.path(user_dir, paste0(base_name, "_info.json"))

message("Resetting vote_input_methods in user info file: ", user_info_file)

if (file.exists(user_info_file)) {
tryCatch({
# Read the existing user info JSON file
user_info <- jsonlite::read_json(user_info_file)

# Reset vote_input_methods counts to 0
if (!is.null(user_info$vote_input_methods)) {
user_info$vote_input_methods <- list(
hotkey_count = 0,
mouse_count = 0,
unknown_count = 0
)

# Write the updated user info back to the file
jsonlite::write_json(
user_info,
user_info_file,
auto_unbox = TRUE,
pretty = TRUE
)

message("Successfully reset vote_input_methods for file: ", user_info_file)
} else {
message("No vote_input_methods found in user info file: ", user_info_file)
}
}, error = function(e) {
warning("Failed to reset vote_input_methods in user info file: ", e$message)
})
} else {
message("User info file does not exist: ", user_info_file)
}

return(TRUE)
}, error = function(e) {
warning("Failed to reset annotations: ", e$message)
Expand Down
7 changes: 6 additions & 1 deletion R/main_server.R
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,12 @@ makeVotingAppServer <- function(db_pool, cfg) {
user_info <- list(
user_id = user_id,
voting_institute = voting_institute,
images_randomisation_seed = seed
images_randomisation_seed = seed,
vote_input_methods = list(
hotkey_count = 0,
mouse_count = 0,
unknown_count = 0
)
)

session$userData$sessionInfo <- list(
Expand Down
48 changes: 48 additions & 0 deletions R/mod_voting.R
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,54 @@ votingServer <- function(
quote = FALSE
)

# Update user_info.json with input method tracking
if (!already_voted) {
user_info_file <- session$userData$userInfoFile
if (file.exists(user_info_file)) {
user_info <- jsonlite::read_json(user_info_file)

# Get the input method (default to "unknown" if not set to avoid misattribution)
input_method <- input$last_input_method
if (is.null(input_method) || input_method == "") {
input_method <- "unknown"
warning("Input method not captured for vote, using 'unknown'")
}

# Initialize vote_input_methods if it doesn't exist
if (is.null(user_info$vote_input_methods)) {
user_info$vote_input_methods <- list(
hotkey_count = 0,
mouse_count = 0,
unknown_count = 0
)
}

# Update the count based on input method
if (input_method == "hotkey") {
user_info$vote_input_methods$hotkey_count <-
user_info$vote_input_methods$hotkey_count + 1
} else if (input_method == "mouse") {
user_info$vote_input_methods$mouse_count <-
user_info$vote_input_methods$mouse_count + 1
} else {
# Track unknown cases for debugging
if (is.null(user_info$vote_input_methods$unknown_count)) {
user_info$vote_input_methods$unknown_count <- 0
}
user_info$vote_input_methods$unknown_count <-
user_info$vote_input_methods$unknown_count + 1
}

# Write updated user_info back to file
jsonlite::write_json(
user_info,
user_info_file,
auto_unbox = TRUE,
pretty = TRUE
)
}
}

if (
already_voted &&
previous_agreement != input$agreement
Expand Down
111 changes: 111 additions & 0 deletions VOTE_INPUT_TRACKING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Vote Input Method Tracking

## Overview
This document describes the implementation of vote input method tracking in ShinyImageVoteR. This feature tracks whether users cast votes using keyboard hotkeys or mouse clicks to help analyze the relationship between input method and voting speed.

## Motivation
Analysis revealed that median vote times range from 2-15 seconds across users. We want to understand how much of this difference is explained by hotkey usage versus mouse clicking.

## Implementation

### 1. JavaScript Changes (hotkeys.js)

#### Hotkey Detection
When a user presses a hotkey (1, 2, 3, 4 for radio buttons or a, s, d, f for checkboxes):
- The input element is marked with `dataset.inputMethod = "hotkey"`
- `Shiny.setInputValue("voting-last_input_method", "hotkey")` sends this information to R

#### Mouse Click Detection
When a user clicks on a radio button or checkbox:
- The click is verified to be within the voting questions div (same validation as hotkeys)
- The input element is marked with `dataset.inputMethod = "mouse"`
- `Shiny.setInputValue("voting-last_input_method", "mouse")` sends this information to R

**Note**: This validation prevents accidental tracking of clicks on other inputs with the same names elsewhere in the application.

### 2. R Server Changes

#### User Info Structure (main_server.R)
When a user logs in, their `user_info.json` file is initialized with:
```json
{
"user_id": "User1",
"voting_institute": "InstituteA",
"images_randomisation_seed": 12345,
"vote_input_methods": {
"hotkey_count": 0,
"mouse_count": 0,
"unknown_count": 0
}
}
```

**Note**: The `unknown_count` field tracks cases where the input method couldn't be determined, which helps identify potential issues with the tracking mechanism.

#### Vote Tracking (mod_voting.R)
When a user casts a **new vote** (not a vote change):
1. The input method is read from `input$last_input_method`
2. If not set, it's marked as "unknown" (with a warning logged)
3. The `user_info.json` file is read
4. The appropriate counter (`hotkey_count`, `mouse_count`, or `unknown_count`) is incremented
5. The updated info is written back to `user_info.json`

**Note**:
- Vote changes are not tracked to avoid skewing the data. We only track initial votes.
- Using "unknown" instead of defaulting to "mouse" ensures accurate data and helps identify tracking issues.

## Data Analysis

After users complete their voting sessions, the `user_info.json` files can be analyzed to:
1. Calculate the percentage of hotkey vs mouse usage per user
2. Correlate this with average voting times (from `time_till_vote_casted_in_seconds` in annotations)
3. Determine if hotkey users are significantly faster

### Example Analysis Query
For each user:
- Read `user_info.json` to get hotkey/mouse counts
- Read `user_annotations.tsv` to get vote times
- Calculate:
- Hotkey usage percentage: `hotkey_count / (hotkey_count + mouse_count) * 100`
- Median vote time: median of `time_till_vote_casted_in_seconds`
- Plot correlation between hotkey usage and vote speed

## Files Modified

1. **inst/shiny-app/www/js/hotkeys.js**: Added input method detection and Shiny communication
2. **vignettes/www/hotkeys.js**: Updated to match inst version
3. **R/main_server.R**: Added `vote_input_methods` initialization to user_info
4. **R/mod_voting.R**: Added tracking logic to update user_info.json after each vote

## Testing

To test this implementation:
1. Start the Shiny app
2. Log in as a user
3. Vote using hotkeys (press 1, 2, 3, or 4)
4. Vote using mouse clicks
5. Check the `user_info.json` file for the user - it should show updated counts

Example:
```json
{
"user_id": "TestUser",
"voting_institute": "TestInstitute",
"images_randomisation_seed": 67890,
"vote_input_methods": {
"hotkey_count": 12,
"mouse_count": 5,
"unknown_count": 0
}
}
```

**Note**: If `unknown_count` is greater than 0, it indicates potential issues with the tracking mechanism that should be investigated.

## Future Enhancements

Potential improvements:
1. Track vote changes separately to understand behavior patterns
2. Add per-vote logs instead of just counts for more detailed analysis
3. Add real-time dashboard showing hotkey vs mouse usage statistics
4. Export aggregated statistics across all users
34 changes: 34 additions & 0 deletions inst/shiny-app/www/js/hotkeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,41 @@ document.addEventListener("keydown", (e) => {
if (!input) return;

input.checked = toggle ? !input.checked : true;
// Mark this input as triggered via hotkey
input.dataset.inputMethod = "hotkey";
// Send to Shiny for tracking
if (window.Shiny) {
Shiny.setInputValue("voting-last_input_method", "hotkey", { priority: "event" });
}
input.dispatchEvent(new Event("change", { bubbles: true }));
return;
}
});

// Mark mouse clicks on voting inputs
document.addEventListener("click", (e) => {
const target = e.target;

// Check if click is on a radio button or checkbox in voting groups
if (target.type === "radio" || target.type === "checkbox") {
if (target.name === "voting-agreement" || target.name === "voting-observation") {
// Verify the target is within the voting questions div (same as hotkey handler)
const questionsDiv = document.getElementById("voting-voting_questions_div");
if (!questionsDiv || questionsDiv.offsetParent === null) {
return;
}

// Check if target is within the questions div
if (!questionsDiv.contains(target)) {
return;
}

// Mark this input as triggered via mouse
target.dataset.inputMethod = "mouse";
// Send to Shiny for tracking
if (window.Shiny) {
Shiny.setInputValue("voting-last_input_method", "mouse", { priority: "event" });
}
}
}
});
Loading
Loading