diff --git a/docs/assets/api/schemas.json b/docs/assets/api/schemas.json index 3746375a7..0a3786c36 100644 --- a/docs/assets/api/schemas.json +++ b/docs/assets/api/schemas.json @@ -1450,6 +1450,10 @@ "type": "string" } ] + }, + "informalNameStyle": { + "default": false, + "type": "boolean" } } }, diff --git a/frontend/src/components/stages/profile_stage_editor.scss b/frontend/src/components/stages/profile_stage_editor.scss index f3308ec9e..56448a9b2 100644 --- a/frontend/src/components/stages/profile_stage_editor.scss +++ b/frontend/src/components/stages/profile_stage_editor.scss @@ -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; } diff --git a/frontend/src/components/stages/profile_stage_editor.ts b/frontend/src/components/stages/profile_stage_editor.ts index 760ade01d..cf37a890a 100644 --- a/frontend/src/components/stages/profile_stage_editor.ts +++ b/frontend/src/components/stages/profile_stage_editor.ts @@ -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'; @@ -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'; @@ -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` -
-
- handleProfileTypeChange(ProfileType.DEFAULT)} - > - -
-
- - handleProfileTypeChange(ProfileType.DEFAULT_GENDERED)} - > - -
-
- - handleProfileTypeChange(ProfileType.ANONYMOUS_ANIMAL)} - > - -
-
- - handleProfileTypeChange(ProfileType.ANONYMOUS_PARTICIPANT)} - > - -
-
+
Participant-created profiles
+ + +
+
Anonymous profiles
+ + + ${isAnonymous + ? html` + + ` + : nothing} `; } } diff --git a/frontend/src/shared/templates/quickstart_private_chat.ts b/frontend/src/shared/templates/quickstart_private_chat.ts index 06b30460d..54c25a8c9 100644 --- a/frontend/src/shared/templates/quickstart_private_chat.ts +++ b/frontend/src/shared/templates/quickstart_private_chat.ts @@ -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', diff --git a/functions/src/participant.endpoints.ts b/functions/src/participant.endpoints.ts index d982400c8..afe209c40 100644 --- a/functions/src/participant.endpoints.ts +++ b/functions/src/participant.endpoints.ts @@ -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); } diff --git a/scripts/deliberate_lab/types.py b/scripts/deliberate_lab/types.py index 0d717a9c6..d086be45c 100644 --- a/scripts/deliberate_lab/types.py +++ b/scripts/deliberate_lab/types.py @@ -391,6 +391,7 @@ class ProfileStageConfig(BaseModel): descriptions: StageTextConfig progress: StageProgressConfig profileType: ProfileType + informalNameStyle: bool | None = False class Strategy(StrEnum): diff --git a/utils/src/participant.ts b/utils/src/participant.ts index 044ede71c..dc017ed63 100644 --- a/utils/src/participant.ts +++ b/utils/src/participant.ts @@ -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> = { + [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 = ''; } } diff --git a/utils/src/stages/profile_stage.ts b/utils/src/stages/profile_stage.ts index f10607691..5326636b2 100644 --- a/utils/src/stages/profile_stage.ts +++ b/utils/src/stages/profile_stage.ts @@ -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" } // ************************************************************************* // @@ -39,5 +40,6 @@ export function createProfileStage( descriptions: config.descriptions ?? createStageTextConfig(), progress: config.progress ?? createStageProgressConfig(), profileType: config.profileType ?? ProfileType.DEFAULT, + informalNameStyle: config.informalNameStyle ?? false, }; } diff --git a/utils/src/stages/profile_stage.validation.ts b/utils/src/stages/profile_stage.validation.ts index d1fbe4183..b98592a1d 100644 --- a/utils/src/stages/profile_stage.validation.ts +++ b/utils/src/stages/profile_stage.validation.ts @@ -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, ),