diff --git a/.gitignore b/.gitignore index afd871c30..f30613131 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ node_modules/ # Packed shared utils package *deliberation-lab-utils-*.tgz +.idea/ + # Python scripts .venv *.pyc diff --git a/frontend/assets/survival_desert/aid.jpg b/frontend/assets/survival_desert/aid.jpg new file mode 100644 index 000000000..fe275365e Binary files /dev/null and b/frontend/assets/survival_desert/aid.jpg differ diff --git a/frontend/assets/survival_desert/book.jpg b/frontend/assets/survival_desert/book.jpg new file mode 100644 index 000000000..d217c0747 Binary files /dev/null and b/frontend/assets/survival_desert/book.jpg differ diff --git a/frontend/assets/survival_desert/flashlight.jpg b/frontend/assets/survival_desert/flashlight.jpg new file mode 100644 index 000000000..277c52f94 Binary files /dev/null and b/frontend/assets/survival_desert/flashlight.jpg differ diff --git a/frontend/assets/survival_desert/knife.jpg b/frontend/assets/survival_desert/knife.jpg new file mode 100644 index 000000000..41087a0f9 Binary files /dev/null and b/frontend/assets/survival_desert/knife.jpg differ diff --git a/frontend/assets/survival_desert/mirror.jpg b/frontend/assets/survival_desert/mirror.jpg new file mode 100644 index 000000000..fdcec66a3 Binary files /dev/null and b/frontend/assets/survival_desert/mirror.jpg differ diff --git a/frontend/assets/survival_desert/parachute.jpg b/frontend/assets/survival_desert/parachute.jpg new file mode 100644 index 000000000..7fae613aa Binary files /dev/null and b/frontend/assets/survival_desert/parachute.jpg differ diff --git a/frontend/assets/survival_desert/pistol.jpg b/frontend/assets/survival_desert/pistol.jpg new file mode 100644 index 000000000..14a90bbef Binary files /dev/null and b/frontend/assets/survival_desert/pistol.jpg differ diff --git a/frontend/assets/survival_desert/raincoat.jpg b/frontend/assets/survival_desert/raincoat.jpg new file mode 100644 index 000000000..9c725621b Binary files /dev/null and b/frontend/assets/survival_desert/raincoat.jpg differ diff --git a/frontend/assets/survival_desert/salt.jpg b/frontend/assets/survival_desert/salt.jpg new file mode 100644 index 000000000..bc050e57a Binary files /dev/null and b/frontend/assets/survival_desert/salt.jpg differ diff --git a/frontend/assets/survival_desert/sea_and_desert_solutions.pdf b/frontend/assets/survival_desert/sea_and_desert_solutions.pdf new file mode 100644 index 000000000..814311198 Binary files /dev/null and b/frontend/assets/survival_desert/sea_and_desert_solutions.pdf differ diff --git a/frontend/assets/survival_desert/water.jpg b/frontend/assets/survival_desert/water.jpg new file mode 100644 index 000000000..783d79275 Binary files /dev/null and b/frontend/assets/survival_desert/water.jpg differ diff --git a/frontend/index.example.html b/frontend/index.example.html index f6a83dded..ef40beaaf 100644 --- a/frontend/index.example.html +++ b/frontend/index.example.html @@ -1,15 +1,25 @@ - - - - - Deliberate Lab - - - - - <% if (process.env.NODE_ENV==='production' ) { %> + + + + Deliberate Lab + + + + + <% if (process.env.NODE_ENV==='production' ) { %> <% } %> - - + .noscript h1 { + font-size: 22px; + font-weight: 500; + line-height: 28px; + margin: 0; + } - - - + .noscript p { + margin: 0; + } + + - \ No newline at end of file + + + + diff --git a/frontend/src/components/experiment_builder/stage_builder_dialog.ts b/frontend/src/components/experiment_builder/stage_builder_dialog.ts index ca01778e7..aabf8dccd 100644 --- a/frontend/src/components/experiment_builder/stage_builder_dialog.ts +++ b/frontend/src/components/experiment_builder/stage_builder_dialog.ts @@ -20,6 +20,7 @@ import { createAssetAllocationStage, createChatStage, createRankingStage, + createLRRankingStage, createInfoStage, createFlipCardStage, createMultiAssetAllocationStage, @@ -41,6 +42,10 @@ import { getLASStageConfigs, getAnonLASStageConfigs, } from '../../shared/templates/lost_at_sea'; +import { + LR_METADATA, + getLeadershipRejectionStageConfigs, +} from '../../shared/templates/leader_rejection_template'; import { getChipMetadata, getChipNegotiationStageConfigs, @@ -173,7 +178,7 @@ export class StageBuilderDialog extends MobxLitElement { configuration! @@ -301,6 +306,25 @@ export class StageBuilderDialog extends MobxLitElement { `; } + private renderLRCard() { + const metadata = LR_METADATA; + const configs = getLeadershipRejectionStageConfigs(); + + const addGame = () => { + this.addGame(metadata, configs); + }; + + return html` +
+
${metadata.name}
+
+ ${metadata.description} +
+
+
+ `; + } + private renderAgentIntegrationCard() { const addTemplate = () => { this.addTemplate(getAgentParticipantIntegrationTemplate()); @@ -508,6 +532,22 @@ export class StageBuilderDialog extends MobxLitElement { `; } + private renderLRRankingCard() { + const addStage = () => { + this.addStage(createLRRankingStage()); + }; + + return html` +
+
πŸ—³οΈ LR Triggering Selection Logic
+
+ TODO: Stage that triggers the selection of a leader based on candidacy + and performance in two initial tasks +
+
+ `; + } + private renderRevealCard() { const addStage = () => { this.addStage(createRevealStage()); diff --git a/frontend/src/components/stages/leader_status_reveal_view.ts b/frontend/src/components/stages/leader_status_reveal_view.ts new file mode 100644 index 000000000..79668097f --- /dev/null +++ b/frontend/src/components/stages/leader_status_reveal_view.ts @@ -0,0 +1,88 @@ +import '../participant_profile/profile_display'; + +import {MobxLitElement} from '@adobe/lit-mobx'; +import {CSSResultGroup, html, nothing} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; + +import {LRRankingStagePublicData} from '@deliberation-lab/utils'; +import {core} from '../../core/core'; +import {ParticipantService} from '../../services/participant.service'; +import {styles} from './ranking_reveal_view.scss'; +import {getParticipantInlineDisplay} from '../../shared/participant.utils'; + +import {CohortService} from '../../services/cohort.service'; + +/** Leader selection reveal view */ +@customElement('leader-reveal-view') +export class LeaderRevealView extends MobxLitElement { + static override styles: CSSResultGroup = [styles]; + + private readonly participantService = core.getService(ParticipantService); + private readonly cohortService = core.getService(CohortService); + + @property() publicData: LRRankingStagePublicData | undefined = undefined; + + override render() { + if (!this.publicData) return html`

Waiting for results...

`; + + const leaderStatusMap = this.publicData.leaderStatusMap || {}; + const winnerId = this.publicData.winnerId || ''; + const participantId = this.participantService.profile?.publicId ?? ''; // βœ… FIXED + const status = leaderStatusMap[participantId] ?? 'waiting'; + + const messages: Record = { + candidate_accepted: 'βœ… You applied and were selected as leader!', + candidate_rejected: '❌ You applied but were not selected.', + non_candidate_accepted: + 'βœ… You did not apply, but since no one else did, you were selected!', + non_candidate_rejected: '❌ You did not apply and were not selected.', + non_candidate_hypo_selected: + 'πŸ’‘ You did not apply, but had you done so, you would have been selected.', + non_candidate_hypo_rejected: + 'ℹ️ You did not apply, and even if you had, you would not have been selected.', + waiting: '⏳ Waiting for results...', + }; + + // πŸ” Determine whether to show who the leader is + const showWinner = + winnerId && participantId !== winnerId && status !== 'waiting'; + + // πŸ” Convert winnerId β†’ "Participant 7506" + let winnerPretty: string | null = null; + if (showWinner) { + const winnerProfile = this.cohortService.participantMap?.[winnerId]; + if (winnerProfile) { + winnerPretty = getParticipantInlineDisplay(winnerProfile); // πŸ‘ˆ magic happens here + } else { + winnerPretty = winnerId; // fallback (should rarely happen) + } + } + + console.log('[LeaderRevealView] my ID:', participantId); + console.log('[LeaderRevealView] all keys:', Object.keys(leaderStatusMap)); + console.log( + '[LeaderRevealView] publicData snapshot:', + JSON.parse(JSON.stringify(this.publicData)), + ); + + return html` +
+

Leader Selection Result

+

${messages[status] ?? messages.waiting}

+ ${showWinner && winnerPretty + ? html` +

+ ⭐ ${winnerPretty} was selected as the leader. +

+ ` + : nothing} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'leader-reveal-view': LeaderRevealView; + } +} diff --git a/frontend/src/components/stages/lr_info_ranking_view.ts b/frontend/src/components/stages/lr_info_ranking_view.ts new file mode 100644 index 000000000..ad029571b --- /dev/null +++ b/frontend/src/components/stages/lr_info_ranking_view.ts @@ -0,0 +1,44 @@ +import {MobxLitElement} from '@adobe/lit-mobx'; +import {html, nothing} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; + +import {core} from '../../core/core'; +import {ParticipantService} from '../../services/participant.service'; +import {RankingStageConfig} from '@deliberation-lab/utils'; + +@customElement('lr-info-ranking-view') +export class LRInfoRankingView extends MobxLitElement { + @property() stage!: RankingStageConfig; + + private readonly participantService = core.getService(ParticipantService); + + override render() { + if (!this.stage) return nothing; + + return html` +
+

${this.stage.name}

+

This is an information-only page. No ranking is required.

+ + +
+ `; + } + + private async next() { + // Submit empty ranking list β†’ triggers LR logic + const ps = this.participantService; + await ps.updateRankingStageParticipantAnswer( + this.stage.id, + [], // very important! + ); + + await ps.progressToNextStage(); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'lr-info-ranking-view': LRInfoRankingView; + } +} diff --git a/frontend/src/components/stages/payout_summary_view.ts b/frontend/src/components/stages/payout_summary_view.ts index 66d619bb4..f1da630a7 100644 --- a/frontend/src/components/stages/payout_summary_view.ts +++ b/frontend/src/components/stages/payout_summary_view.ts @@ -296,8 +296,9 @@ export class PayoutView extends MobxLitElement {
${rankingWinner !== null - ? `Election winner's answer:` - : 'Your answer:'} + ? `Leader's answer:` + : //? `Election winner's answer:` + 'Your answer:'}
${participantAnswer?.text ?? ''}
diff --git a/frontend/src/components/stages/ranking_editor.ts b/frontend/src/components/stages/ranking_editor.ts index 5fb7e95fe..50aecc45e 100644 --- a/frontend/src/components/stages/ranking_editor.ts +++ b/frontend/src/components/stages/ranking_editor.ts @@ -12,6 +12,8 @@ import { createRankingItem, ParticipantRankingStage, ItemRankingStage, + LRRankingStage, + RankingType, } from '@deliberation-lab/utils'; import {styles} from './ranking_editor.scss'; @@ -38,6 +40,16 @@ export class RankingEditorComponent extends MobxLitElement { .enableSelfVoting; const isElection = this.stage.strategy === ElectionStrategy.CONDORCET; const isParticipantRanking = this.stage.rankingType === 'participants'; + const isLRRanking = this.stage.rankingType === RankingType.LR; + + console.debug( + '[LR] renderRankingSettings, isElection=', + isElection, + ', isParticipantRanking=', + isParticipantRanking, + ', isLRRanking=', + isLRRanking, + ); const updateRankingType = () => { if (!this.stage) return; @@ -61,7 +73,7 @@ export class RankingEditorComponent extends MobxLitElement { }; const toggleElectionStrategy = () => { - if (!this.stage) return; + if (!this.stage || this.stage.rankingType === RankingType.LR) return; const newStrategy = isElection ? ElectionStrategy.NONE : ElectionStrategy.CONDORCET; @@ -80,6 +92,24 @@ export class RankingEditorComponent extends MobxLitElement { `; const waitForAllParticipants = this.stage.progress.waitForAllParticipants; + console.debug( + '[LR] renderRankingSettings bis, isElection=', + isElection, + ', isParticipantRanking=', + isParticipantRanking, + ', isLRRanking=', + isLRRanking, + ); + + // RankingType.LR + if (isLRRanking) { + return html` +
+
LR does not require specific settings
+
+ `; + } + // RankingType.ITEMS RankingType.PARTICIPANTS : return html`
Ranking Settings
diff --git a/frontend/src/components/stages/ranking_participant_view.ts b/frontend/src/components/stages/ranking_participant_view.ts index f87a9b91a..af58d363a 100644 --- a/frontend/src/components/stages/ranking_participant_view.ts +++ b/frontend/src/components/stages/ranking_participant_view.ts @@ -40,6 +40,11 @@ export class RankingView extends MobxLitElement { @property() stage: RankingStageConfig | undefined = undefined; private getItems() { + // LR info-only ranking: return zero items + if (this.stage?.rankingType === RankingType.LR) { + return []; + } + return this.stage ? getCohortRankingItems( this.cohortService.activeParticipants, @@ -62,10 +67,17 @@ export class RankingView extends MobxLitElement { return nothing; } + const isLRRanking = this.stage.rankingType === RankingType.LR; + console.debug( + '[LR] ranking-participant-view, this.stage.rankingType=', + this.stage.rankingType, + ); + const items = this.getItems(); const disabled = + this.stage.rankingType !== RankingType.LR && this.participantAnswerService.getNumRankings(this.stage.id) < - items.length; + items.length; const saveRankings = async () => { if (!this.stage) return; @@ -79,9 +91,13 @@ export class RankingView extends MobxLitElement { return html` -
- ${this.renderStartZone()} ${this.renderEndZone()} -
+ ${!isLRRanking + ? html` +
+ ${this.renderStartZone()} ${this.renderEndZone()} +
+ ` + : ''} ${this.stage.progress.showParticipantProgress ? html`` @@ -241,7 +257,7 @@ export class RankingView extends MobxLitElement { ...rankings.slice(existingIndex + 1), ]; if (existingIndex <= newIndex) { - newIndex -= 1; // Adjust index because participant was removed + newIndex -= 1; // Adjust } } rankings = [ @@ -249,6 +265,7 @@ export class RankingView extends MobxLitElement { itemId, ...rankings.slice(newIndex), ]; + // Update ranking list this.participantAnswerService.updateRankingAnswer( this.stage.id, @@ -280,6 +297,7 @@ export class RankingView extends MobxLitElement { const rankings = this.participantAnswerService.getRankingList( this.stage.id, ); + const onCancel = () => { if (index === -1 || !this.stage) { return; diff --git a/frontend/src/components/stages/reveal_summary_view.ts b/frontend/src/components/stages/reveal_summary_view.ts index b950966f9..4c91cc6d2 100644 --- a/frontend/src/components/stages/reveal_summary_view.ts +++ b/frontend/src/components/stages/reveal_summary_view.ts @@ -1,6 +1,7 @@ import './ranking_reveal_view'; import './survey_reveal_view'; import './allocation_reveal_view'; +import './leader_status_reveal_view'; import {MobxLitElement} from '@adobe/lit-mobx'; import {CSSResultGroup, html, nothing} from 'lit'; @@ -83,6 +84,12 @@ export class RevealView extends MobxLitElement { `; case StageKind.RANKING: + // If it's a leadership version (has leaderStatusMap), use your new view + if ('leaderStatusMap' in publicData && publicData?.leaderStatusMap) { + return html` + + `; + } return html` diff --git a/frontend/src/shared/file.utils.ts b/frontend/src/shared/file.utils.ts index 20a5775b3..7685931f4 100644 --- a/frontend/src/shared/file.utils.ts +++ b/frontend/src/shared/file.utils.ts @@ -19,6 +19,7 @@ import { PayoutStageConfig, PayoutStageParticipantAnswer, RankingStageConfig, + LRRankingStagePublicData, StageKind, SurveyPerParticipantStageConfig, SurveyQuestionKind, @@ -1066,6 +1067,38 @@ export function getRankingStageCSVColumns( : '', ); + // 🧩 Leadership Rejection (LR) extension + if (participant) { + // Column data + let leaderStatus = ''; + let winnerId = ''; + if (publicData && 'leaderStatusMap' in publicData) { + const lrData = publicData as LRRankingStagePublicData; + const participantPublicId = participant.profile.publicId ?? ''; + leaderStatus = lrData.leaderStatusMap?.[participantPublicId] ?? ''; + winnerId = lrData.winnerId ?? ''; + } + console.debug( + '[LR][getRankingStageCSVColumns] [Leadership Rejection (LR) extension] columns data: leaderStatus=', + leaderStatus, + ', winnerId=', + winnerId, + ); + // Column for participant’s leader status + columns.push(leaderStatus); + // Optional: export winner ID again for clarity + columns.push(winnerId); + } else { + // Column header. Note: if participant is null (case of headers to return), publicData is null too. + // Column for participant’s leader status + console.debug( + '[LR][getRankingStageCSVColumns] [Leadership Rejection (LR) extension] header title', + ); + columns.push(`Leader status - ${rankingStage.id}`); + // Optional: export winner ID again for clarity + columns.push(`Leader winner ID - ${rankingStage.id}`); + } + return columns; } diff --git a/frontend/src/shared/templates/leader_rejection_template.ts b/frontend/src/shared/templates/leader_rejection_template.ts new file mode 100644 index 000000000..7099257c2 --- /dev/null +++ b/frontend/src/shared/templates/leader_rejection_template.ts @@ -0,0 +1,1847 @@ +/** + * Leadership Rejection (LR) Experiment Template + * Adapted from Lost at Sea (v4) + * + * Features: + * - Two baseline individual tasks used to compute performance + * - Performance-weighted probabilistic leader selection + * - Multiple rounds with application, selection, feedback + */ + +import { + Experiment, + MultipleChoiceSurveyQuestion, + PayoutCurrency, + ProfileType, + StageConfig, + StageKind, + SurveyQuestion, + SurveyQuestionKind, + ParticipantProfile, + ParticipantProfileBase, + ParticipantProfileExtended, + choice, + createExperimentConfig, + createInfoStage, + createMultipleChoiceItem, + createPayoutStage, + createProfileStage, + createMetadataConfig, + createMultipleChoiceSurveyQuestion, + createMultipleChoiceComprehensionQuestion, + createRevealStage, + createRankingRevealItem, + createScaleSurveyQuestion, + createStageTextConfig, + createSurveyPayoutItem, + createSurveyRevealItem, + createSurveyStage, + createTextSurveyQuestion, + createTOSStage, + createTransferStage, + createStageProgressConfig, + randint, + RevealAudience, + LRRankingStagePublicData, + LAS_WTL_QUESTION_ID, + LR_BASELINE_TASK1_ID, + LR_BASELINE_TASK2_ID, + r1_apply, + createLRRankingStage, +} from '@deliberation-lab/utils'; +import {mustWaitForAllParticipants} from '../experiment.utils'; +import { + LAS_PART_1_SURVIVAL_SURVEY_STAGE_ID, + LAS_PART_2_ELECTION_STAGE_ID, + LAS_PART_2_UPDATED_TASK_SURVEY_STAGE_ID, + LAS_PART_3_LEADER_TASK_SURVEY_ID, + LAS_PART_3_REVEAL_DESCRIPTION_INFO, + LAS_PART_3_REVEAL_DESCRIPTION_PRIMARY, +} from './lost_at_sea'; + +// **************************************************************************** +// Experiment config +// **************************************************************************** + +export const LR_METADATA = createMetadataConfig({ + name: '🎯 Leadership Rejection', + publicName: 'Decision-making Experiment', + description: + 'A multi-round experiment examining individual and group decision-making.', +}); + +/* --------------------------------------------------------------------------- + * Stage flow + * ------------------------------------------------------------------------- */ +export function getLeadershipRejectionStageConfigs(): StageConfig[] { + const stages: StageConfig[] = []; + + // Consent / intro + stages.push(LR_TOS_STAGE); + stages.push(LR_INTRO_STAGE); + + // Profile + stages.push(LR_PERSONAL_INFO_STAGE); + stages.push(LR_PROFILE_STAGE); + + //Individual stage + stages.push(LR_P1_TASK1_INSTRUCTIONS_STAGE); + stages.push(LR_BASELINE_TASK_1); + stages.push(LR_P1_TASK2_INSTRUCTIONS_STAGE); + stages.push(LR_BASELINE_TASK_2); + stages.push(LR_BASELINE_CONFIDENCE); + stages.push(LR_BASELINE_CONFIDENCE_v2); + + // Transfer + stages.push(LR_TRANSFER_STAGE); + + // Group Stage - Round 1 + stages.push(LR_R1_INSTRUCTIONS); + stages.push(LR_R1_APPLY_STAGE); + stages.push(LR_R1_BELIEF_CANDIDATES); + stages.push(LR_R1_INSTRUCTIONS_GROUP); + stages.push(LR_R1_GROUP_TASK_STAGE); + stages.push(LR_R1_STATUS_FEEDBACK_STAGE); + stages.push(LR_R1_BELIEF_STAGE); + stages.push(LR_R1_BELIEF_CANDIDATES_UPDATE); + stages.push(LR_ROUND1_CONFIDENCE_v2); + + // Group Stage - Round 2 + stages.push(LR_R2_INSTRUCTIONS); + stages.push(LR_R2_APPLY_STAGE); + stages.push(LR_R2_BELIEF_CANDIDATES); + stages.push(LR_R2_INSTRUCTIONS_GROUP); + stages.push(LR_R2_GROUP_TASK_STAGE); + stages.push(LR_R2_STATUS_FEEDBACK_STAGE); + stages.push(LR_R2_BELIEF_STAGE); + stages.push(LR_R2_BELIEF_CANDIDATES_UPDATE); + stages.push(LR_ROUND2_CONFIDENCE_v2); + + // Group Stage - Hypothetical Round 3 + //stages.push(LR_R3_INSTRUCTIONS); + stages.push(LR_R3_APPLY_STAGE); + + // Final feedback, survey, payout + stages.push(LR_FEEDBACK_STAGE); + stages.push(LR_FEEDBACK_STAGE_BIS); + stages.push(LR_PAYOUT_INFO_STAGE); + stages.push(LR_PAYOUT_STAGE); + stages.push(LR_SURVEY_STAGE_PRIMARY); + stages.push(LR_FINAL_SURVEY_STAGE); + + return stages; +} + +// **************************************************************************** +// Shared constants and functions +// **************************************************************************** + +/*NEW TASK: SURVIVAL IN THE DESERT: */ +export const SD_SCENARIO_REMINDER = + 'Here is a reminder of the scenario:\n\nYou have crash-landed in the Desert. The plane has burned out, the pilot is dead, and no one knows your exact location. You and the three other passengers are uninjured but stranded about 70 miles from the nearest known settlement.\nYou managed to save 10 items before the plane caught fire.\n\nEvaluate the relative importance of items in each presented pair by selecting the one you believe is most useful. You can earn Β£2 per correct answer if that question is drawn to determine your payoff.'; + +interface SDItem { + name: string; + ranking: number; +} +export const SD_ITEMS: Record = { + mirror: {name: 'A Mirror', ranking: 1}, + raincoat: {name: 'A plastic raincoat per Person', ranking: 2}, + water: {name: 'Water (2L / per Person)', ranking: 3}, + flashlight: {name: 'A flashlight with 4 batteries', ranking: 4}, + parachute: {name: 'A Parachute (red and white)', ranking: 5}, + knife: {name: 'A Folding Knife', ranking: 6}, + pistol: {name: 'A .45 Calibre Pistol (loaded)', ranking: 7}, + aid: {name: 'A First-Aid Kit', ranking: 8}, + book: {name: 'A book titled β€œEdible Animals in the Desert”', ranking: 9}, + salt: {name: 'A bottle of salt tablets', ranking: 10}, +}; + +//export function getSDItemImageId(itemId: string) { +// return `https://raw.githubusercontent.com/PAIR-code/deliberate-lab/refs/heads/main/frontend/assets/survival_desert/${itemId}.jpg`; +//} +export function getSDItemImageId(itemId: string) { + return `https://raw.githubusercontent.com/clebouleau/deliberate-lab/main/frontend/assets/survival_desert/${itemId}.jpg`; +} + +export const SD_ITEM_MULTIPLE_CHOICE_QUESTION_TITLE = + 'Choose the item that would be more helpful to your survival.'; + +export const SD_ITEM_SCALE_QUESTION_TITLE = + 'How confident are you that your answer is correct?'; + +export const ITEMS_SD_SET_1: Array<[string, string]> = [ + ['salt', 'raincoat'], + ['parachute', 'pistol'], + ['water', 'knife'], + ['aid', 'mirror'], + ['flashlight', 'book'], +]; + +export const ITEMS_SD_SET_2: Array<[string, string]> = [ + ['mirror', 'book'], + ['flashlight', 'knife'], + ['parachute', 'aid'], + ['water', 'salt'], + ['pistol', 'raincoat'], +]; + +export const SD_INDIVIDUAL_ITEMS_MULTIPLE_CHOICE_QUESTIONS: MultipleChoiceSurveyQuestion[] = + ITEMS_SD_SET_1.map((itemSet) => + createSDMultipleChoiceQuestion(itemSet[0], itemSet[1]), + ); + +export const SD_LEADER_ITEMS_MULTIPLE_CHOICE_QUESTIONS: MultipleChoiceSurveyQuestion[] = + ITEMS_SD_SET_2.map((itemSet) => + createSDMultipleChoiceQuestion(itemSet[0], itemSet[1]), + ); + +export function createSDSurvivalSurvey( + itemQuestions: MultipleChoiceSurveyQuestion[], +) { + const questions: SurveyQuestion[] = []; + itemQuestions.forEach((question) => { + questions.push(question); + questions.push( + createScaleSurveyQuestion({ + questionTitle: SD_ITEM_SCALE_QUESTION_TITLE, + upperText: 'Very confident', + lowerText: 'Not confident', + }), + ); + }); + return questions; +} + +export function getCorrectSDAnswer(id1: string, id2: string): string { + const item1 = SD_ITEMS[id1]; + const item2 = SD_ITEMS[id2]; + if (!item1 || !item2) return ''; + + return item1.ranking < item2.ranking ? id1 : id2; +} + +export function createSDMultipleChoiceQuestion( + id1: string, + id2: string, +): MultipleChoiceSurveyQuestion { + return { + id: `sd-${id1}-${id2}`, + kind: SurveyQuestionKind.MULTIPLE_CHOICE, + questionTitle: SD_ITEM_MULTIPLE_CHOICE_QUESTION_TITLE, + options: [ + { + id: id1, + imageId: getSDItemImageId(id1), + text: SD_ITEMS[id1]?.name ?? '', + }, + { + id: id2, + imageId: getSDItemImageId(id2), + text: SD_ITEMS[id2]?.name ?? '', + }, + ], + correctAnswerId: getCorrectSDAnswer(id1, id2), + }; +} + +/*LAS TASK */ + +export const LAS_SCENARIO_REMINDER = + 'Here is a reminder of the scenario:\n\nYou and your friends are on a yacht trip across the Atlantic. A fire breaks out, and the skipper and crew are lost. The yacht is sinking, and your location is unclear.\nYou have saved 10 items, a life raft, and a box of matches.\n\nEvaluate the relative importance of items in each presented pair by selecting the one you believe is most useful. You can earn Β£2 per correct answer if that question is drawn to determine your payoff.'; + +interface LASItem { + name: string; + ranking: number; +} + +export const LAS_ITEMS: Record = { + mirror: {name: 'Mirror', ranking: 1}, + oil: {name: 'Can of oil/petrol (10L)', ranking: 2}, + water: {name: 'Water (25L)', ranking: 3}, + rations: {name: 'Case of army rations', ranking: 4}, + sheeting: {name: 'Plastic sheeting', ranking: 5}, + chocolate: {name: 'Chocolate bars (2 boxes)', ranking: 6}, + fishing: {name: 'A fishing kit & pole', ranking: 7}, + rope: {name: 'Nylon rope (15 ft.)', ranking: 8}, + cushion: {name: 'Floating seat cushion', ranking: 9}, + repellent: {name: 'Can of shark repellent', ranking: 10}, + rubbing_alcohol: {name: 'Bottle of rubbing alcohol', ranking: 11}, + radio: {name: 'Small transistor radio', ranking: 12}, + map: {name: 'Maps of the Atlantic Ocean', ranking: 13}, + netting: {name: 'Mosquito netting', ranking: 14}, +}; + +export function getLASItemImageId(itemId: string) { + return `https://raw.githubusercontent.com/PAIR-code/deliberate-lab/refs/heads/main/frontend/assets/lost_at_sea/${itemId}.jpg`; +} + +export const LAS_ITEM_MULTIPLE_CHOICE_QUESTION_TITLE = + 'Choose the item that would be more helpful to your survival.'; + +export const LAS_ITEM_SCALE_QUESTION_TITLE = + 'How confident are you that your answer is correct?'; + +export const ITEMS_SET_1: Array<[string, string]> = [ + ['cushion', 'mirror'], + ['oil', 'water'], + ['rations', 'sheeting'], + ['rope', 'netting'], + ['map', 'radio'], +]; + +export const ITEMS_SET_2: Array<[string, string]> = [ + ['mirror', 'rope'], + ['oil', 'netting'], + ['water', 'cushion'], + ['rations', 'radio'], + ['sheeting', 'map'], +]; + +export const LAS_INDIVIDUAL_ITEMS_MULTIPLE_CHOICE_QUESTIONS: MultipleChoiceSurveyQuestion[] = + ITEMS_SET_1.map((itemSet) => + createLASMultipleChoiceQuestion(itemSet[0], itemSet[1]), + ); + +export const LAS_LEADER_ITEMS_MULTIPLE_CHOICE_QUESTIONS: MultipleChoiceSurveyQuestion[] = + ITEMS_SET_2.map((itemSet) => + createLASMultipleChoiceQuestion(itemSet[0], itemSet[1]), + ); + +export function createLASSurvivalSurvey( + itemQuestions: MultipleChoiceSurveyQuestion[], +) { + const questions: SurveyQuestion[] = []; + itemQuestions.forEach((question) => { + questions.push(question); + questions.push( + createScaleSurveyQuestion({ + questionTitle: LAS_ITEM_SCALE_QUESTION_TITLE, + upperText: 'Very confident', + lowerText: 'Not confident', + }), + ); + }); + return questions; +} + +export function getCorrectLASAnswer(id1: string, id2: string): string { + const item1 = LAS_ITEMS[id1]; + const item2 = LAS_ITEMS[id2]; + if (!item1 || !item2) return ''; + + return item1.ranking < item2.ranking ? id1 : id2; +} + +export function createLASMultipleChoiceQuestion( + id1: string, + id2: string, +): MultipleChoiceSurveyQuestion { + return { + id: `las-${id1}-${id2}`, + kind: SurveyQuestionKind.MULTIPLE_CHOICE, + questionTitle: LAS_ITEM_MULTIPLE_CHOICE_QUESTION_TITLE, + options: [ + { + id: id1, + imageId: getLASItemImageId(id1), + text: LAS_ITEMS[id1]?.name ?? '', + }, + { + id: id2, + imageId: getLASItemImageId(id2), + text: LAS_ITEMS[id2]?.name ?? '', + }, + ], + correctAnswerId: getCorrectLASAnswer(id1, id2), + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Terms of Service +// ───────────────────────────────────────────────────────────────────────────── +const LR_TOS_LINES = [ + 'Thank you for participating in this study.', + '\nThis research is conducted by the Paris School of Economics and has been approved by their institutional review board for [ethical standards](https://www.parisschoolofeconomics.eu/a-propos-de-pse/engagements-ethiques/integrite-scientifique/).', + '\nThe study will take approximately 35 minutes. Detailed instructions about the compensation will be provided in the relevant sections.', + '\n By participating, you agree that your responses, including basic demographic information, will be saved. No identifiable personal data will be collected. All data will be anonymized and used solely for scientific research. Your data will not be shared with third parties.', + "\n By ticking the box below and clicking 'Next,' you accept these terms and proceed with the study.", + '\n\nIf you have any questions, you can write to us at pse.experimenter@gmail.com.', +]; + +const LR_TOS_STAGE = createTOSStage({ + tosLines: LR_TOS_LINES, + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// **************************************************************************** +// Intro info stage +// **************************************************************************** +const LR_INTRO_INFO_DESCRIPTION_PRIMARY = ''; + +const LR_INTRO_INFO_LINES = [ + 'In this experiment you will play engaging games that present survival scenarios, and answer questions. \n', + 'This experiment consists of two parts:\n\n* Part 1: lasts about 10–15 minutes. \n* Part 2: lasts about 20 minutes.', + 'You will earn a **Β£5 fixed fee** for completing the full experiment. On top of that, you’ll have the opportunity to earn **bonuses of up to Β£3**, depending on your own and other participants decisions. At the end of the experiment, one of the tasks you are about to complete will be randomly selected to determine part of your bonus payment. On top of that, you can gain additional bonuses in certain questions (more details later).\n', + '⚠️ At the end of Part 1, you will be redirected to a **waiting page**. This waiting time is part of the experiment and allows us to form groups, as the experiment involves live interactions with other participants in Part 2. **You must remain on this page for the full requested durationβ€”if you leave early or close the study before the waiting period ends, your submission will not be approved.** Once a group is formed, you will be invited to continue to Part 2.\n' + + '\n\n In rare cases, if we are unable to match you with a group for Part 2 (for example, if there are not enough participants online), you will complete only the first part and still receive a fixed fee for completing the first part.\n', + 'To sum up: You will complete the first part individually, and then wait to be invited to the next part of the experiment in groups. **If your are invited, your submission will only be approved if you do the full experiment**. In the rare case where we could not send you an invitation for Part 2, your submission will still be approved.\n', + 'πŸ’Έ Your payments will be translated into the currency of your specification when they are paid out to you on the Prolific platform. **Please allow us 24-48 hours to process the payments.**', + '‼️ This is an interactive experiment. Because of this, there may occasionally be short waiting periods while others make their decisions. These waiting times are already included in the estimated duration and payment. Please remain patient and attentive during these moments.\n ', + 'If you experience technical difficulties during the study, **please message the experiment administrators on Prolific as soon as possible.**', + 'Please click β€œNext stage” to proceed.', +]; + +const LR_INTRO_STAGE = createInfoStage({ + name: 'Welcome to the experiment', + descriptions: createStageTextConfig({ + primaryText: LR_INTRO_INFO_DESCRIPTION_PRIMARY, + }), + infoLines: LR_INTRO_INFO_LINES, + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +const LR_PERSONAL_INFO_STAGE = createSurveyStage({ + name: 'Personal information', + descriptions: createStageTextConfig({ + primaryText: + 'Please fill out the following for demographic data collection. Your gender will remain anonymous throughout this experiment.', + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + questionTitle: 'What is your gender?', + options: [ + { + id: '1', + imageId: '', + text: 'Female', + }, + { + id: '2', + imageId: '', + text: 'Male', + }, + { + id: '3', + imageId: '', + text: 'Other', + }, + ], + }), + ], + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// **************************************************************************** +// Profile stage +// **************************************************************************** + +const LR_PROFILE_STAGE = createProfileStage({ + id: 'profile', + name: 'View randomly assigned profile', + descriptions: createStageTextConfig({ + primaryText: + 'Here is the profile that has been randomly assigned to you. This information may be visible to other participants.', + }), + profileType: ProfileType.ANONYMOUS_PARTICIPANT, + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// **************************************************************************** +// Part 1 - Task 1 +// **************************************************************************** + +// ******************* +// Instructions + +const LR_P1_TASK1_INSTRUCTIONS_INFO_LINES = [ + '## Imagine the following scenario:', + "You have chartered a yacht with three friends for a holiday trip across the Atlantic Ocean. None of you have sailing experience, so you hired an experienced skipper and a two-person crew. In the middle of the Atlantic a fierce fire breaks out in the ship's galley. The skipper and crew have been lost whilst trying to fight the blaze. Much of the yacht is destroyed and is slowly sinking. Vital navigational and radio equipment are damaged, and your location is unclear. Your best estimate is that you are many hundreds of miles from the nearest landfall.", + '*You and your friends have managed to save 10 items, undamaged and intact after the fire. In addition, you have salvaged a four-man rubber life craft and a box of matches*.', + '## Your task:', + 'You are asked to **evaluate these 10 items in terms of their importance for your survival, as you wait to be rescued**. The computer will randomly generate pairs of items, and you will select which of the two is the most useful in your situation.', + '## Payment:', + 'At the end of the experiment, one task from the experiment will be randomly selected to determine your bonus payment. Within that task, one question will be chosen at random.', + 'If your answer to that question matches the solutions provided by a panel of experts, you will receive a Β£2 bonus; otherwise, the bonus will be Β£0.', + '⚠️ **Important note: It is possible that your performance on this task will influence later stages of the experiment. Please make sure to stay focused and do your best throughout.**\n', + 'Please click β€œNext stage” to proceed.', +]; + +const LR_P1_TASK1_INSTRUCTIONS_STAGE = createInfoStage({ + name: 'Part 1a instructions', + infoLines: LR_P1_TASK1_INSTRUCTIONS_INFO_LINES, + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// ******************* +// Task 1a + +//export const LR_BASELINE_TASK1_ID = 'baseline1'; + +const LR_BASELINE_TASK_1 = createSurveyStage({ + id: LR_BASELINE_TASK1_ID, + name: 'Part 1a - Survival task', + descriptions: createStageTextConfig({infoText: LAS_SCENARIO_REMINDER}), + questions: createLASSurvivalSurvey( + LAS_INDIVIDUAL_ITEMS_MULTIPLE_CHOICE_QUESTIONS, + ), + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// **************************************************************************** +// Part 1 - Task 2 +// **************************************************************************** + +// ******************* +// Instructions + +const LR_P1_TASK2_INSTRUCTIONS_INFO_LINES = [ + '## We now ask you to imagine a new scenario:', + 'You and three friends are flying in a small twin-engine plane when it crash-lands in the Atacama Desert. The aircraft has burned out completely, and the pilot and co-pilot did not survive. None of you are injured, but no one was able to report your position before the crash. Shortly before impact, the pilot estimated you were about 50 miles from a mining campβ€”the nearest known settlementβ€”and roughly 65 miles off your planned route. The immediate area is quite flat, except for occasional cacti, and appears to be rather barren. The last weather report indicated that the temperature would reach 110 F today, which means that the temperature at ground level will be 130 F. You are dressed in lightweight clothing-short-sleeved shirts, pants, socks, and street shoes.', + 'Before your plane caught fire, your group was able to salvage 10 items, undamaged and intact. In addition, everyone has a handkerchief and collectively, you have 3 packs of cigarettes and a ballpoint pen.', + '## Your task:', + 'You are asked to **evaluate these 10 items in terms of their importance for your survival, as you wait to be rescued**. The computer will randomly generate pairs of items, and you will select which of the two is the most useful in your situation.', + '## Payment:', + 'At the end of the experiment, one task from the experiment will be randomly selected to determine your bonus payment. Within that task, one question will be chosen at random.', + 'If your answer to that question matches the solutions provided by a panel of experts, you will receive a Β£2 bonus; otherwise, the bonus will be Β£0.', + '⚠️ **Important note: It is possible that your performance on this task will influence later stages of the experiment. Please make sure to stay focused and do your best throughout.**\n', + 'Please click β€œNext stage” to proceed.', +]; + +const LR_P1_TASK2_INSTRUCTIONS_STAGE = createInfoStage({ + name: 'Part 1b instructions', + infoLines: LR_P1_TASK2_INSTRUCTIONS_INFO_LINES, + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// ******************* +// Task 1b + +// export const LR_BASELINE_TASK2_ID = 'baseline2'; + +const LR_BASELINE_TASK_2 = createSurveyStage({ + id: LR_BASELINE_TASK2_ID, + name: 'Part 1b - Survival task', + descriptions: createStageTextConfig({infoText: SD_SCENARIO_REMINDER}), + questions: createSDSurvivalSurvey( + SD_INDIVIDUAL_ITEMS_MULTIPLE_CHOICE_QUESTIONS, + ), + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// **************************************************************************** +// Part 1 - Confidence Stage +// **************************************************************************** +export const LR_BASELINE_CONF_INFO = + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.'; + +const LR_BASELINE_CONFIDENCE = createSurveyStage({ + name: 'Part 1 - Performance Estimation', + descriptions: createStageTextConfig({ + primaryText: LR_BASELINE_CONF_INFO, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + questionTitle: + 'We would like you to guess how well you did in Part 1 compared to a sample of 100 other participants who completed the same task before you. \n\n How well do you think you did compared to previous participants?', + options: [ + { + id: '1', + imageId: '', + text: 'My score is in the top quarter of all participants', + }, + { + id: '2', + imageId: '', + text: 'My score is in the second quarter (above average)', + }, + { + id: '3', + imageId: '', + text: 'My score is in the third quarter (below average)', + }, + { + id: '4', + imageId: '', + text: 'My score is in the bottom quarter', + }, + ], + }), + ], + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +export const LR_BASELINE_CONF_INFO_v2 = + 'You can get an extra bonus for these questions. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.'; + +const LR_BASELINE_CONFIDENCE_v2 = createSurveyStage({ + name: 'Part 1 - Performance Estimation', + descriptions: createStageTextConfig({ + primaryText: LR_BASELINE_CONF_INFO_v2, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + questionTitle: + '\n Imagine you were matched with a group of five other participants (so six in total, including you) doing the same task at the moment. Based on your performance in the previous task, please select the option that best reflects your belief about your rank:', + options: [ + { + id: '1', + imageId: '', + text: 'I would be ranked 1st (the best performer in my group)', + }, + { + id: '2', + imageId: '', + text: 'I would be ranked 2nd', + }, + { + id: '3', + imageId: '', + text: 'I would be ranked 3rd', + }, + { + id: '4', + imageId: '', + text: 'I would be ranked 4th', + }, + { + id: '5', + imageId: '', + text: 'I would be ranked 5th', + }, + { + id: '6', + imageId: '', + text: 'I would be ranked 6th (the worst performer in my group)', + }, + ], + }), + createScaleSurveyQuestion({ + id: 'conf_baseline', + questionTitle: + 'How many questions of the 10 questions do you think you answered correctly in the previous tasks?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 10, + }), + ], + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +// **************************************************************************** +// "Lobby" - transfer stage +// **************************************************************************** +export const LR_TRANSFER_DESCRIPTION_PRIMARY = + 'Please wait on this page for up to 10 minutes while we pair you with other participants for the next part of the experiment.' + + '\n\n During this time, there may be occasional attention checks to ensure that you are waiting and still active.' + + '\n\n **If you leave this page or close the browser before the time is up, your submission will not be approved for payment.**' + + '\n\n Once we find a suitable group, a link will appear on this page inviting you to continue to the next part of the experiment. If you decline the invitation, your submission will not be approved, as completing Part 2 is required for full participation.' + + '\n\n In the rare case that we are unable to find a group for you within 10 minutes, the page will time out automatically. If this happens, your submission will still be approved and you will receive a fixed fee for your participation.' + + '\n\n The next part will take approximately 20 minutes to complete.' + + '\n\n This is an interactive experiment, meaning you will be grouped with other participants who are playing at the same time. Because of this, there may occasionally be short waiting periods while others make their decisions. These waiting times are already included in the estimated duration and payment. Please remain patient and attentive during these moments.' + + 'Thank you for your patience!'; + +export const LR_TRANSFER_STAGE = createTransferStage({ + name: 'Lobby', + descriptions: createStageTextConfig({ + primaryText: LR_TRANSFER_DESCRIPTION_PRIMARY, + }), + enableTimeout: true, + timeoutSeconds: 600, // 10 minutes + + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +//========================================================== +//========================================================== +//========================================================== +// GROUP STAGE - ROUND 1 +//========================================================== +//========================================================== +//========================================================== + +//========================================================== +// Info stage +//========================================================== + +const LR_R1_INSTRUCTIONS_INFO = [ + `You are now about to start the second part of the Experiment.`, + 'For this part, and for the remainder of the experiment, you will work in groups. You have been randomly assigned to a group with other participants who are taking part in the same experiment today.' + + '\n\n **A leader will be designated within each group**. The details concerning the role of the leaders and how they are chosen are provided below. \n', + 'The task that you will complete in Part 2a is the same as in the first part of this experiment but with different pairs of items. \n', + + "\n\n **Leader Role**: For each question, the leaders will be responsible for providing an answer on behalf of the group. The leader's answers will be evaluated just as in Part 1 (i.e. compared to the answers given by a panel of experts), and **all members' payoff for this task will be entirely determined by the leader's answers**. Leaders will receive a fixed Β£0.50 payment for endorsing the role.\n\n", + + '\n\n **Leader Selection**: On the next page, you will be asked whether you would like to apply for the leader role.\n' + + '* Everyone who chooses to apply will be considered a candidate.\n' + + '* We will then conduct a weighted lottery based on performance scores in Part 1a and Part 1b:\n' + + ' * Participants with higher performance scores higher in Part 1a and 1b have chances of being selected.\n' + + ' * The top performer (the person who has the highest number of correct answers out of the 10 questions of Part 1a + Part 1b) has the highest chance of being selected.\n' + + ' * The probability then decreases with rank. For example, if 4 people apply:\n' + + ' * Best performer β†’ ~60%\n' + + ' * Second β†’ 30%\n' + + ' * Third β†’ 8%\n' + + ' * Fourth β†’ 2%\n' + + '* If no one applies, all group members will automatically enter the lottery using the same rule.\n\n', + + '\n\n **Timing of the Selection**: To avoid unnecessary waiting, everyone will complete the group task before the leader is revealed. ⚠️ Keep in mind that you could be selected as the leader, so please do your best to maximize your own (and potentially your group’s) payoff during the task.\n\n', + + '\n\n **Payment**: At the end of the experiment, one task will be randomly selected to determine your bonus payment. Within that task, one question will also be chosen at random.\n' + + 'If the selected question comes from Part 2a, the leader’s answer will be used to determine the outcome. You will receive a Β£2 bonus if the leader’s answer is correct, and Β£0 otherwise.\n', +]; + +const LR_R1_INSTRUCTIONS = createInfoStage({ + name: 'Part 2a - Instructions', + infoLines: LR_R1_INSTRUCTIONS_INFO, +}); + +//========================================================== +// Application stage +//========================================================== +export const LR_R1_APPLY_STAGE_INFO = + 'As mentioned on the previous page one leader will be appointed in each group. The leader will be chosen through a lottery in which only those who apply are considered, and each applicant’s chance of being selected depends on their performance (with higher performers having higher odds), while if no one applies, all group members enter the lottery under the same rule.' + + 'The leader’s answer will determine everyone’s payoff for this stage of the experiment.\n'; + +const LR_R1_APPLY_STAGE = createSurveyStage({ + id: 'r1_apply', + name: 'Part 2a - Apply', + descriptions: createStageTextConfig({ + primaryText: LR_R1_APPLY_STAGE_INFO, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + id: 'apply_r1', + questionTitle: + 'Would you like to apply to be the leader of your group for this stage?', + options: [ + {id: 'yes', imageId: '', text: 'Yes'}, + {id: 'no', imageId: '', text: 'No'}, + ], + }), + createScaleSurveyQuestion({ + id: 'wtl_r1', + questionTitle: + 'How much do you want to be the leader (0 = not at all, 10 = very much)?', + lowerText: 'Not at all', + upperText: 'Very much', + lowerValue: 0, + upperValue: 10, + }), + ], +}); + +const LR_R1_BELIEF_CANDIDATES = createSurveyStage({ + id: 'r1_belief_candidate', + name: 'Part 2a - Survey', + descriptions: createStageTextConfig({ + primaryText: + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.', + }), + questions: [ + createScaleSurveyQuestion({ + id: 'r1_belief_candidate', + questionTitle: + 'How many members of the group applied to the role (excluding you), according to you?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 5, + }), + ], +}); + +//========================================================== +// Group Task 1 +//========================================================== + +//Instructions +export const LR_R1_INSTRUCTIONS_GROUP_INFO = + 'You will complete the group task, while the computer gathers information to determine who the selected leader is. ' + + "\n\n Recall that for this task only the leader's answers count." + + "\n\n ⚠️ Since you could potentially be the leader without knowing it yet, keep in mind that your performance might determine everyone's payoff for this part." + + '\n\nAfter the task ends, you will be informed of whether or not you were the leader for this round.' + + '\n\n Remember also that in the extreme case where no one applied, you could be selected as the leader. As a result, try to perform to the best of your ability in the following task, regardless of your application status.'; + +export const LR_R1_INSTRUCTIONS_GROUP = createLRRankingStage({ + id: 'r1_instructions', + name: 'Part 2a - Task Instructions', + descriptions: createStageTextConfig({ + primaryText: LR_R1_INSTRUCTIONS_GROUP_INFO, + }), + progress: createStageProgressConfig({waitForAllParticipants: true}), +}); + +//Task +export const LR_R1_GROUP_TASK_ID = 'grouptask1'; + +export const LR_R1_GROUP_TASK_STAGE = createSurveyStage({ + id: LR_R1_GROUP_TASK_ID, + name: 'Part 2a - Group task', + descriptions: createStageTextConfig({infoText: LAS_SCENARIO_REMINDER}), + questions: createLASSurvivalSurvey( + LAS_LEADER_ITEMS_MULTIPLE_CHOICE_QUESTIONS, + ), +}); + +//========================================================== +// Feedback Stage +//========================================================== + +export const LR_R1_STATUS_FEEDBACK_STAGE = createRevealStage({ + id: 'r1_status_feedback', + name: 'Part 2a β€” Leader Selection Result', + descriptions: createStageTextConfig({ + primaryText: 'Results of leader selection for this round.', + infoText: 'Please wait until everyone in your group has reached this page.', + }), + progress: createStageProgressConfig({ + showParticipantProgress: false, + waitForAllParticipants: false, + }), + items: [ + createRankingRevealItem({ + id: 'r1_instructions', + customRender: 'leaderStatus', // 🧩 triggers your custom reveal + revealAudience: RevealAudience.CURRENT_PARTICIPANT, + }), + ], +}); + +//========================================================== +// Attribution beliefs Stage +//========================================================== + +const LR_R1_BELIEF_STAGE = createSurveyStage({ + id: 'r1_belief', + name: 'Part 2a - Survey', + questions: [ + createMultipleChoiceSurveyQuestion({ + id: 'belief_binary_rule_r1', + questionTitle: + 'Do you think the selected leader had the highest performance score in your group? (If you correctly answer this question you can get an extra Β£0.50 bonus)', + options: [ + {id: 'yes', imageId: '', text: 'Yes'}, + {id: 'no', imageId: '', text: 'No'}, + ], + }), + createScaleSurveyQuestion({ + id: 'belief_rule_r1', + questionTitle: + 'Thinking about the result of this round β€” whether you were selected as leader, not selected, or chose not to apply. To what extent do you think the outcome of the leadership selection was due to performance versus external circumstances/luck?', + lowerText: 'Entirely due to external factors (luck, randomness)', + upperText: 'Entirely due to performance', + lowerValue: 0, + upperValue: 100, + useSlider: true, + }), + ], +}); + +const LR_R1_BELIEF_CANDIDATES_UPDATE = createSurveyStage({ + id: 'r1_belief_candidate_update', + name: 'Part 2a - Survey', + descriptions: createStageTextConfig({ + primaryText: + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.', + }), + questions: [ + createScaleSurveyQuestion({ + id: 'r1_belief_candidate_update', + questionTitle: + 'We ask you to answer this question again: How many members of the group applied to the role (excluding you), according to you?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 5, + }), + ], +}); +export const LR_ROUND1_CONF_INFO_v2 = + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.'; + +const LR_ROUND1_CONFIDENCE_v2 = createSurveyStage({ + name: 'Part 2a - Performance Estimation', + descriptions: createStageTextConfig({ + primaryText: LR_ROUND1_CONF_INFO_v2, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + questionTitle: + 'We would like you to guess how well you did in the previous task compared to other members of your group. Please select the option that best reflects your belief about your rank:', + options: [ + { + id: '1', + imageId: '', + text: 'I would be ranked 1st (the best performer in my group)', + }, + { + id: '2', + imageId: '', + text: 'I would be ranked 2nd', + }, + { + id: '3', + imageId: '', + text: 'I would be ranked 3rd', + }, + { + id: '4', + imageId: '', + text: 'I would be ranked 4th', + }, + { + id: '5', + imageId: '', + text: 'I would be ranked 5th', + }, + { + id: '6', + imageId: '', + text: 'I would be ranked 6th (the worst performer in my group)', + }, + ], + }), + createScaleSurveyQuestion({ + id: 'conf_round1', + questionTitle: + 'How many of the 5 questions from the previous task do you think you answered correctly?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 5, + }), + ], + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +//========================================================== +//========================================================== +//========================================================== +// Group Stage - Round 2 +//========================================================== +//========================================================== +//========================================================== + +//========================================================== +// Info stage +//========================================================== +export const LR_R2_INSTRUCTIONS_INFO = [ + `In the next part of the experiment, you are given the chance to select a leader again. The role of the leader, as well as the selection rule, are the same as before.`, + 'The type of task that you will complete in this part is similar to previous survival tasks, but with different pairs of items.', + 'As a reminder, here is how leaders are selected:', + "\n\n **Leader Role**: For each question, the leaders will be responsible for providing an answer on behalf of the group. The leader's answers will be evaluated just as in Part 1 (i.e. compared to the answers given by a panel of experts), and **all members' payoff for this task will be entirely determined by the leader's answers**. Leaders will receive a fixed Β£0.50 payment for endorsing the role.\n\n", + + '\n\n **Leader Selection**: On the next page, you will be asked whether you would like to apply for the leader role.\n' + + '* Everyone who chooses to apply will be considered a candidate.\n' + + '* We will then conduct a weighted lottery based on performance scores in Part 1a and Part 1b:\n' + + ' * Participants with higher performance scores higher in Part 1a and 1b have chances of being selected.\n' + + ' * The top performer (the person who has the highest number of correct answers out of the 10 questions of Part 1a + Part 1b) has the highest chance of being selected.\n' + + ' * The probability then decreases with rank. For example, if 4 people apply:\n' + + ' * Best performer β†’ ~60%\n' + + ' * Second β†’ 30%\n' + + ' * Third β†’ 8%\n' + + ' * Fourth β†’ 2%\n' + + '* If no one applies, all group members will automatically enter the lottery using the same rule.\n\n', + + '\n\n **Timing of the Selection**: To avoid unnecessary waiting, everyone will complete the group task before the leader is revealed. ⚠️ Keep in mind that you could be selected as the leader, so please do your best to maximize your own (and potentially your group’s) payoff during the task.\n\n', + + '\n\n **Payment**: At the end of the experiment, one task will be randomly selected to determine your bonus payment. Within that task, one question will also be chosen at random.\n' + + 'If the selected question comes from Part 2a, the leader’s answer will be used to determine the outcome. You will receive a Β£2 bonus if the leader’s answer is correct, and Β£0 otherwise.\n', +]; + +export const LR_R2_INSTRUCTIONS = createInfoStage({ + id: 'r2_first_instructions', + name: 'Part 2b - Instructions', + progress: createStageProgressConfig({waitForAllParticipants: false}), + infoLines: LR_R2_INSTRUCTIONS_INFO, +}); + +//========================================================== +// Survey stage +//========================================================== +export const LR_R2_APPLY_STAGE_INFO = + 'As mentioned on the previous page one leader will be appointed in each group. The leader will be chosen through a lottery in which only those who apply are considered, and each applicant’s chance of being selected depends on their performance (with higher performers having higher odds), while if no one applies, all group members enter the lottery under the same rule.' + + 'The leader’s answer will determine everyone’s payoff for this stage of the experiment.\n'; + +const LR_R2_APPLY_STAGE = createSurveyStage({ + id: 'r2_apply', + name: 'Part 2b - Apply', + descriptions: createStageTextConfig({ + primaryText: LR_R2_APPLY_STAGE_INFO, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + id: 'apply_r2', + questionTitle: + 'Would you like to apply to be the leader of your group for this stage?', + options: [ + {id: 'yes', imageId: '', text: 'Yes'}, + {id: 'no', imageId: '', text: 'No'}, + ], + }), + createScaleSurveyQuestion({ + id: 'wtl_r2', + questionTitle: + 'How much do you want to be the leader (0 = not at all, 10 = very much)?', + lowerText: 'Not at all', + upperText: 'Very much', + lowerValue: 0, + upperValue: 10, + }), + ], +}); + +const LR_R2_BELIEF_CANDIDATES = createSurveyStage({ + id: 'r2_belief_candidate', + name: 'Part 2b - Survey', + descriptions: createStageTextConfig({ + primaryText: + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.', + }), + questions: [ + createScaleSurveyQuestion({ + id: 'r2_belief_candidate', + questionTitle: + 'How many members of the group applied to the role (excluding you), according to you?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 5, + }), + ], +}); + +export const LR_R2_INFO_INSTRUCTIONS_GROUP = + 'You will complete the group task, while the computer gathers information to determine who the selected leader is. ' + + "\n\n Recall that for this task only the leader's answers count." + + "\n\n ⚠️ Since you could potentially be the leader without knowing it yet, keep in mind that your performance might determine everyone's payoff for this part." + + '\n\nAfter the task ends, you will be informed of whether or not you were the leader for this round.' + + '\n\n Remember also that in the extreme case where no one applied, you could be selected as the leader. As a result, try to perform to the best of your ability in the following task, regardless of your application status.'; + +export const LR_R2_INSTRUCTIONS_GROUP = createLRRankingStage({ + id: 'r2_instructions', + name: 'Part 2b - Task Instructions', + descriptions: createStageTextConfig({ + primaryText: LR_R2_INFO_INSTRUCTIONS_GROUP, + }), + progress: createStageProgressConfig({waitForAllParticipants: true}), +}); + +// Instructions + +//========================================================== +// Group Task +//========================================================== + +//Task +export const LR_R2_GROUP_TASK_ID = 'grouptask2'; + +export const LR_R2_GROUP_TASK_STAGE = createSurveyStage({ + id: LR_R2_GROUP_TASK_ID, + name: 'Part 2b - Group task', + descriptions: createStageTextConfig({infoText: SD_SCENARIO_REMINDER}), + questions: createSDSurvivalSurvey(SD_LEADER_ITEMS_MULTIPLE_CHOICE_QUESTIONS), +}); + +//========================================================== +// Feedback Stage +//========================================================== + +export const LR_R2_STATUS_FEEDBACK_STAGE = createRevealStage({ + id: 'r2_status_feedback', + name: 'Part 2b β€” Leader Selection Result', + descriptions: createStageTextConfig({ + primaryText: 'Results of leader selection for this round.', + infoText: 'Please wait until everyone in your group has reached this page.', + }), + progress: createStageProgressConfig({ + showParticipantProgress: false, + waitForAllParticipants: false, + }), + items: [ + createRankingRevealItem({ + id: 'r2_instructions', + customRender: 'leaderStatus', // 🧩 triggers your custom reveal + revealAudience: RevealAudience.CURRENT_PARTICIPANT, + }), + ], +}); + +//========================================================== +// Attribution beliefs Stage +//========================================================== + +const LR_R2_BELIEF_STAGE = createSurveyStage({ + id: 'r2_belief', + name: 'Part 2b - Survey', + questions: [ + createMultipleChoiceSurveyQuestion({ + id: 'belief_binary_rule_r2', + questionTitle: + 'Do you think the selected leader had the highest performance score in your group? (If you correctly answer this question you can get an extra Β£0.50 bonus)', + options: [ + {id: 'yes', imageId: '', text: 'Yes'}, + {id: 'no', imageId: '', text: 'No'}, + ], + }), + createScaleSurveyQuestion({ + id: 'belief_rule_r2', + questionTitle: + 'Thinking about the result of this round β€” whether you were selected as leader, not selected, or chose not to apply. To what extent do you think the outcome of the leadership selection was due to performance versus external circumstances/luck?', + lowerText: 'Entirely due to external factors (luck, randomness)', + upperText: 'Entirely due to performance', + lowerValue: 0, + upperValue: 100, + useSlider: true, + }), + ], +}); + +const LR_R2_BELIEF_CANDIDATES_UPDATE = createSurveyStage({ + id: 'r2_belief_candidate_update', + name: 'Part 2b - Survey', + descriptions: createStageTextConfig({ + primaryText: + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.', + }), + questions: [ + createScaleSurveyQuestion({ + id: 'r2_belief_candidate_update', + questionTitle: + 'We ask you to answer this question again: How many members of the group applied to the role (excluding you), according to you?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 5, + }), + ], +}); +export const LR_ROUND2_CONF_INFO_v2 = + 'You can get an extra bonus for that question. At the end of the experiment, one of the estimation questions (from this or a similar section) will be randomly selected. If your estimation is correct, you will receive an Β£0.50 extra bonus.'; + +const LR_ROUND2_CONFIDENCE_v2 = createSurveyStage({ + name: 'Part 2b - Performance Estimation', + descriptions: createStageTextConfig({ + primaryText: LR_ROUND2_CONF_INFO_v2, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + questionTitle: + 'We would like you to guess how well you did in the previous task compared to other members of your group. Please select the option that best reflects your belief about your rank:', + options: [ + { + id: '1', + imageId: '', + text: 'I would be ranked 1st (the best performer in my group)', + }, + { + id: '2', + imageId: '', + text: 'I would be ranked 2nd', + }, + { + id: '3', + imageId: '', + text: 'I would be ranked 3rd', + }, + { + id: '4', + imageId: '', + text: 'I would be ranked 4th', + }, + { + id: '5', + imageId: '', + text: 'I would be ranked 5th', + }, + { + id: '6', + imageId: '', + text: 'I would be ranked 6th (the worst performer in my group)', + }, + ], + }), + createScaleSurveyQuestion({ + id: 'conf_round2', + questionTitle: + 'How many of the 5 questions from the previous task do you think you answered correctly?', + lowerText: '', + upperText: '', + lowerValue: 0, + upperValue: 5, + }), + ], + progress: createStageProgressConfig({ + showParticipantProgress: false, + }), +}); + +//========================================================== +//========================================================== +//========================================================== +// Group Stage - Round 3 (hypothetical) +//========================================================== +//========================================================== + +//========================================================== +// Info stage +//========================================================== +export const LR_R3_INSTRUCTIONS_INFO = [ + `Imagine you could complete the group task one last time with a new leader (same task, same role, same selection process).`, +]; + +export const LR_R3_INSTRUCTIONS = createInfoStage({ + name: 'Round 3 - Instructions', + infoLines: LR_R3_INSTRUCTIONS_INFO, +}); + +//========================================================== +// Survey stage +//========================================================== + +const LR_R3_APPLY_STAGE = createSurveyStage({ + id: 'r3_apply', + name: 'Part 2c', + descriptions: createStageTextConfig({ + primaryText: `Imagine you could complete the group task one last time with a new leader (same task, same role, same selection process).`, + }), + questions: [ + createMultipleChoiceSurveyQuestion({ + id: 'apply_r3', + questionTitle: + 'Would you like to apply to become the leader in this hypothetical round?', + options: [ + {id: 'yes', imageId: '', text: 'Yes'}, + {id: 'no', imageId: '', text: 'No'}, + ], + }), + createScaleSurveyQuestion({ + id: 'wtl_r3', + questionTitle: + 'How much would you like to be the leader (0 = not at all, 10 = very much)?', + lowerText: 'Not at all', + upperText: 'Very much', + lowerValue: 0, + upperValue: 10, + }), + ], +}); + +//========================================================== +//========================================================== +//========================================================== +// Final survey stage - +//========================================================== +//========================================================== +//========================================================== +const LR_SURVEY_PRIMARY = `Please rate how much you agree with the following statements. (1= strongly disagree, 7 = strongly agree)`; +export const LR_SURVEYFULL_QUESTION: SurveyQuestion[] = [ + createScaleSurveyQuestion({ + id: 'g', + questionTitle: + 'Even the things in life I can’t control tend to go my way because I’m lucky ', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'h', + questionTitle: 'I consistently have good luck', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'i', + questionTitle: 'I often feel like it’s my lucky day', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'j', + questionTitle: 'Luck works in my favour', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'k', + questionTitle: ' I consider myself to be a lucky person', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'q', + questionTitle: 'Some people are consistently lucky, and others are unlucky', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 's', + questionTitle: + 'There is such a thing as good luck that favors some people, but not others', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'u', + questionTitle: 'Luck plays an important part in everyone’s life', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'v', + questionTitle: 'I believe in Luck', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + + createScaleSurveyQuestion({ + id: '2z', + questionTitle: + 'I would rather do something at which I feel confident and relaxed than something which is challenging and difficult', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2a', + questionTitle: + 'When a group I belong to plans an activity, I would rather direct it myself than just help out and have someone else organize it', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2b', + questionTitle: + ' I would rather learn easy, fun games than difficult, thought games', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2c', + questionTitle: + 'If I am not good at something, I would rather keep struggling to master it than move on to something I may be good at', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2d', + questionTitle: 'Once I undertake a task, I persist', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2e', + questionTitle: + 'I prefer to work in situations that require a high level of skill', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2f', + questionTitle: + ' I more often attempt tasks that I am not sure I can do than tasks that I believe I can do', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: '2g', + questionTitle: 'I like to be busy all the time', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), +]; +export const LR_SURVEYFULL__QUESTIOND: SurveyQuestion[] = [ + createScaleSurveyQuestion({ + id: 'a', + questionTitle: 'I consider myself to be an unlucky person', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'b', + questionTitle: 'I consistently have bad luck', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'c', + questionTitle: + 'Even the things in life I can control in life don’t go my way because I am unlucky', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'd', + questionTitle: 'Luck works against me', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'e', + questionTitle: 'I often feel like it’s my unlucky day', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'f', + questionTitle: + 'I mind leaving things to chance because I am an unlucky person', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'g', + questionTitle: + 'Even the things in life I can’t control tend to go my way because I’m lucky', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'h', + questionTitle: 'I consistently have good luck', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'i', + questionTitle: 'I often feel like it’s my lucky day', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'j', + questionTitle: 'Luck works in my favor', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'k', + questionTitle: ' I consider myself to be a lucky person', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'l', + questionTitle: + 'I don’t mind leaving things to chance because I’m a lucky person ', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'm', + questionTitle: + ' It’s a mistake to base any decisions on how unlucky you feel', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'n', + questionTitle: 'Being unlucky is nothing more than random', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'o', + questionTitle: + ' It’s a mistake to base any decisions on how lucky you feel', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'p', + questionTitle: 'Being lucky is nothing more than random', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'q', + questionTitle: 'Some people are consistently lucky, and others are unlucky', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'r', + questionTitle: + 'Some people are consistently unlucky, and others are lucky ', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 's', + questionTitle: + 'There is such a thing as good luck that favours some people, but not others', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 't', + questionTitle: + 'There is such a thing as bad luck that affects some people more than others', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'u', + questionTitle: 'Luck plays an important part in everyone’s life', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), + createScaleSurveyQuestion({ + id: 'v', + questionTitle: 'I believe in Luck', + lowerText: 'Strongly disagree', + lowerValue: 1, + upperText: 'Strongly agree', + upperValue: 7, + }), +]; + +const LR_SURVEY_STAGE_PRIMARY = createSurveyStage({ + name: 'Final survey (1/2)', + descriptions: createStageTextConfig({ + primaryText: LR_SURVEY_PRIMARY, + }), + questions: LR_SURVEYFULL_QUESTION, +}); + +const LR_FINAL_SURVEY = `Please answer the following questions.`; + +export const LR_FINAL_SURVEY_QUESTION: SurveyQuestion[] = [ + createTextSurveyQuestion({ + id: '0', + questionTitle: + 'During the experiment, you were asked whether wanted to apply to become the group leader. Can you explain the reasons behind your choice? Please provide specific and concrete arguments for your choices.', + }), + createMultipleChoiceSurveyQuestion({ + id: '1', + questionTitle: + 'After seeing the outcome of the leader’s selection in Part 2b (accepted vs. rejected if you applied, or just not selected if you did not apply), did you choose to reapply in the next Round? ', + options: [ + {id: 'yes', imageId: '', text: 'Yes'}, + {id: 'no', imageId: '', text: 'No'}, + ], + }), + createTextSurveyQuestion({ + id: '1b', + questionTitle: 'Why? (Explain your answer to the previous question)', + }), + createTextSurveyQuestion({ + id: '2', + questionTitle: + 'Consider the survival task performed in this study. Did you have any prior knowledge or experience in the domain of survival that could have helped you solve the task? If yes, please share specific memories or experiences that explain your answer.', + }), + + createTextSurveyQuestion({ + id: '3', + questionTitle: + 'Do you have previous experience of leadership activities? If yes, please share specific memories or experiences that explain your answer.', + }), + createScaleSurveyQuestion({ + id: '4', + questionTitle: + 'In general, how willing or unwilling are you to take risks on a scale from 0 to 10?', + lowerText: 'Completely unwilling to take risks', + lowerValue: 0, + upperText: 'Very willing to take risks', + upperValue: 10, + }), + createScaleSurveyQuestion({ + id: '5', + questionTitle: + 'Consider the survival task performed in this study. On average, do you think that men are better at such tasks, that men and women are equally good, or that women are better?', + lowerText: 'Men are better', + lowerValue: 0, + upperText: 'Women are better', + upperValue: 10, + }), + createScaleSurveyQuestion({ + id: '6', + questionTitle: + 'On average, do you think that men are better leaders, that men and women are equally good leaders, or that women are better leaders.', + lowerText: 'Men are better', + lowerValue: 0, + upperText: 'Women are better', + upperValue: 10, + }), + createTextSurveyQuestion({ + id: '7', + questionTitle: + 'Would you like to share any more context about your reasoning in this task?', + }), + createTextSurveyQuestion({ + id: '8', + questionTitle: 'Would you like to share any feedback about the task?', + }), +]; + +const LR_FINAL_SURVEY_STAGE = createSurveyStage({ + name: 'Final survey (2/2)', + descriptions: createStageTextConfig({ + primaryText: LR_FINAL_SURVEY, + }), + questions: LR_FINAL_SURVEY_QUESTION, +}); + +//========================================================== +// * PAYOUT STAGE +//========================================================== + +//========================================================== +//========================================================== +//========================================================== +// FINAL FEEDBACK + SURVEY + PAYOUT +//========================================================== +//========================================================== +//========================================================== +export const LR_FEEDBACK_STAGE_PRIMARY = 'Here are the results from the task.'; +export const LR_FEEDBACK_STAGE_INFO = `An explanation of the results can be found [here](https://raw.githubusercontent.com/PAIR-code/deliberate-lab/main/frontend/src/assets/lost_at_sea/task_answers.pdf).`; + +export const r1_instructions = 'r1_instructions'; +export const r2_instructions = 'r2_instructions'; + +export const LR_FEEDBACK_STAGE = createRevealStage({ + name: 'Results reveal - Part 2a (Group Stage)', + descriptions: createStageTextConfig({ + infoText: LR_FEEDBACK_STAGE_INFO, + primaryText: LR_FEEDBACK_STAGE_PRIMARY, + }), + items: [ + createRankingRevealItem({ + id: r1_instructions, + }), + createSurveyRevealItem({ + id: LR_R1_GROUP_TASK_ID, + revealAudience: RevealAudience.ALL_PARTICIPANTS, + revealScorableOnly: true, + }), + ], +}); + +export const LR_FEEDBACK_STAGE_BIS = createRevealStage({ + name: 'Results reveal - Part 2b (Group Stage)', + descriptions: createStageTextConfig({ + infoText: LR_FEEDBACK_STAGE_INFO, + primaryText: LR_FEEDBACK_STAGE_PRIMARY, + }), + items: [ + createRankingRevealItem({ + id: r2_instructions, + }), + createSurveyRevealItem({ + id: LR_R2_GROUP_TASK_ID, + revealAudience: RevealAudience.ALL_PARTICIPANTS, + revealScorableOnly: true, + }), + ], + progress: createStageProgressConfig({ + showParticipantProgress: false, + waitForAllParticipants: false, + }), +}); + +// **************************************************************************** +// Payout Breakdown info stage +// **************************************************************************** +export const LR_PAYMENT_PART_1_DESCRIPTION = `If Part 1a or Part 1b is selected, the bonus is determined by randomly selecting one question from this part. If your answer to this question is correct, you earn Β£2; otherwise, you earn Β£0.`; +export const LR_PAYMENT_PART_1A_DESCRIPTION = `On top of the fixed fee, your payment includes a bonus from one randomly selected Part of the experiment. Part 1a was selected to determine your bonus. The bonus is determined by randomly selecting one question from this part. If your answer to this question is correct, you earn Β£2; otherwise, you earn Β£0.`; + +export const LR_PAYMENT_PART_1B_DESCRIPTION = `On top of the fixed fee, your payment includes a bonus from one randomly selected Part of the experiment. Part 1b was selected to determine your bonus. The bonus is determined by randomly selecting one question from this part. If your answer to this question is correct, you earn Β£2; otherwise, you earn Β£0.`; + +export const LR_PAYMENT_PARTS_2_AND_3_DESCRIPTION = `If Part 2a or Part 2b is selected to determine your bonus, one question is randomly chosen, with only the leader's answer counting. You earn Β£2 if the leader's answer is correct, and Β£0 otherwise.`; + +export const LR_PAYMENT_PART_2_DESCRIPTION = `On top of the fixed fee, your payment includes a bonus from one randomly selected Part of the experiment. Part 2a was selected to determine your bonus. One question is randomly chosen from this part, with only the leader's answer counting. You earn Β£2 if the leader's answer is correct, and Β£0 otherwise.`; + +export const LR_PAYMENT_PART_3_DESCRIPTION = `On top of the fixed fee, your payment includes a bonus from one randomly selected Part of the experiment. Part 2b was selected to determine your bonus. One question is randomly chosen from this part with only the leader's answer counting. You earn Β£2 if the leader's answer is correct, and Β£0 otherwise.`; + +export const LR_PAYMENT_INSTRUCTIONS = [ + 'Your payment includes a fixed fee of Β£5 and a Β£2 bonus from one randomly selected Part of the experiment.', + '## If Part 1a or 1b is selected:', + LR_PAYMENT_PART_1_DESCRIPTION, + '\n ## If Parts 2a or 2b is selected:', + LR_PAYMENT_PARTS_2_AND_3_DESCRIPTION, + 'On the next page, you will see which question was selected for the Β£2 bonus.', + //`* If Part 2 is selected: ${LR_PAYMENT_PART_2_DESCRIPTION}`, + //`* If Part 3 is selected: ${LR_PAYMENT_PART_3_DESCRIPTION}`, + 'In addition, you answered several estimation questions throughout the experiment; one of these will be randomly selected to determine an extra Β£0.50 bonus.', + 'If you served as the leader at any point, you will also receive Β£0.50 for each round in which you were the leader.', + 'These additional bonuses will be calculated and paid on Prolific within the next 24–48 hours. If you would like more information about how they were computed, please contact the researcher via Prolific.', +]; + +export const LR_PAYMENT_INSTRUCTIONS_ALL = [...LR_PAYMENT_INSTRUCTIONS]; + +const LR_PAYOUT_INFO_STAGE = createInfoStage({ + name: 'Payment breakdown', + infoLines: LR_PAYMENT_INSTRUCTIONS_ALL, +}); + +// **************************************************************************** +// Payout stage +// **************************************************************************** + +export function createLRPayoutItems() { + const RANDOM_SELECTION_ID = 'lr-part'; + + const part1a = createSurveyPayoutItem({ + id: 'payout-part-1a', + randomSelectionId: RANDOM_SELECTION_ID, + name: 'Part 1a selected', + description: LR_PAYMENT_PART_1A_DESCRIPTION, + stageId: LR_BASELINE_TASK1_ID, + baseCurrencyAmount: 5, + }); + const part1aQuestion = choice(LAS_INDIVIDUAL_ITEMS_MULTIPLE_CHOICE_QUESTIONS); + part1a.questionMap[part1aQuestion.id] = 2; + + // Only one payout item with this ID will be selected (at random) + // for each participant + + const part1b = createSurveyPayoutItem({ + id: 'payout-part-1b', + randomSelectionId: RANDOM_SELECTION_ID, + name: 'Parts 1b selected', + description: LR_PAYMENT_PART_1B_DESCRIPTION, + stageId: LR_BASELINE_TASK2_ID, + baseCurrencyAmount: 5, + }); + const part1bQuestion = choice(SD_INDIVIDUAL_ITEMS_MULTIPLE_CHOICE_QUESTIONS); + part1b.questionMap[part1bQuestion.id] = 2; + + const part2 = createSurveyPayoutItem({ + id: 'payout-part-2', + randomSelectionId: RANDOM_SELECTION_ID, + name: 'Parts 2a selected', + description: [LR_PAYMENT_PART_2_DESCRIPTION].join('\n\n'), + stageId: LR_R1_GROUP_TASK_ID, + baseCurrencyAmount: 5, + }); + const part2Question = choice(LAS_LEADER_ITEMS_MULTIPLE_CHOICE_QUESTIONS); + part2.questionMap[part2Question.id] = 2; + + const part3 = createSurveyPayoutItem({ + id: 'payout-part-3', + randomSelectionId: RANDOM_SELECTION_ID, + name: 'Parts 2b selected', + description: [LR_PAYMENT_PART_3_DESCRIPTION].join('\n\n'), + stageId: LR_R2_GROUP_TASK_ID, + baseCurrencyAmount: 5, + }); + const part3Question = choice(SD_LEADER_ITEMS_MULTIPLE_CHOICE_QUESTIONS); + part3.questionMap[part3Question.id] = 2; + + return [part1a, part1b, part2, part3]; +} + +const LR_PAYOUT_STAGE = createPayoutStage({ + id: 'payout', + currency: PayoutCurrency.GBP, + //descriptions: createStageTextConfig({ + // infoText: LR_PAYMENT_INSTRUCTIONS.join('\n'), + //}), + payoutItems: createLRPayoutItems(), +}); diff --git a/functions/src/stages/leadership_rejection.utils.ts b/functions/src/stages/leadership_rejection.utils.ts new file mode 100644 index 000000000..e417828b8 --- /dev/null +++ b/functions/src/stages/leadership_rejection.utils.ts @@ -0,0 +1,225 @@ +// functions/src/stages/leadership.utils.ts + +// Adjust the import path depending on where your frontend templates live +// (You only need this if the backend doesn’t already have access to those functions.) + +export interface LeaderSelectionInput { + publicId: string; // participant.publicId + performanceScore: number; // baseline1_correct + baseline2_correct + applied: boolean; // did they apply this round? +} + +export interface LeaderSelectionResult { + winnerId: string; + participantStatusMap: Record; + debug: { + seed: number; + roll: number; + rankedCandidates: string[]; + probabilities: Record; + candidatePoolAppliedOnly: boolean; + }; +} + +/** + * Compute geometric tail weights for candidates ranked by performance. + * Input: candidates sorted descending by performance. + */ +function computeWeights(candidateIds: string[]): Record { + const k = candidateIds.length; + if (k === 1) { + return {[candidateIds[0]]: 1.0}; + } + + // k > 1 + const topId = candidateIds[0]; + const weights: Record = {}; + const P1 = 0.6; + weights[topId] = P1; + + // tail gets 0.40 + const tailMass = 0.4; + const rho = Math.pow(1 / 3, 1 / (k - 1)); // ρ = (1/3)^(1/(k-1)) + + // for rank r = 2..k + for (let r = 2; r <= k; r++) { + const id = candidateIds[r - 1]; + //const pr = P1 * (1 - rho) * Math.pow(rho, r-2); + // careful: we said "spread 0.40", but formula uses tailMass, not P1: + // your spec says: + // - P1 = 0.60 + // - tail 0.40 distributed geometrically + // so use tailMass here instead of P1. + const pr = tailMass * (1 - rho) * Math.pow(rho, r - 2); + weights[id] = pr; + } + + // (Optional) small normalize if rounding error bugs you: + const sum = Object.values(weights).reduce((a, b) => a + b, 0); + Object.keys(weights).forEach((id) => { + weights[id] = weights[id] / sum; + }); + + return weights; +} + +/** + * Draws a winner given a prob distribution. + * We also allow passing in a seed for reproducibility, but here we just Math.random(). + * You can later replace Math.random() with a seeded PRNG from the experiment ID. + */ +function drawWeightedWinner(weights: Record): { + winnerId: string; + roll: number; +} { + const roll = Math.random(); + let acc = 0; + for (const [id, p] of Object.entries(weights)) { + acc += p; + if (roll <= acc) { + return {winnerId: id, roll}; + } + } + // fallback in case of float precision + const lastId = Object.keys(weights)[Object.keys(weights).length - 1]; + return {winnerId: lastId, roll}; +} + +/** + * Main selection logic for one round. + */ + +export function runLeaderLottery( + participants: LeaderSelectionInput[], +): LeaderSelectionResult { + console.debug('──────────────────────────────────────────────'); + console.debug('[LR][lottery] START Leader Lottery'); + console.debug('[LR][lottery] Raw inputs:'); + for (const p of participants) { + console.debug( + ` - ${p.publicId} | applied=${p.applied} | score=${p.performanceScore}`, + ); + } + console.debug('──────────────────────────────────────────────'); + // 1. Who applied? + const applicants = participants.filter((p) => p.applied); + + const hasApplicants = applicants.length > 0; + console.debug( + '[LR][lottery] Applicants:', + applicants.map((a) => a.publicId), + ); + console.debug('[LR][lottery] hasApplicants =', hasApplicants); + + // 2. Candidate pool + const candidatePool = hasApplicants ? applicants : participants; + console.debug( + '[LR][lottery] Candidate pool:', + candidatePool.map((p) => p.publicId), + ); + + // 3. Rank candidate pool by performanceScore desc, tiebreak deterministic + const ranked = [...candidatePool].sort((a, b) => { + if (b.performanceScore !== a.performanceScore) { + return b.performanceScore - a.performanceScore; + } + // deterministic tie-break: alphabetical on publicId + return a.publicId.localeCompare(b.publicId); + }); + + const rankedIds = ranked.map((p) => p.publicId); + console.debug('[LR][lottery] Ranked order (best β†’ worst):', rankedIds); + + // 4. Weights + const weights = computeWeights(rankedIds); + console.debug('[LR][lottery] Weights:', weights); + + // 5. Draw winner + const {winnerId, roll} = drawWeightedWinner(weights); + + // 6. Assign statuses + const participantStatusMap: Record = {}; + if (hasApplicants) { + // case β‰₯1 applied: + // applied & selected β†’ candidate_accepted + // applied & not β†’ candidate_rejected + // did not apply β†’ non_candidate (+ hypothetical check) + console.debug('[LR][lottery] Assigning statuses because applicants > 0'); + + const winner = winnerId; + + // first assign accepted/rejected or non_candidate + for (const p of participants) { + if (p.applied) { + participantStatusMap[p.publicId] = + p.publicId === winner ? 'candidate_accepted' : 'candidate_rejected'; + } else { + participantStatusMap[p.publicId] = 'non_candidate'; + } + } + + // hypothetical for each non-candidate: + for (const p of participants) { + if (!p.applied) { + // pretend they had applied too + const hypoPool = [...applicants, p]; + + // rank hypo pool + const hypoRanked = [...hypoPool].sort((a, b) => { + if (b.performanceScore !== a.performanceScore) { + return b.performanceScore - a.performanceScore; + } + return a.publicId.localeCompare(b.publicId); + }); + + const hypoIds = hypoRanked.map((x) => x.publicId); + const hypoWeights = computeWeights(hypoIds); + + // Instead of randomizing again, we replicate same draw roll + // to make hypo deterministic w.r.t. this round. + // We'll reuse the SAME `roll` so it's "would you have won THIS draw?" + let acc = 0; + let hypoWinner = hypoIds[hypoIds.length - 1]; + for (const id of hypoIds) { + acc += hypoWeights[id]; + if (roll <= acc) { + hypoWinner = id; + break; + } + } + + if (hypoWinner === p.publicId) { + participantStatusMap[p.publicId] = 'non_candidate_hypo_selected'; + } else { + participantStatusMap[p.publicId] = 'non_candidate_hypo_rejected'; + } + } + } + } else { + // case 0 applied -> everyone is a "candidate" + // winner β†’ non_candidate_accepted + // others β†’ non_candidate_rejected + console.debug( + '[LR][lottery] NO applicants β†’ everyone considered non_candidate_*', + ); + + for (const p of participants) { + participantStatusMap[p.publicId] = + p.publicId === winnerId + ? 'non_candidate_accepted' + : 'non_candidate_rejected'; + } + } + + return { + winnerId, + participantStatusMap, + debug: { + seed: 123, + roll, + rankedCandidates: rankedIds, + probabilities: weights, + candidatePoolAppliedOnly: hasApplicants, + }, + }; +} diff --git a/functions/src/stages/ranking.utils.ts b/functions/src/stages/ranking.utils.ts index 97adedfdd..8b6f3a8bb 100644 --- a/functions/src/stages/ranking.utils.ts +++ b/functions/src/stages/ranking.utils.ts @@ -2,15 +2,27 @@ import { ParticipantProfileExtended, RankingStageConfig, RankingStageParticipantAnswer, - RankingStagePublicData, StageKind, SurveyStagePublicData, + RankingStagePublicData, + LRRankingStagePublicData, filterRankingsByCandidates, getCondorcetElectionWinner, getRankingCandidatesFromWTL, + getApplicationsFromLRApplyStage, + isLRRankingStagePublicData, LAS_WTL_STAGE_ID, + // NEW imports from utils + getBaselineScoresFromStage, + //getCorrectLASAnswer_mini, + //getCorrectSDAnswer_mini, } from '@deliberation-lab/utils'; +import { + runLeaderLottery, + LeaderSelectionInput, +} from './leadership_rejection.utils'; + import {app} from '../app'; /** Update ranking stage public data to include participant private data. */ @@ -31,43 +43,210 @@ export async function addParticipantAnswerToRankingStagePublicData( .collection('publicStageData') .doc(stage.id); - // For hardcoded WTL stage in LAS game only - const wtlDoc = app - .firestore() - .collection('experiments') - .doc(experimentId) - .collection('cohorts') - .doc(participant.currentCohortId) - .collection('publicStageData') - .doc(LAS_WTL_STAGE_ID); - // Update public stage data (current participant rankings, current winner) - const publicStageData = ( - await publicDocument.get() - ).data() as RankingStagePublicData; - publicStageData.participantAnswerMap[participant.publicId] = - answer.rankingList; - - // Calculate rankings - let participantAnswerMap = publicStageData.participantAnswerMap; - - // If experiment has hardcoded WTL stage (for LAS game), use the WTL - // stage/question IDs to only consider top ranking participants - const wtlResponse = await wtlDoc.get(); - if (wtlResponse.exists) { - const wtlData = wtlResponse.data() as SurveyStagePublicData; - - if (wtlData?.kind === StageKind.SURVEY) { - const candidateList = getRankingCandidatesFromWTL(wtlData); - participantAnswerMap = filterRankingsByCandidates( - participantAnswerMap, - candidateList, + // Fetch public stage data + const publicStageData = (await publicDocument.get()).data() as + | RankingStagePublicData + | LRRankingStagePublicData; + + console.debug( + '[LR] addParticipantAnswerToRankingStagePublicData: publicStageData=', + publicStageData, + ); + + // ───────────────────────────────────────────────────────────── + // 🧩 Leadership Rejection logic (LR template) + // ───────────────────────────────────────────────────────────── + if (isLRRankingStagePublicData(publicStageData)) { + // We only trigger the leader lottery when reaching the special + // ranking "instruction" stages for each round. + //if (stage.id === 'r1_instructions' || stage.id === 'r2_instructions') { + if ( + (stage.id === 'r1_instructions' || stage.id === 'r2_instructions') && + Object.keys(publicStageData.participantAnswerMap || {}).length === 0 && + !publicStageData.winnerId // ensure lottery not run yet + ) { + console.debug(`[LR] Triggering leader lottery at ${stage.id}`); + + // Determine which "apply" stage we are in (Round 1 or Round 2) + const applyStageId = stage.id.startsWith('r1_') + ? 'r1_apply' + : 'r2_apply'; + + // ------------------------------------------------------------------- + // 1. Read applications from the cohort's publicStageData for applyStageId + // (this is analogous to how WTL is handled in the LAS template) + // ------------------------------------------------------------------- + const applyDoc = await app + .firestore() + .collection('experiments') + .doc(experimentId) + .collection('cohorts') + .doc(participant.currentCohortId) + .collection('publicStageData') + .doc(applyStageId) + .get(); + + let applications: Record = {}; + + if (applyDoc.exists) { + const applyData = applyDoc.data() as SurveyStagePublicData; + console.debug( + `[LR] Found applyStage public data for ${applyStageId}:`, + JSON.stringify(applyData), + ); + + if (applyData.kind === StageKind.SURVEY) { + applications = getApplicationsFromLRApplyStage(applyData); + console.debug( + '[LR] Applications map from apply stage:', + applications, + ); + } else { + console.debug( + `[LR] applyStageId=${applyStageId} has kind=${applyData.kind}, expected SURVEY`, + ); + } + } else { + console.debug( + `[LR] No applyStage public data found for ${applyStageId}. All applied=false.`, + ); + } + + // ------------------------------------------------------------------- + // 2. Fetch all participants in this cohort + // ------------------------------------------------------------------- + const cohortParticipantsSnap = await app + .firestore() + .collection('experiments') + .doc(experimentId) + .collection('participants') + .where('currentCohortId', '==', participant.currentCohortId) + .get(); + + // ------------------------------------------------------------------- + // 3. Helper: compute performance score (baseline1 + baseline2 correct) + // **** + // ------------------------------------------------------------------- + async function getPerformanceScore(pPublicId: string): Promise { + let totalCorrect = 0; + + // Load baseline1 publicStageData (LAS) + const baseline1Doc = await app + .firestore() + .collection('experiments') + .doc(experimentId) + .collection('cohorts') + .doc(participant.currentCohortId) + .collection('publicStageData') + .doc('baseline1') + .get(); + + if (baseline1Doc.exists) { + const baseline1Data = baseline1Doc.data() as SurveyStagePublicData; + const scores1 = getBaselineScoresFromStage(baseline1Data, 'LAS'); + totalCorrect += scores1[pPublicId] ?? 0; + } + + // Load baseline2 publicStageData (SD) + const baseline2Doc = await app + .firestore() + .collection('experiments') + .doc(experimentId) + .collection('cohorts') + .doc(participant.currentCohortId) + .collection('publicStageData') + .doc('baseline2') + .get(); + + if (baseline2Doc.exists) { + const baseline2Data = baseline2Doc.data() as SurveyStagePublicData; + const scores2 = getBaselineScoresFromStage(baseline2Data, 'SD'); + totalCorrect += scores2[pPublicId] ?? 0; + } + + return totalCorrect; + } + + // ------------------------------------------------------------------- + // 4. Build lottery inputs: one entry per participant in cohort + // ------------------------------------------------------------------- + const leaderInputs: LeaderSelectionInput[] = []; + + for (const doc of cohortParticipantsSnap.docs) { + const pData = doc.data() as ParticipantProfileExtended; + const pId = pData.publicId; + + const applied = !!applications[pId]; + const performanceScore = await getPerformanceScore(pId); + + leaderInputs.push({ + publicId: pId, + performanceScore, + applied, + }); + } + + console.debug('[LR] Inputs passed into lottery:'); + for (const inp of leaderInputs) { + console.debug( + `[LR][input] publicId=${inp.publicId} applied=${inp.applied} score=${inp.performanceScore}`, + ); + } + + // ------------------------------------------------------------------- + // 5. Run lottery and store results in LRRankingStagePublicData + // ------------------------------------------------------------------- + const lotteryResult = runLeaderLottery(leaderInputs); + + publicStageData.winnerId = lotteryResult.winnerId; + publicStageData.leaderStatusMap = lotteryResult.participantStatusMap; + + console.debug( + `[LR] Winner selected at ${stage.id}: ${lotteryResult.winnerId}`, ); } - } - // Calculate winner (not used in frontend if strategy is none) - publicStageData.winnerId = getCondorcetElectionWinner(participantAnswerMap); + // Note: In LR, we do NOT use Condorcet rankings here + } else { + // ───────────────────────────────────────────────────────────── + // 🧩 Default Condorcet logic (e.g. Lost at Sea template) + // ───────────────────────────────────────────────────────────── + + // Store participant's ranking answer + publicStageData.participantAnswerMap[participant.publicId] = + answer.rankingList; + + // LAS-style: filter candidate set using WTL survey stage + const wtlDoc = app + .firestore() + .collection('experiments') + .doc(experimentId) + .collection('cohorts') + .doc(participant.currentCohortId) + .collection('publicStageData') + .doc(LAS_WTL_STAGE_ID); + + let participantAnswerMap = publicStageData.participantAnswerMap; + + const wtlResponse = await wtlDoc.get(); + if (wtlResponse.exists) { + const wtlData = wtlResponse.data() as SurveyStagePublicData; + + if (wtlData?.kind === StageKind.SURVEY) { + const candidateList = getRankingCandidatesFromWTL(wtlData); + participantAnswerMap = filterRankingsByCandidates( + participantAnswerMap, + candidateList, + ); + } + } + + // Compute Condorcet winner among remaining candidates + publicStageData.winnerId = + getCondorcetElectionWinner(participantAnswerMap); + } + // Finally, write back updated public stage data transaction.set(publicDocument, publicStageData); }); } diff --git a/functions/src/triggers/stage.triggers.ts b/functions/src/triggers/stage.triggers.ts index 2b45aed5b..d2474a909 100644 --- a/functions/src/triggers/stage.triggers.ts +++ b/functions/src/triggers/stage.triggers.ts @@ -18,6 +18,7 @@ import {addParticipantAnswerToAssetAllocationStagePublicData} from '../stages/as import {addParticipantAnswerToMultiAssetAllocationStagePublicData} from '../stages/multi_asset_allocation.utils'; import {updateParticipantReadyToEndChat} from '../chat/chat.utils'; +//database.instance /** When participant (private) stage data is updated. */ export const onParticipantStageDataUpdated = onDocumentWritten( { diff --git a/utils/src/shared.ts b/utils/src/shared.ts index 2e75d5e51..d1d8ca9c9 100644 --- a/utils/src/shared.ts +++ b/utils/src/shared.ts @@ -77,6 +77,12 @@ export const LAS_WTL_STAGE_ID = 'wtl'; */ export const LAS_WTL_QUESTION_ID = 'wtl'; +export const r1_apply = 'r1_apply'; +export const apply_r1 = 'apply_r1'; + +export const LR_BASELINE_TASK1_ID = 'baseline1'; + +export const LR_BASELINE_TASK2_ID = 'baseline2'; // ************************************************************************* // // FUNCTIONS // // ************************************************************************* // diff --git a/utils/src/stages/ranking_stage.ts b/utils/src/stages/ranking_stage.ts index 153576133..7d61b6b9e 100644 --- a/utils/src/stages/ranking_stage.ts +++ b/utils/src/stages/ranking_stage.ts @@ -4,6 +4,7 @@ import { BaseStageParticipantAnswer, BaseStagePublicData, StageKind, + StagePublicData, createStageProgressConfig, createStageTextConfig, } from './stage'; @@ -27,6 +28,7 @@ export enum ElectionStrategy { export enum RankingType { ITEMS = 'items', // Item ranking. PARTICIPANTS = 'participants', // Participant ranking. + LR = 'LR', // Leader Rejection } export interface BaseRankingStage extends BaseStageConfig { @@ -40,6 +42,12 @@ export interface ParticipantRankingStage extends BaseRankingStage { enableSelfVoting: boolean; // Whether to allow voting for oneself. } +export interface LRRankingStage extends BaseRankingStage { + rankingType: RankingType.LR; + enableSelfVoting: false; // Whether to allow voting for oneself. + strategy: ElectionStrategy.NONE; +} + export interface RankingItem { id: string; imageId: string; // image URL, or empty if no image provided @@ -51,7 +59,10 @@ export interface ItemRankingStage extends BaseRankingStage { rankingItems: RankingItem[]; } -export type RankingStageConfig = ParticipantRankingStage | ItemRankingStage; +export type RankingStageConfig = + | ParticipantRankingStage + | ItemRankingStage + | LRRankingStage; /** * RankingStageParticipantAnswer. @@ -79,6 +90,10 @@ export interface RankingStagePublicData extends BaseStagePublicData { participantAnswerMap: Record; } +export interface LRRankingStagePublicData extends RankingStagePublicData { + leaderStatusMap?: Record; // e.g. participant -> status +} + // ************************************************************************* // // FUNCTIONS // // ************************************************************************* // @@ -119,6 +134,31 @@ export function createRankingStage( } } +export function createLRRankingStage( + config: Partial = {}, +): RankingStageConfig { + console.debug('[LR] createLRRankingStage'); + const baseStageConfig = { + id: config.id ?? generateId(), + kind: StageKind.RANKING, + name: config.name ?? 'LRRanking', + descriptions: + config.descriptions ?? + createStageTextConfig({ + helpText: ``, + }), + progress: + config.progress ?? + createStageProgressConfig({waitForAllParticipants: true}), + strategy: config.strategy ?? ElectionStrategy.NONE, + }; + + return { + ...baseStageConfig, + rankingType: RankingType.LR, + } as LRRankingStage; // Assert as LRRankingStage +} + /** Create item for ranking. */ export function createRankingItem( config: Partial = {}, @@ -152,3 +192,27 @@ export function createRankingStagePublicData( participantAnswerMap: {}, }; } + +/** Create LR ranking stage public data (Leadership Rejection version). */ +export function createLRRankingStagePublicData( + id: string, // stage ID +): LRRankingStagePublicData { + return { + id, + kind: StageKind.RANKING, + winnerId: '', + participantAnswerMap: {}, + leaderStatusMap: {}, // πŸ‘ˆ critical addition + // debugLeaderSelection: {}, // optional + }; +} + +export function isLRRankingStagePublicData( + obj: StagePublicData, +): obj is LRRankingStagePublicData { + return ( + obj && + obj.kind === StageKind.RANKING && + ('leaderStatusMap' in obj || 'debugLeaderSelection' in obj) + ); +} diff --git a/utils/src/stages/ranking_stage.validation.ts b/utils/src/stages/ranking_stage.validation.ts index 941d3636c..c4a93b5ab 100644 --- a/utils/src/stages/ranking_stage.validation.ts +++ b/utils/src/stages/ranking_stage.validation.ts @@ -58,8 +58,26 @@ export const ParticipantRankingStageConfigData = Type.Object( {$id: 'ParticipantRankingStageConfig', ...strict}, ); +export const LRRankingStageConfigData = Type.Object( + { + id: Type.String({minLength: 1}), + kind: Type.Literal(StageKind.RANKING), + name: Type.String({minLength: 1}), + descriptions: StageTextConfigSchema, + progress: StageProgressConfigSchema, + rankingType: Type.Literal(RankingType.LR), + strategy: Type.Literal(ElectionStrategy.NONE), + enableSelfVoting: Type.Boolean(), + }, + {$id: 'LRRankingStageConfigData', ...strict}, +); + export const RankingStageConfigData = Type.Union( - [ItemRankingStageConfigData, ParticipantRankingStageConfigData], + [ + ItemRankingStageConfigData, + ParticipantRankingStageConfigData, + LRRankingStageConfigData, + ], {$id: 'RankingStageConfig'}, ); diff --git a/utils/src/stages/reveal_stage.ts b/utils/src/stages/reveal_stage.ts index ce3319a77..145c0d594 100644 --- a/utils/src/stages/reveal_stage.ts +++ b/utils/src/stages/reveal_stage.ts @@ -35,6 +35,7 @@ export interface BaseRevealItem { export type RevealItem = | ChipRevealItem | RankingRevealItem + | LRRankingRevealItem | SurveyRevealItem | MultiAssetAllocationRevealItem; @@ -48,6 +49,11 @@ export interface RankingRevealItem extends BaseRevealItem { kind: StageKind.RANKING; } +/** Reveal settings for LR survey stage. */ +export interface LRRankingRevealItem extends RankingRevealItem { + customRender?: string; +} + /** Reveal settings for survey stage. */ export interface SurveyRevealItem extends BaseRevealItem { kind: StageKind.SURVEY; @@ -113,15 +119,26 @@ export function createChipRevealItem( }; } -/** Create ranking reveal item. */ +/** Create ranking reveal item (standard or Leadership Rejection custom). */ export function createRankingRevealItem( - config: Partial = {}, -): RankingRevealItem { - return { + config: Partial | Partial = {}, +): RankingRevealItem | LRRankingRevealItem { + const base = { id: config.id ?? generateId(), kind: StageKind.RANKING, revealAudience: config.revealAudience ?? RevealAudience.CURRENT_PARTICIPANT, }; + + // Leadership Rejection custom reveal + if ('customRender' in config && config.customRender === 'leaderStatus') { + return { + ...base, + customRender: 'leaderStatus', + } as LRRankingRevealItem; + } + + // Default ranking reveal + return base as RankingRevealItem; } /** Create survey reveal item. */ diff --git a/utils/src/stages/reveal_stage.validation.ts b/utils/src/stages/reveal_stage.validation.ts index c4946de67..1dbdf9d43 100644 --- a/utils/src/stages/reveal_stage.validation.ts +++ b/utils/src/stages/reveal_stage.validation.ts @@ -26,6 +26,20 @@ export const RankingRevealItemData = Type.Object( strict, ); +/** LR Ranking reveal item input validation (with customRender). */ +export const LRRankingRevealItemData = Type.Object( + { + id: Type.String({minLength: 1}), + kind: Type.Literal(StageKind.REVEAL), + revealAudience: Type.Union([ + Type.Literal(RevealAudience.CURRENT_PARTICIPANT), + Type.Literal(RevealAudience.ALL_PARTICIPANTS), + ]), + customRender: Type.Optional(Type.String({minLength: 1})), + }, + strict, +); + /** Survey reveal item input validation. */ export const SurveyRevealItemData = Type.Object( { @@ -43,6 +57,7 @@ export const SurveyRevealItemData = Type.Object( /** Reveal item input validation. */ export const RevealItemData = Type.Any([ RankingRevealItemData, + LRRankingRevealItemData, SurveyRevealItemData, ]); diff --git a/utils/src/stages/stage.ts b/utils/src/stages/stage.ts index 95407f893..d5368f2ca 100644 --- a/utils/src/stages/stage.ts +++ b/utils/src/stages/stage.ts @@ -26,7 +26,9 @@ import { RankingStageConfig, RankingStageParticipantAnswer, RankingStagePublicData, + LRRankingStagePublicData, createRankingStagePublicData, + createLRRankingStagePublicData, } from './ranking_stage'; import {InfoStageConfig} from './info_stage'; import {PayoutStageConfig, PayoutStageParticipantAnswer} from './payout_stage'; @@ -188,6 +190,7 @@ export type StagePublicData = | ChipStagePublicData | FlipCardStagePublicData | RankingStagePublicData + | LRRankingStagePublicData | RoleStagePublicData | SalespersonStagePublicData | AssetAllocationStagePublicData @@ -254,7 +257,12 @@ export function createPublicDataFromStageConfigs(stages: StageConfig[]) { publicData.push(createFlipCardStagePublicData(stage.id)); break; case StageKind.RANKING: - publicData.push(createRankingStagePublicData(stage.id)); + if (stage.id.startsWith('r1_') || stage.id.startsWith('r2_')) { + // πŸ‘‡ These are your Leadership Rejection ranking stages + publicData.push(createLRRankingStagePublicData(stage.id)); + } else { + publicData.push(createRankingStagePublicData(stage.id)); + } break; case StageKind.ROLE: publicData.push(createRoleStagePublicData(stage)); diff --git a/utils/src/utils/algebraic.utils.ts b/utils/src/utils/algebraic.utils.ts index be97251f5..3d269ffb1 100644 --- a/utils/src/utils/algebraic.utils.ts +++ b/utils/src/utils/algebraic.utils.ts @@ -204,6 +204,148 @@ export function filterRankingsByCandidates( return participantRankings; } +export function getApplicationsFromLRApplyStage( + applyData: SurveyStagePublicData, +): Record { + const applications: Record = {}; + const answerMap = applyData.participantAnswerMap || {}; + + for (const publicId of Object.keys(answerMap)) { + const participantAnswer = answerMap[publicId]; + if (!participantAnswer || typeof participantAnswer !== 'object') { + applications[publicId] = false; + continue; + } + + const applyAnswer = + participantAnswer['apply_r1'] ?? participantAnswer['apply_r2']; + + if (!applyAnswer) { + applications[publicId] = false; + continue; + } + + // DL multiple choice stores answers in various ways depending on template + let answerValue = undefined; + if (applyAnswer.kind === SurveyQuestionKind.MULTIPLE_CHOICE) { + answerValue = applyAnswer.choiceId; + } else if (applyAnswer.kind === SurveyQuestionKind.CHECK) { + answerValue = applyAnswer.isChecked; + } else if (applyAnswer.kind === SurveyQuestionKind.SCALE) { + answerValue = applyAnswer.value; + } else if (applyAnswer.kind === SurveyQuestionKind.TEXT) { + answerValue = applyAnswer.answer; + } + + applications[publicId] = answerValue === 'yes'; + } + + return applications; +} + +/* ============================================================================ + Backend-safe correct answer helpers for LAS and SD baseline scoring + (duplicate of frontend logic but minimal; no images, no template imports) + ========================================================================== */ + +// ---- LAS survival items ---- +interface LASItemMini { + ranking: number; +} + +const LAS_ITEMS_MINI: Record = { + mirror: {ranking: 1}, + oil: {ranking: 2}, + water: {ranking: 3}, + rations: {ranking: 4}, + sheeting: {ranking: 5}, + chocolate: {ranking: 6}, + fishing: {ranking: 7}, + rope: {ranking: 8}, + cushion: {ranking: 9}, + repellent: {ranking: 10}, + rubbing_alcohol: {ranking: 11}, + radio: {ranking: 12}, + map: {ranking: 13}, + netting: {ranking: 14}, +}; + +function getCorrectLASAnswer_mini(id1: string, id2: string): string { + const a = LAS_ITEMS_MINI[id1]; + const b = LAS_ITEMS_MINI[id2]; + if (!a || !b) return ''; + return a.ranking < b.ranking ? id1 : id2; +} + +// ---- SD survival items ---- +interface SDItemMini { + ranking: number; +} + +const SD_ITEMS_MINI: Record = { + mirror: {ranking: 1}, + raincoat: {ranking: 2}, + water: {ranking: 3}, + flashlight: {ranking: 4}, + parachute: {ranking: 5}, + knife: {ranking: 6}, + pistol: {ranking: 7}, + aid: {ranking: 8}, + book: {ranking: 9}, + salt: {ranking: 10}, +}; + +function getCorrectSDAnswer_mini(id1: string, id2: string): string { + const a = SD_ITEMS_MINI[id1]; + const b = SD_ITEMS_MINI[id2]; + if (!a || !b) return ''; + return a.ranking < b.ranking ? id1 : id2; +} + +export function getBaselineScoresFromStage( + baselineData: SurveyStagePublicData, + type: 'LAS' | 'SD', +): Record { + const scores: Record = {}; + const answerMap = baselineData.participantAnswerMap || {}; + + for (const publicId of Object.keys(answerMap)) { + const participantAnswers = answerMap[publicId]; + if (!participantAnswers || typeof participantAnswers !== 'object') { + scores[publicId] = 0; + continue; + } + + let score = 0; + + for (const qid of Object.keys(participantAnswers)) { + // Look at MultipleChoiceSurveyAnswer answers + const ans = participantAnswers[qid]; + if (!ans || ans.kind !== SurveyQuestionKind.MULTIPLE_CHOICE) continue; + + const chosen = ans.choiceId ?? false; + + if (!chosen) continue; + + const parts = qid.split('-'); + if (parts.length !== 3) continue; + + const [, id1, id2] = parts; + + const correct = + type === 'LAS' + ? getCorrectLASAnswer_mini(id1, id2) + : getCorrectSDAnswer_mini(id1, id2); + + if (chosen === correct) score++; + } + + scores[publicId] = score; + } + + return scores; +} + export function getTimeElapsed( timestamp: {seconds: number; nanoseconds: number}, unit: 's' | 'm' | 'h' | 'd' = 'm',