Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/assets/api/schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,10 @@
"type": "string"
}
]
},
"informalNameStyle": {
"default": false,
"type": "boolean"
}
}
},
Expand Down
24 changes: 13 additions & 11 deletions frontend/src/components/stages/profile_stage_editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,27 @@
height: 100%;
}

.checkbox-wrapper {
.profile-option {
@include common.flex-row-align-center;
gap: common.$spacing-medium;
cursor: pointer;
gap: common.$spacing-small;
}

md-checkbox {
flex-shrink: 0;
.divider {
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}

.profile-options {
@include common.flex-column;
gap: common.$spacing-medium;
.title {
@include typescale.title-medium;
color: var(--md-sys-color-secondary);
}

.profile-option {
.checkbox-wrapper {
@include common.flex-row-align-center;
cursor: pointer;
gap: common.$spacing-small;
}

label {
cursor: pointer;
}
md-checkbox {
flex-shrink: 0;
}
127 changes: 73 additions & 54 deletions frontend/src/components/stages/profile_stage_editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../../pair-components/textarea';
import '@material/web/checkbox/checkbox.js';
import '@material/web/radio/radio';

import {MobxLitElement} from '@adobe/lit-mobx';
Expand All @@ -8,11 +9,7 @@ import {customElement, property} from 'lit/decorators.js';
import {core} from '../../core/core';
import {ExperimentEditor} from '../../services/experiment.editor';

import {
ProfileType,
ProfileStageConfig,
StageKind,
} from '@deliberation-lab/utils';
import {ProfileType, ProfileStageConfig} from '@deliberation-lab/utils';

import {styles} from './profile_stage_editor.scss';

Expand Down Expand Up @@ -65,56 +62,78 @@ export class ProfileStageEditorComponent extends MobxLitElement {
});
};

const isAnonymous =
this.stage.profileType === ProfileType.ANONYMOUS_ANIMAL ||
this.stage.profileType === ProfileType.ANONYMOUS_PARTICIPANT;

return html`
<div class="profile-options">
<div class="profile-option">
<md-radio
name="profile-type"
value="default"
?checked=${this.stage.profileType === ProfileType.DEFAULT}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() => handleProfileTypeChange(ProfileType.DEFAULT)}
></md-radio>
<label>Let participants set their own profiles</label>
</div>
<div class="profile-option">
<md-radio
name="profile-type"
value="default-gendered"
?checked=${this.stage.profileType === ProfileType.DEFAULT_GENDERED}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() =>
handleProfileTypeChange(ProfileType.DEFAULT_GENDERED)}
></md-radio>
<label>Let participants choose from the default gendered set</label>
</div>
<div class="profile-option">
<md-radio
name="profile-type"
value="animal"
?checked=${this.stage.profileType === ProfileType.ANONYMOUS_ANIMAL}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() =>
handleProfileTypeChange(ProfileType.ANONYMOUS_ANIMAL)}
></md-radio>
<label>🐱 Generate anonymous animal-themed profiles</label>
</div>
<div class="profile-option">
<md-radio
name="profile-type"
value="participant"
?checked=${this.stage.profileType ===
ProfileType.ANONYMOUS_PARTICIPANT}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() =>
handleProfileTypeChange(ProfileType.ANONYMOUS_PARTICIPANT)}
></md-radio>
<label
>👤 Generate anonymous participant profiles (Participant 1, 2,
...)</label
>
</div>
</div>
<div class="title">Participant-created profiles</div>
<label class="profile-option">
<md-radio
name="profile-type"
value="default"
?checked=${this.stage.profileType === ProfileType.DEFAULT}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() => handleProfileTypeChange(ProfileType.DEFAULT)}
></md-radio>
Let participants set their own profiles
</label>
<label class="profile-option">
<md-radio
name="profile-type"
value="default-gendered"
?checked=${this.stage.profileType === ProfileType.DEFAULT_GENDERED}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() => handleProfileTypeChange(ProfileType.DEFAULT_GENDERED)}
></md-radio>
Let participants choose from the default gendered set
</label>
<div class="divider"></div>
<div class="title">Anonymous profiles</div>
<label class="profile-option">
<md-radio
name="profile-type"
value="animal"
?checked=${this.stage.profileType === ProfileType.ANONYMOUS_ANIMAL}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() => handleProfileTypeChange(ProfileType.ANONYMOUS_ANIMAL)}
></md-radio>
🐱 Generate anonymous animal-themed profiles
</label>
<label class="profile-option">
<md-radio
name="profile-type"
value="participant"
?checked=${this.stage.profileType ===
ProfileType.ANONYMOUS_PARTICIPANT}
?disabled=${!this.experimentEditor.canEditStages}
@change=${() =>
handleProfileTypeChange(ProfileType.ANONYMOUS_PARTICIPANT)}
></md-radio>
👤 Generate anonymous participant profiles (Participant 1, 2, ...)
</label>
${isAnonymous
? html`
<label class="checkbox-wrapper">
<md-checkbox
touch-target="wrapper"
?checked=${this.stage.informalNameStyle}
?disabled=${!this.experimentEditor.canEditStages}
@click=${() => {
if (!this.stage) return;
this.experimentEditor.updateStage({
...this.stage,
informalNameStyle: !this.stage.informalNameStyle,
});
}}
>
</md-checkbox>
<span
>Use informal name style (e.g., bear123 or participant123)</span
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I were an experimenter encountering this for the first time, I'd be confused about what exactly I was choosing between. If this is informal, then what's formal? I think a radio button form would be better-suited here, so we could put examples on each row.

Though also, I don't have all the context here, but does it really make sense to give experimenters exactly this granularity of control over name formatting? Are experimenters asking to change the name formatting, and if so, why only two options?

>
</label>
`
: nothing}
`;
}
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/shared/templates/quickstart_private_chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const CHAT_STAGE_ID = 'chat';
function getStageConfigs(): StageConfig[] {
const stages: StageConfig[] = [];
stages.push(
createProfileStage(),
createProfileStage({profileType: ProfileType.ANONYMOUS_ANIMAL}),
createPrivateChatStage({
id: CHAT_STAGE_ID,
name: 'Private chat with agent',
Expand Down
11 changes: 9 additions & 2 deletions functions/src/participant.endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,15 @@ export const createParticipant = onCall(async (request) => {
) as ProfileStageConfig | undefined;
const profileType =
profileStage?.profileType || ProfileType.ANONYMOUS_ANIMAL;

setProfile(numParticipants, participantConfig, true, profileType);
const informalNameStyle = profileStage?.informalNameStyle ?? false;

setProfile(
numParticipants,
participantConfig,
true,
profileType,
informalNameStyle,
);
} else {
setProfile(numParticipants, participantConfig, false);
}
Expand Down
1 change: 1 addition & 0 deletions scripts/deliberate_lab/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ class ProfileStageConfig(BaseModel):
descriptions: StageTextConfig
progress: StageProgressConfig
profileType: ProfileType
informalNameStyle: bool | None = False


class Strategy(StrEnum):
Expand Down
97 changes: 43 additions & 54 deletions utils/src/participant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,78 +191,67 @@ export function setProfile(
config: ParticipantProfileExtended,
setAnonymousProfile = false,
profileType: ProfileType = ProfileType.ANONYMOUS_ANIMAL,
informalNameStyle = false,
) {
const generateProfileFromSet = (
const randomNumber = Math.floor(Math.random() * 10000);

// Format name with random number.
// Informal style: "bear123" (lowercase, no space)
// Default style: "Bear 1002"
const formatName = (name: string) => {
if (informalNameStyle) {
return `${name.toLowerCase()}${randomNumber}`;
}
return `${name} ${randomNumber}`;
};

// Create anonymous profile from a named profile set.
const profileFromSet = (
profileSet: {name: string; avatar: string}[],
): AnonymousProfileMetadata => {
// TODO: Randomly select from set
const {name, avatar} = profileSet[participantNumber % profileSet.length];
return {
name,
name: formatName(name),
avatar,
repeat: Math.floor(participantNumber / profileSet.length),
};
};

const generateRandomHashProfile = (): AnonymousProfileMetadata => {
return {
name: generateId(),
avatar: '',
repeat: 0,
};
};

// Generate random number for unique participant ID (used in publicID and anonymous participant profile)
const randomNumber = Math.floor(Math.random() * 10000);

const generateAnonymousParticipantProfile = (): AnonymousProfileMetadata => {
return {
name: `Participant ${randomNumber}`,
// Set anonymous profiles for each profile set
config.anonymousProfiles = {
[PROFILE_SET_ANIMALS_1_ID]: profileFromSet(PROFILE_SET_ANIMALS_1),
[PROFILE_SET_ANIMALS_2_ID]: profileFromSet(PROFILE_SET_ANIMALS_2),
[PROFILE_SET_NATURE_ID]: profileFromSet(PROFILE_SET_NATURE),
[PROFILE_SET_ANONYMOUS_PARTICIPANT_ID]: {
name: formatName('Participant'),
avatar: '👤',
repeat: 0,
};
},
// Random hashes for ordering/randomization
[PROFILE_SET_RANDOM_1_ID]: {name: generateId(), avatar: '', repeat: 0},
[PROFILE_SET_RANDOM_2_ID]: {name: generateId(), avatar: '', repeat: 0},
[PROFILE_SET_RANDOM_3_ID]: {name: generateId(), avatar: '', repeat: 0},
};

// Set anonymous profiles
const profileAnimal1 = generateProfileFromSet(PROFILE_SET_ANIMALS_1);
const profileAnimal2 = generateProfileFromSet(PROFILE_SET_ANIMALS_2);
const profileNature = generateProfileFromSet(PROFILE_SET_NATURE);
const profileAnonymousParticipant = generateAnonymousParticipantProfile();

config.anonymousProfiles[PROFILE_SET_ANIMALS_1_ID] = profileAnimal1;
config.anonymousProfiles[PROFILE_SET_ANIMALS_2_ID] = profileAnimal2;
config.anonymousProfiles[PROFILE_SET_NATURE_ID] = profileNature;
config.anonymousProfiles[PROFILE_SET_ANONYMOUS_PARTICIPANT_ID] =
profileAnonymousParticipant;

// Set random hashes (can be used for random ordering, etc.)
config.anonymousProfiles[PROFILE_SET_RANDOM_1_ID] =
generateRandomHashProfile();
config.anonymousProfiles[PROFILE_SET_RANDOM_2_ID] =
generateRandomHashProfile();
config.anonymousProfiles[PROFILE_SET_RANDOM_3_ID] =
generateRandomHashProfile();

// Define public ID (using anonymous animal 1 set)
const mainProfile = profileAnimal1;
// Define public ID using base animal name (without number suffix)
const baseName =
PROFILE_SET_ANIMALS_1[participantNumber % PROFILE_SET_ANIMALS_1.length]
.name;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
config.publicId = `${baseName}-${color}-${randomNumber}`.toLowerCase();

config.publicId =
`${mainProfile.name}-${color}-${randomNumber}`.toLowerCase();

// Set display profile for anonymous participants
if (setAnonymousProfile) {
if (profileType === ProfileType.ANONYMOUS_PARTICIPANT) {
// Use participant number profile
const participantProfile =
config.anonymousProfiles[PROFILE_SET_ANONYMOUS_PARTICIPANT_ID];
config.name = participantProfile.name;
config.avatar = participantProfile.avatar;
} else if (profileType === ProfileType.ANONYMOUS_ANIMAL) {
// Use animal profile (default)
config.name = `${mainProfile.name}${mainProfile.repeat === 0 ? '' : ` ${mainProfile.repeat + 1}`}`;
config.avatar = mainProfile.avatar;
const profileSetMap: Partial<Record<ProfileType, string>> = {
[ProfileType.ANONYMOUS_ANIMAL]: PROFILE_SET_ANIMALS_1_ID,
[ProfileType.ANONYMOUS_PARTICIPANT]: PROFILE_SET_ANONYMOUS_PARTICIPANT_ID,
};
const profileSetId = profileSetMap[profileType];
if (profileSetId) {
const profile = config.anonymousProfiles[profileSetId];
config.name = profile.name;
config.avatar = profile.avatar;
}
// Note: ProfileType.DEFAULT should not reach here as setAnonymousProfile would be false
config.pronouns = '';
}
}
Expand Down
2 changes: 2 additions & 0 deletions utils/src/stages/profile_stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum ProfileType {
export interface ProfileStageConfig extends BaseStageConfig {
kind: StageKind.PROFILE;
profileType: ProfileType;
informalNameStyle?: boolean; // e.g., "bear123" instead of "Bear 1002"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable boolean is confusing. Can we make it a regular bool and migrate all the old experiments? Or at least make it look like a bool to the API, and document here that null is for old experiments.

}

// ************************************************************************* //
Expand All @@ -39,5 +40,6 @@ export function createProfileStage(
descriptions: config.descriptions ?? createStageTextConfig(),
progress: config.progress ?? createStageProgressConfig(),
profileType: config.profileType ?? ProfileType.DEFAULT,
informalNameStyle: config.informalNameStyle ?? false,
};
}
1 change: 1 addition & 0 deletions utils/src/stages/profile_stage.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const ProfileStageConfigData = Type.Composite(
Type.Literal(ProfileType.ANONYMOUS_ANIMAL),
Type.Literal(ProfileType.ANONYMOUS_PARTICIPANT),
]),
informalNameStyle: Type.Optional(Type.Boolean({default: false})),
},
strict,
),
Expand Down
Loading