Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../stages/info_editor';
import '../stages/payout_editor';
import '../stages/profile_stage_editor';
import '../stages/reveal_editor';
import '../stages/role_editor';
import '../stages/stockinfo_editor';
import '../stages/survey_editor';
import '../stages/survey_per_participant_editor';
Expand Down Expand Up @@ -462,6 +463,11 @@ export class ExperimentBuilder extends MobxLitElement {
<base-stage-editor .stage=${stage}></base-stage-editor>
<reveal-editor .stage=${stage}></reveal-editor>
`;
case StageKind.ROLE:
return html`
<base-stage-editor .stage=${stage}></base-stage-editor>
<role-editor .stage=${stage}></role-editor>
`;
case StageKind.STOCKINFO:
return html`
<base-stage-editor .stage=${stage}></base-stage-editor>
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/components/experiment_builder/stage_builder_dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
createPrivateChatStage,
createProfileStage,
createRevealStage,
createRoleStage,
createStockInfoStage,
createSurveyPerParticipantStage,
createSurveyStage,
Expand Down Expand Up @@ -175,8 +176,8 @@ export class StageBuilderDialog extends MobxLitElement {
${this.renderTransferCard()} ${this.renderSurveyCard()}
${this.renderSurveyPerParticipantCard()} ${this.renderFlipCardCard()}
${this.renderRankingCard()} ${this.renderRevealCard()}
${this.renderPayoutCard()} ${this.renderStockInfoCard()}
${this.renderAssetAllocationCard()}
${this.renderPayoutCard()} ${this.renderRoleCard()}
${this.renderStockInfoCard()} ${this.renderAssetAllocationCard()}
</div>
</div>
`;
Expand Down Expand Up @@ -315,6 +316,22 @@ export class StageBuilderDialog extends MobxLitElement {
`;
}

private renderRoleCard() {
const addStage = () => {
this.addStage(createRoleStage());
};

return html`
<div class="card" @click=${addStage}>
<div class="title">Role assignment</div>
<div>
Randomly assign roles to participants and show different
Markdown-rendered info for each role
</div>
</div>
`;
}

private renderGroupChatCard() {
const addStage = () => {
this.addStage(createChatStage());
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/participant_view/participant_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../stages/payout_participant_view';
import '../stages/profile_participant_editor';
import '../stages/profile_participant_view';
import '../stages/reveal_participant_view';
import '../stages/role_participant_view';
import '../stages/salesperson_participant_view';
import '../stages/asset_allocation_participant_view';
import '../stages/stockinfo_participant_view';
Expand Down Expand Up @@ -257,6 +258,10 @@ export class ParticipantView extends MobxLitElement {
return html`
<reveal-participant-view .stage=${stage}></reveal-participant-view>
`;
case StageKind.ROLE:
return html`
<role-participant-view .stage=${stage}></role-participant-view>
`;
case StageKind.SALESPERSON:
return html`
<salesperson-participant-view .stage=${stage}>
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/components/stages/role_editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@use '../../sass/colors';
@use '../../sass/common';
@use '../../sass/typescale';

:host {
@include common.flex-column;
gap: common.$spacing-xxl;
}

.role {
@include common.flex-column;
background: var(--md-sys-color-surface-variant);
gap: common.$spacing-large;
padding: common.$spacing-large;
}

.subtitle {
@include typescale.label-small;
color: var(--md-sys-color-outlline);
}

.checkbox-wrapper {
@include common.flex-row-align-center;
gap: common.$spacing-small;
overflow-wrap: break-word;

md-checkbox {
flex-shrink: 0;
}
}
158 changes: 158 additions & 0 deletions frontend/src/components/stages/role_editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {MobxLitElement} from '@adobe/lit-mobx';
import {CSSResultGroup, html, nothing} from 'lit';
import {customElement, property} from 'lit/decorators.js';

import '../../pair-components/button';
import '@material/web/textfield/outlined-text-field.js';

import {core} from '../../core/core';
import {ExperimentEditor} from '../../services/experiment.editor';

import {
RoleItem,
RoleStageConfig,
StageKind,
createRoleItem,
} from '@deliberation-lab/utils';

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

/** Editor for role stage. */
@customElement('role-editor')
export class RoleEditorComponent extends MobxLitElement {
static override styles: CSSResultGroup = [styles];

private readonly experimentEditor = core.getService(ExperimentEditor);

@property() stage: RoleStageConfig | undefined = undefined;

override render() {
if (this.stage === undefined) {
return nothing;
}

return html`
${this.stage.roles.map((role, index) => this.renderRoleItem(role, index))}
${this.renderAddRoleButton()}
`;
}

private renderAddRoleButton() {
const addButton = () => {
if (!this.stage) return;

const roles = [...this.stage.roles, createRoleItem()];
this.experimentEditor.updateStage({...this.stage, roles});
};

return html` <pr-button @click=${addButton}> Add new role </pr-button> `;
}

private renderRoleItem(role: RoleItem, index: number) {
const updateRoleItem = (config: Partial<RoleItem>) => {
if (!this.stage) return;

const roles = [
...this.stage.roles.slice(0, index),
{...role, ...config},
...this.stage.roles.slice(index + 1),
];
this.experimentEditor.updateStage({...this.stage, roles});
};

const updateName = (e: InputEvent) => {
const name = (e.target as HTMLTextAreaElement).value;
updateRoleItem({name});
};

const updateDisplayLines = (e: InputEvent) => {
const value = (e.target as HTMLTextAreaElement).value;
updateRoleItem({displayLines: [value]});
};

const updateMinParticipants = (e: InputEvent) => {
const minParticipants = Number((e.target as HTMLTextAreaElement).value);
updateRoleItem({minParticipants});
};

const updateMaxParticipantsNumber = (e: InputEvent) => {
const maxParticipants = Number((e.target as HTMLTextAreaElement).value);
updateRoleItem({maxParticipants});
};

const toggleMaxParticipants = () => {
if (role.maxParticipants === null) {
updateRoleItem({maxParticipants: 100});
} else {
updateRoleItem({maxParticipants: null});
}
};

return html`
<div class="role">
<div class="subtitle">Role ID: ${role.id}</div>
<md-outlined-text-field
required
label="Name of role"
placeholder="Add name of role"
.error=${role.name.length === 0}
.value=${role.name}
?disabled=${!this.experimentEditor.canEditStages}
@input=${updateName}
>
</md-outlined-text-field>
<md-outlined-text-field
label="Minimum number of participants"
type="number"
id="minParticipants"
name="minParticipants"
min="0"
.value=${role.minParticipants ?? 0}
?disabled=${!this.experimentEditor.canEditStages}
@input=${updateMinParticipants}
>
</md-outlined-text-field>
<div class="checkbox-wrapper">
<md-checkbox
touch-target="wrapper"
?checked=${role.maxParticipants !== null}
?disabled=${!this.experimentEditor.canEditStages}
@click=${toggleMaxParticipants}
>
</md-checkbox>
<div>Set maximum number of participants assigned to this role</div>
</div>
<md-outlined-text-field
label="Maximum number of participants"
type="number"
id="maxParticipants"
name="maxParticipants"
min="0"
.value=${role.maxParticipants ?? 100}
?disabled=${!this.experimentEditor.canEditStages ||
role.maxParticipants === null}
@input=${updateMaxParticipantsNumber}
>
</md-outlined-text-field>
<md-outlined-text-field
required
type="textarea"
rows="5"
label="Information to display to role"
placeholder="Add info to display to role"
.error=${role.displayLines.length === 0}
.value=${role.displayLines.join('\n\n') ?? ''}
?disabled=${!this.experimentEditor.canEditStages}
@input=${updateDisplayLines}
>
</md-outlined-text-field>
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'role-editor': RoleEditorComponent;
}
}
83 changes: 83 additions & 0 deletions frontend/src/components/stages/role_participant_view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import '../progress/progress_stage_completed';

import './stage_description';
import './stage_footer';

import {MobxLitElement} from '@adobe/lit-mobx';
import {CSSResultGroup, html, nothing} from 'lit';
import {customElement, property} from 'lit/decorators.js';

import {core} from '../../core/core';
import {CohortService} from '../../services/cohort.service';
import {ParticipantService} from '../../services/participant.service';

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

import {unsafeHTML} from 'lit/directives/unsafe-html.js';
import {convertMarkdownToHTML} from '../../shared/utils';
import {styles} from './info_view.scss';

/** Role stage view for participants. */
@customElement('role-participant-view')
export class RoleView extends MobxLitElement {
static override styles: CSSResultGroup = [styles];

private readonly cohortService = core.getService(CohortService);
private readonly participantService = core.getService(ParticipantService);

@property() stage: RoleStageConfig | null = null;

override render() {
if (!this.stage) {
return nothing;
}

const publicData = this.cohortService.stagePublicDataMap[this.stage.id];
if (publicData?.kind !== StageKind.ROLE) {
return nothing;
}

const roleId =
publicData.participantMap[
this.participantService.profile?.publicId ?? ''
];
const role = this.stage.roles.find((role) => role.id === roleId);

const getRole = () => {
this.participantService.setParticipantRoles(this.stage?.id ?? '');
};

const renderRoleDisplay = () => {
if (!role) return nothing;
return html`
${unsafeHTML(convertMarkdownToHTML(role.displayLines.join('\n\n')))}
`;
};

const renderRoleButton = () => {
return html`
<pr-button @click=${getRole}>Get my participant role</pr-button>
`;
};

return html`
<stage-description .stage=${this.stage}></stage-description>
<div class="html-wrapper">
<div class="info-block">
${role ? renderRoleDisplay() : renderRoleButton()}
</div>
</div>
<stage-footer>
${this.stage.progress.showParticipantProgress
? html`<progress-stage-completed></progress-stage-completed>`
: nothing}
</stage-footer>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'role-view': RoleView;
}
}
17 changes: 17 additions & 0 deletions frontend/src/services/participant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ParticipantProfileBase,
ParticipantProfileExtended,
ParticipantStatus,
RoleStageConfig,
StageKind,
StageParticipantAnswer,
SurveyAnswer,
Expand Down Expand Up @@ -52,6 +53,7 @@ import {
sendChipOfferCallable,
sendChipResponseCallable,
setChipTurnCallable,
setParticipantRolesCallable,
setSalespersonControllerCallable,
setSalespersonMoveCallable,
setSalespersonResponseCallable,
Expand Down Expand Up @@ -979,6 +981,21 @@ export class ParticipantService extends Service {
return output;
}

async setParticipantRoles(stageId: string) {
let output = {success: false};
if (this.experimentId && this.profile) {
output = await setParticipantRolesCallable(
this.sp.firebaseService.functions,
{
experimentId: this.experimentId,
cohortId: this.profile.currentCohortId,
stageId,
},
);
}
return output;
}

async sendAlertMessage(message: string) {
let response = {};
if (this.experimentId && this.profile) {
Expand Down
Loading
Loading