Add MS Windows support#265
Conversation
- Add getrecruitment CLI (scripts/get_recruitment_data.mjs) for getting bonusing data - Fix key collision in Prolific URL params: rename SESSION_ID to prolific_session_id so it no longer overwrites smile's seedID - Generate random test* IDs in Prolific demo URL for realistic testing - Add googleapis dependency; wire get_secrets.sh to download script
- Fix race condition: await completeConsent() in router, show spinner in MainApp while Firebase doc is being created after consent - Add trials prop to StroopExpView for parameterizable trial sets - Add StroopInstructionsView with proper R/G/B key instructions - Add DemographicSurveyMinimalView with age + gender only - Add TaskFeedbackSurveyView without "upload data" button - Replace Stroop quiz with task-specific comprehension questions - Replace brain.svg with Necker cube + lab name in advertisement - Clean up design.js: remove demo content, fix duplicate config, move demographics to after stroop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR aims to make the project setup/deploy tooling work on Windows by replacing bash-based scripts with Node.js equivalents and wiring them into the build workflow. It also includes several template-level changes to experiment UX/components and recruitment/data handling.
Changes:
- Replace
sh scripts/*.shcommands with cross-platformnode scripts/*.mjstooling (setup, secrets, config upload, force deploy, git env generation) and invoke git env generation fromvite.config.js. - Add UI/state to show a “Connecting...” screen while consent completion triggers DB connection, plus some store/router adjustments.
- Update the default experiment template content (survey flow/components, quiz questions, Stroop views) and alter recruitment/data handling behavior.
Reviewed changes
Copilot reviewed 23 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.js | Runs git env generation via Node before Vite config executes. |
| src/user/design.js | Updates default experiment timeline/components (Stroop, surveys). |
| src/user/components/TaskFeedbackSurveyView.vue | Adds a user-scoped feedback survey view. |
| src/user/components/stroop_exp/StroopInstructionsView.vue | Adds custom Stroop instructions view. |
| src/user/components/stroop_exp/StroopExpView.vue | Adds prop-driven trial list for Stroop experiment view. |
| src/user/components/quizQuestions.js | Replaces example quiz questions with task-relevant ones. |
| src/core/utils/utils.js | Changes Prolific session field naming in recruitment info. |
| src/core/stores/smilestore.js | Adds dbConnecting, changes Prolific example URL, changes recruitment handling. |
| src/core/router.js | Toggles dbConnecting around consent completion. |
| src/core/MainApp.vue | Shows a full-screen “Connecting...” state when dbConnecting is true. |
| src/builtins/demographicSurvey/DemographicSurveyMinimalView.vue | Adds a minimal demographic survey view. |
| src/builtins/advertisement/AdvertisementView.vue | Replaces logo image with inline SVG “Lab logo” block. |
| scripts/update_config.mjs | Cross-platform GitHub secret upload script. |
| scripts/setup_project.mjs | Cross-platform setup script installing deps + git hooks. |
| scripts/get_secrets.sh | Bash secrets fetch script (now alongside Node variant). |
| scripts/get_secrets.mjs | Cross-platform secrets fetch script using gh api. |
| scripts/generate_git_env.mjs | Cross-platform git-derived env file generator. |
| scripts/force_deploy.mjs | Cross-platform workflow dispatch helper using gh workflow run. |
| scripts/download_data.mjs | Changes export directory behavior and removes recruitment-info option. |
| README.md | Replaces upstream README with fork/template setup instructions. |
| package.json | Switches scripts to Node-based commands; adds getrecruitment command; adds deps. |
| package-lock.json | Locks new dependencies and transitive updates. |
| .gitignore | Ignores downloaded scripts/get_recruitment_data.mjs. |
| .github/workflows/docs-deploy.yml | Converts docs workflow to a no-op for forks. |
| .github/workflows/deploy.yml | Skips deploy when required secrets are not configured. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| setRecruitmentService(service, info) { | ||
| this.data.recruitmentService = service | ||
| this.private.recruitmentInfo = info | ||
| // recruitment IDs are not stored in Firebase — instead POST to a Google Form if configured | ||
| if (info && import.meta.env.VITE_RECRUITMENT_FORM_URL && import.meta.env.VITE_RECRUITMENT_FORM_ENTRY) { | ||
| const formData = new FormData() | ||
| formData.append(import.meta.env.VITE_RECRUITMENT_FORM_ENTRY, JSON.stringify({ | ||
| session_id: this.browserPersisted.seedID, | ||
| service, | ||
| ...info, | ||
| })) | ||
| fetch(import.meta.env.VITE_RECRUITMENT_FORM_URL, { method: 'POST', mode: 'no-cors', body: formData }).catch( | ||
| () => {} | ||
| ) // fire and forget | ||
| } | ||
| }, |
There was a problem hiding this comment.
setRecruitmentService() no longer assigns this.private.recruitmentInfo = info. Several UI paths read smilestore.private.recruitmentInfo (e.g., StatusBar email prefill, ThanksView completion links), so this will leave those values undefined. Keep storing info in private.recruitmentInfo locally (even if you avoid writing it to Firebase) so downstream components still work.
| // recruitment IDs are not stored in Firebase — instead POST to a Google Form if configured | ||
| if (info && import.meta.env.VITE_RECRUITMENT_FORM_URL && import.meta.env.VITE_RECRUITMENT_FORM_ENTRY) { | ||
| const formData = new FormData() | ||
| formData.append(import.meta.env.VITE_RECRUITMENT_FORM_ENTRY, JSON.stringify({ | ||
| session_id: this.browserPersisted.seedID, | ||
| service, | ||
| ...info, | ||
| })) | ||
| fetch(import.meta.env.VITE_RECRUITMENT_FORM_URL, { method: 'POST', mode: 'no-cors', body: formData }).catch( | ||
| () => {} | ||
| ) // fire and forget |
There was a problem hiding this comment.
Posting recruitment IDs directly from the client using VITE_RECRUITMENT_FORM_URL/ENTRY makes the endpoint and entry ID publicly visible in the shipped bundle, so anyone can submit arbitrary payloads to the form. If this data needs integrity, consider sending these IDs via a server-side endpoint/Cloud Function (or add some form of verification/rate limiting) rather than a public Google Form POST.
| await api.completeConsent() | ||
| api.store.browserEphemeral.dbConnecting = false |
There was a problem hiding this comment.
dbConnecting is set to true/false around await api.completeConsent(), but if completeConsent() throws/rejects, the flag will never be reset and the UI can get stuck on the “Connecting...” screen. Wrap the await in a try/finally (or ensure errors clear the flag) so dbConnecting is always restored.
| await api.completeConsent() | |
| api.store.browserEphemeral.dbConnecting = false | |
| try { | |
| await api.completeConsent() | |
| } finally { | |
| api.store.browserEphemeral.dbConnecting = false | |
| } |
| function installHook(hookName) { | ||
| const hooksDir = path.join(".git", "hooks"); | ||
| const hookPath = path.join(hooksDir, hookName); | ||
| const hookContent = "#!/bin/sh\nnode scripts/generate_git_env.mjs\n"; | ||
|
|
||
| fs.mkdirSync(hooksDir, { recursive: true }); | ||
| fs.writeFileSync(hookPath, hookContent, "utf8"); | ||
| } |
There was a problem hiding this comment.
The installed git hook files are written but never marked executable. On macOS/Linux, non-executable hooks in .git/hooks/* won’t run, so generate_git_env won’t regenerate after commit/checkout. After writing the hook, set permissions (e.g., chmod 755) and consider using process.execPath in the hook body to avoid relying on node being on PATH.
| const hash = run("git", ["rev-parse", "--short", "HEAD"]).toLowerCase(); | ||
| const branch = run("git", ["rev-parse", "--abbrev-ref", "HEAD"]).toLowerCase(); | ||
| const lastMsg = run("git", ["log", "-1", "--pretty=%s"]); | ||
|
|
||
| const codenamizeOutput = run("node", ["scripts/codenamize.cjs", `/${owner}/${projectName}/${branch}`]); | ||
| const codeName = codenamizeOutput.split(/\r?\n/).pop() || ""; | ||
|
|
||
| const envLines = [ | ||
| "# DO NOT EDIT THIS FILE IT IS AUTOMATICALLY GENERATED", | ||
| "# this file is automatically generated by the post-commit hook (see scripts/generate_git_env).", | ||
| "", | ||
| `VITE_PROJECT_NAME = ${projectName}`, | ||
| `VITE_GIT_HASH = ${hash}`, | ||
| "VITE_GIT_REPO_NAME = ${VITE_PROJECT_NAME}", | ||
| `VITE_GIT_OWNER = ${owner}`, | ||
| `VITE_GIT_BRANCH_NAME = ${branch}`, | ||
| 'VITE_DEPLOY_BASE_PATH = "/${VITE_GIT_OWNER}/${VITE_GIT_REPO_NAME}/${VITE_GIT_BRANCH_NAME}/"', | ||
| `VITE_CODE_NAME = ${codeName}`, | ||
| `VITE_GIT_LAST_MSG = ${lastMsg}`, | ||
| "", |
There was a problem hiding this comment.
lastMsg (commit subject) is written into .env.git.local without escaping/quoting. Commit subjects containing #, newlines, or quotes can break dotenv parsing (and therefore Vite env loading). Escape/quote this value (e.g., write it as a JSON-string-escaped value, or wrap in quotes and escape "/newlines/#).
| dbChanges: true, | ||
| urls: { | ||
| prolific: '?PROLIFIC_PID=XXXX&STUDY_ID=XXXX&SESSION_ID=XXXXX#/welcome/prolific/', | ||
| prolific: `?PROLIFIC_PID=test${Array.from({length: 6}, () => Math.floor(Math.random() * 16).toString(16)).join('')}&STUDY_ID=test${Array.from({length: 6}, () => Math.floor(Math.random() * 16).toString(16)).join('')}&SESSION_ID=test${Array.from({length: 6}, () => Math.floor(Math.random() * 16).toString(16)).join('')}#/welcome/prolific/`, |
There was a problem hiding this comment.
initBrowserEphemeral.urls.prolific uses Math.random() during module initialization, which makes the initial store state non-deterministic (and can complicate debugging/test snapshots). If the goal is just an example URL, consider using a stable placeholder (e.g., XXXX) or generate the random example at the moment it’s displayed instead of at import time.
| prolific: `?PROLIFIC_PID=test${Array.from({length: 6}, () => Math.floor(Math.random() * 16).toString(16)).join('')}&STUDY_ID=test${Array.from({length: 6}, () => Math.floor(Math.random() * 16).toString(16)).join('')}&SESSION_ID=test${Array.from({length: 6}, () => Math.floor(Math.random() * 16).toString(16)).join('')}#/welcome/prolific/`, | |
| prolific: | |
| '?PROLIFIC_PID=testXXXXXX&STUDY_ID=testXXXXXX&SESSION_ID=testXXXXXX#/welcome/prolific/', |
| @@ -18,19 +35,7 @@ const trials = api.steps.append([ | |||
| }, | |||
| ]) | |||
|
|
|||
| trials[0] | |||
| .append([ | |||
| { id: 'a', word: 'SHIP', color: 'red', condition: 'unrelated' }, | |||
| { id: 'b', word: 'MONKEY', color: 'green', condition: 'unrelated' }, | |||
| { id: 'c', word: 'ZAMBONI', color: 'blue', condition: 'unrelated' }, | |||
| { id: 'd', word: 'RED', color: 'red', condition: 'congruent' }, | |||
| { id: 'e', word: 'GREEN', color: 'green', condition: 'congruent' }, | |||
| { id: 'f', word: 'BLUE', color: 'blue', condition: 'congruent' }, | |||
| { id: 'g', word: 'GREEN', color: 'red', condition: 'incongruent' }, | |||
| { id: 'h', word: 'BLUE', color: 'green', condition: 'incongruent' }, | |||
| { id: 'i', word: 'RED', color: 'blue', condition: 'incongruent' }, | |||
| ]) | |||
| .shuffle() | |||
| trials[0].append(props.trials).shuffle() | |||
|
|
|||
| trials.append([{ id: 'summary' }]) | |||
There was a problem hiding this comment.
This file defines both a trials prop (props.trials) and a local const trials = api.steps.append(...), which makes the code hard to read and easy to misuse. Rename one of them (e.g., trialSpec for the stepper spec, or trialList for the prop) to avoid the name collision.
| // 1. Import main built-in View components | ||
| import AdvertisementView from '@/builtins/advertisement/AdvertisementView.vue' | ||
| import MTurkRecruitView from '@/builtins/mturk/MTurkRecruitView.vue' | ||
| import InformedConsentView from '@/builtins/informedConsent/InformedConsentView.vue' | ||
| import DemographicSurveyView from '@/builtins/demographicSurvey/DemographicSurveyView.vue' | ||
| import DeviceSurveyView from '@/builtins/deviceSurvey/DeviceSurveyView.vue' | ||
| import InstructionsView from '@/builtins/instructions/InstructionsView.vue' | ||
| import DemographicSurveyView from '@/builtins/demographicSurvey/DemographicSurveyMinimalView.vue' | ||
| import InstructionsQuizView from '@/builtins/instructionsQuiz/InstructionsQuiz.vue' | ||
| import DebriefView from '@/builtins/debrief/DebriefView.vue' | ||
| import TaskFeedbackSurveyView from '@/builtins/taskFeedbackSurvey/TaskFeedbackSurveyView.vue' | ||
| import ThanksView from '@/builtins/thanks/ThanksView.vue' | ||
| import WithdrawView from '@/builtins/withdraw/WithdrawView.vue' | ||
| import WindowSizerView from '@/builtins/windowSizer/WindowSizerView.vue' | ||
|
|
||
| // 2. Import user View components | ||
| import ExpView from '@/builtins/demoTasks/ExpView.vue' | ||
| import FavoriteNumber from '@/builtins/demoTasks/FavoriteNumber.vue' | ||
| import FavoriteColor from '@/builtins/demoTasks/FavoriteColor.vue' | ||
| import InstructionsView from '@/user/components/stroop_exp/StroopInstructionsView.vue' | ||
| import StroopExpView from '@/user/components/stroop_exp/StroopExpView.vue' | ||
| import TaskFeedbackSurveyView from '@/user/components/TaskFeedbackSurveyView.vue' | ||
|
|
There was a problem hiding this comment.
This PR is described as adding Windows support for setup scripts, but it also changes experiment flow/components (custom Stroop instructions/experiment, minimal demographic survey, new feedback survey) and recruitment handling. Consider splitting these functional changes into separate PRs or updating the PR description to reflect the additional scope so reviewers can assess impact appropriately.
Smile setup scripts are currently bash script, which can not be run on Windows. This PR will add OS agnostic node js scripts, that will work on all Operating Systems.