diff --git a/frontend/src/components/experiment_builder/experiment_builder.ts b/frontend/src/components/experiment_builder/experiment_builder.ts index b5abe9918..cbb482df7 100644 --- a/frontend/src/components/experiment_builder/experiment_builder.ts +++ b/frontend/src/components/experiment_builder/experiment_builder.ts @@ -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'; @@ -462,6 +463,11 @@ export class ExperimentBuilder extends MobxLitElement { `; + case StageKind.ROLE: + return html` + + + `; case StageKind.STOCKINFO: return html` diff --git a/frontend/src/components/experiment_builder/stage_builder_dialog.ts b/frontend/src/components/experiment_builder/stage_builder_dialog.ts index 2204831fc..be8253daf 100644 --- a/frontend/src/components/experiment_builder/stage_builder_dialog.ts +++ b/frontend/src/components/experiment_builder/stage_builder_dialog.ts @@ -24,6 +24,7 @@ import { createPrivateChatStage, createProfileStage, createRevealStage, + createRoleStage, createStockInfoStage, createSurveyPerParticipantStage, createSurveyStage, @@ -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()} `; @@ -315,6 +316,22 @@ export class StageBuilderDialog extends MobxLitElement { `; } + private renderRoleCard() { + const addStage = () => { + this.addStage(createRoleStage()); + }; + + return html` +
+
Role assignment
+
+ Randomly assign roles to participants and show different + Markdown-rendered info for each role +
+
+ `; + } + private renderGroupChatCard() { const addStage = () => { this.addStage(createChatStage()); diff --git a/frontend/src/components/participant_view/participant_view.ts b/frontend/src/components/participant_view/participant_view.ts index bae955d74..bf6883a2e 100644 --- a/frontend/src/components/participant_view/participant_view.ts +++ b/frontend/src/components/participant_view/participant_view.ts @@ -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'; @@ -257,6 +258,10 @@ export class ParticipantView extends MobxLitElement { return html` `; + case StageKind.ROLE: + return html` + + `; case StageKind.SALESPERSON: return html` diff --git a/frontend/src/components/stages/role_editor.scss b/frontend/src/components/stages/role_editor.scss new file mode 100644 index 000000000..7d472e798 --- /dev/null +++ b/frontend/src/components/stages/role_editor.scss @@ -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; + } +} diff --git a/frontend/src/components/stages/role_editor.ts b/frontend/src/components/stages/role_editor.ts new file mode 100644 index 000000000..8edd71013 --- /dev/null +++ b/frontend/src/components/stages/role_editor.ts @@ -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` Add new role `; + } + + private renderRoleItem(role: RoleItem, index: number) { + const updateRoleItem = (config: Partial) => { + 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` +
+
Role ID: ${role.id}
+ + + + +
+ + +
Set maximum number of participants assigned to this role
+
+ + + + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'role-editor': RoleEditorComponent; + } +} diff --git a/frontend/src/components/stages/role_participant_view.ts b/frontend/src/components/stages/role_participant_view.ts new file mode 100644 index 000000000..c83a085b6 --- /dev/null +++ b/frontend/src/components/stages/role_participant_view.ts @@ -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` + Get my participant role + `; + }; + + return html` + +
+
+ ${role ? renderRoleDisplay() : renderRoleButton()} +
+
+ + ${this.stage.progress.showParticipantProgress + ? html`` + : nothing} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'role-view': RoleView; + } +} diff --git a/frontend/src/services/participant.service.ts b/frontend/src/services/participant.service.ts index 24ef6b34a..44a09bce4 100644 --- a/frontend/src/services/participant.service.ts +++ b/frontend/src/services/participant.service.ts @@ -9,6 +9,7 @@ import { ParticipantProfileBase, ParticipantProfileExtended, ParticipantStatus, + RoleStageConfig, StageKind, StageParticipantAnswer, SurveyAnswer, @@ -52,6 +53,7 @@ import { sendChipOfferCallable, sendChipResponseCallable, setChipTurnCallable, + setParticipantRolesCallable, setSalespersonControllerCallable, setSalespersonMoveCallable, setSalespersonResponseCallable, @@ -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) { diff --git a/frontend/src/shared/callables.ts b/frontend/src/shared/callables.ts index 0d7b46ff7..314be0257 100644 --- a/frontend/src/shared/callables.ts +++ b/frontend/src/shared/callables.ts @@ -21,6 +21,7 @@ import { SendChipResponseData, SendParticipantCheckData, SetChipTurnData, + SetParticipantRolesData, SetSalespersonControllerData, SetSalespersonMoveData, SetSalespersonResponseData, @@ -410,6 +411,18 @@ export const createChatMessageCallable = async ( return data; }; +/** Generic endpoint for assigning participants to roles for role stage. */ +export const setParticipantRolesCallable = async ( + functions: Functions, + config: SetParticipantRolesData, +) => { + const {data} = await httpsCallable( + functions, + 'setParticipantRoles', + )(config); + return data; +}; + /** Generic endpoint for sending chip negotiation offer. */ export const sendChipOfferCallable = async ( functions: Functions, diff --git a/functions/src/agent_participant.utils.ts b/functions/src/agent_participant.utils.ts index a0a3c58c5..ee5e2e292 100644 --- a/functions/src/agent_participant.utils.ts +++ b/functions/src/agent_participant.utils.ts @@ -16,6 +16,7 @@ import { import {createAgentChatMessageFromPrompt} from './chat/chat.agent'; import {completeProfile} from './stages/profile.utils'; import {getAgentParticipantRankingStageResponse} from './stages/ranking.agent'; +import {assignRolesToParticipants} from './stages/role.utils'; import {getAgentParticipantSurveyStageResponse} from './stages/survey.agent'; import { getExperimenterData, @@ -100,6 +101,15 @@ export async function completeStageAsAgentParticipant( await completeStage(); participantDoc.set(participant); break; + case StageKind.ROLE: + await assignRolesToParticipants( + experimentId, + participant.currentCohortId, + stage.id, + ); + await completeStage(); + participantDoc.set(participant); + break; case StageKind.SALESPERSON: createAgentChatMessageFromPrompt( experimentId, diff --git a/functions/src/index.ts b/functions/src/index.ts index 8ff5da53a..cacb8e2cf 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -17,6 +17,7 @@ export * from './stages/chip.endpoints'; export * from './stages/flipcard.endpoints'; export * from './stages/ranking.endpoints'; +export * from './stages/role.endpoints'; export * from './stages/salesperson.endpoints'; export * from './stages/survey.endpoints'; diff --git a/functions/src/participant.endpoints.ts b/functions/src/participant.endpoints.ts index 80378153e..d44bcd2c4 100644 --- a/functions/src/participant.endpoints.ts +++ b/functions/src/participant.endpoints.ts @@ -6,6 +6,7 @@ import { Experiment, ParticipantProfileExtended, ParticipantStatus, + RoleStagePublicData, StageKind, SurveyStagePublicData, createParticipantProfileExtended, @@ -531,6 +532,13 @@ export const acceptParticipantTransfer = onCall(async (request) => { publicChipData.participantChipValueMap[publicId] = stage.chipValueMap; transaction.set(publicDocument, publicChipData); break; + case StageKind.ROLE: + const publicRoleData = ( + await publicDocument.get() + ).data() as RoleStagePublicData; + // TODO: Assign new role to participant (or move role over) + transaction.set(publicDocument, publicRoleData); + break; default: break; } diff --git a/functions/src/prompt.utils.ts b/functions/src/prompt.utils.ts index 88aaa0412..40991a8b0 100644 --- a/functions/src/prompt.utils.ts +++ b/functions/src/prompt.utils.ts @@ -18,7 +18,9 @@ import { import { getFirestoreAnswersForStage, getFirestoreExperiment, + getFirestoreParticipant, getFirestoreStage, + getFirestoreStagePublicData, getFirestorePublicStageChatMessages, getFirestorePrivateChatMessages, } from './utils/firestore'; @@ -176,6 +178,29 @@ export async function getStageDisplayForPrompt( stage.id, ); return getChatPromptMessageHistory(privateMessages, stage); + case StageKind.ROLE: + const rolePublicData = await getFirestoreStagePublicData( + experimentId, + cohortId, + stage.id, + ); + const getRoleDisplay = (roleId: string) => { + if (stage.kind !== StageKind.ROLE) return ''; + return ( + stage.roles.find((role) => role.id === roleId)?.displayLines ?? [] + ); + }; + const roleInfo: string[] = []; + for (const participantId of participantIds) { + const participant = await getFirestoreParticipant( + experimentId, + participantId, + ); + roleInfo.push( + `${participant.publicId}: ${getRoleDisplay(rolePublicData.participantMap[participant.publicId] ?? '').join('\n\n')}`, + ); + } + return roleInfo.join('\n'); case StageKind.STOCKINFO: return getStockInfoSummaryText(stage); case StageKind.ASSET_ALLOCATION: diff --git a/functions/src/stages/role.endpoints.ts b/functions/src/stages/role.endpoints.ts new file mode 100644 index 000000000..bf1149d21 --- /dev/null +++ b/functions/src/stages/role.endpoints.ts @@ -0,0 +1,39 @@ +import { + RoleItem, + RoleStageConfig, + RoleStagePublicData, + StageKind, +} from '@deliberation-lab/utils'; + +import { + getFirestoreActiveParticipants, + getFirestoreStage, + getFirestoreStagePublicDataRef, +} from '../utils/firestore'; +import {assignRolesToParticipants} from './role.utils'; + +import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; +import {onCall} from 'firebase-functions/v2/https'; + +import {app} from '../app'; + +// ************************************************************************* // +// setParticipantRoles endpoint // +// // +// Randomly assign one of the stage's defined roles to each of the active // +// cohort participants. // +// // +// Input structure: { // +// experimentId, cohortId, stageId // +// } // +// Validation: utils/src/role_stage.validation.ts // +// ************************************************************************* // +export const setParticipantRoles = onCall(async (request) => { + const {data} = request; + const experimentId = data.experimentId; + const cohortId = data.cohortId; + const stageId = data.stageId; + + return await assignRolesToParticipants(experimentId, cohortId, stageId); +}); diff --git a/functions/src/stages/role.utils.ts b/functions/src/stages/role.utils.ts new file mode 100644 index 000000000..ac03a4780 --- /dev/null +++ b/functions/src/stages/role.utils.ts @@ -0,0 +1,92 @@ +import { + RoleItem, + RoleStageConfig, + RoleStagePublicData, + StageKind, +} from '@deliberation-lab/utils'; + +import { + getFirestoreActiveParticipants, + getFirestoreStage, + getFirestoreStagePublicDataRef, +} from '../utils/firestore'; + +import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; + +import {app} from '../app'; +/** Assign roles to participants for given stage. */ +export async function assignRolesToParticipants( + experimentId: string, + cohortId: string, + stageId: string, +) { + // Define role stage config + const stage = await getFirestoreStage(experimentId, stageId); + if (stage.kind !== StageKind.ROLE) { + return {success: false}; + } + + // Define role stage public data document reference + const publicDoc = getFirestoreStagePublicDataRef( + experimentId, + cohortId, + stageId, + ); + + await app.firestore().runTransaction(async (transaction) => { + const publicStageData = ( + await publicDoc.get() + ).data() as RoleStagePublicData; + + // Get relevant (active, in cohort) participants + const participants = await getFirestoreActiveParticipants( + experimentId, + cohortId, + stageId, + ); + + // TODO: For each participant, check if they have been assigned a role + // If not, assign role according to stage minimum/maximums + const getRoleCounts = () => { + const roleToFrequencyMap: Record = {}; + Object.values(publicStageData.participantMap).forEach((role) => { + roleToFrequencyMap[role] = (roleToFrequencyMap[role] ?? 0) + 1; + }); + return roleToFrequencyMap; + }; + const getNextRole = () => { + const roleToFrequencyMap = getRoleCounts(); + // First, fill roles with minimum number of participants required + for (const role of stage.roles) { + const roleFrequency = roleToFrequencyMap[role.id] ?? 0; + if ( + roleFrequency < role.minParticipants && + roleFrequency < role.maxParticipants + ) { + return role; + } + } + // Otherwise, randomly pick role + const availableRoles = stage.roles.filter( + (role) => + (roleToFrequencyMap[role.id] ?? 0) < role.maxParticipants || + role.maxParticipants === null, + ); + return availableRoles[Math.floor(Math.random() * availableRoles.length)]; + + return null; + }; + + for (const participant of participants) { + if (!publicStageData.participantMap[participant.publicId]) { + publicStageData.participantMap[participant.publicId] = + getNextRole()?.id ?? ''; + } + } + + transaction.set(publicDoc, publicStageData); + }); // end transaction + + return {success: true}; +} diff --git a/utils/src/index.ts b/utils/src/index.ts index 167687baa..3cfebf57a 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -65,6 +65,8 @@ export * from './stages/flipcard_stage.validation'; export * from './stages/ranking_stage'; export * from './stages/ranking_stage.prompts'; export * from './stages/ranking_stage.validation'; +export * from './stages/role_stage'; +export * from './stages/role_stage.validation'; export * from './stages/info_stage'; export * from './stages/info_stage.prompts'; export * from './stages/info_stage.validation'; diff --git a/utils/src/stages/role_stage.ts b/utils/src/stages/role_stage.ts new file mode 100644 index 000000000..d922f72c1 --- /dev/null +++ b/utils/src/stages/role_stage.ts @@ -0,0 +1,79 @@ +import {generateId} from '../shared'; +import { + BaseStageConfig, + BaseStagePublicData, + StageKind, + createStageTextConfig, + createStageProgressConfig, +} from './stage'; + +/** Role assignment stage types and functions. */ + +// ************************************************************************* // +// TYPES // +// ************************************************************************* // + +export interface RoleStageConfig extends BaseStageConfig { + kind: StageKind.ROLE; + roles: RoleItem[]; +} + +export interface RoleItem { + id: string; // unique identifier + name: string; // name of role + displayLines: string[]; // markdown content to display to user + minParticipants: number; + maxParticipants: number | null; // if null, no limit on participants +} + +/** + * RoleStagePublicData. + * + * This is saved as a stage doc (with stage ID as doc ID) under + * experiments/{experimentId}/cohorts/{cohortId}/publicStageData + */ +export interface RoleStagePublicData extends BaseStagePublicData { + kind: StageKind.ROLE; + // Maps from participant public ID to role ID + participantMap: Record; +} + +// ************************************************************************* // +// FUNCTIONS // +// ************************************************************************* // + +/** Create role stage. */ +export function createRoleStage( + config: Partial = {}, +): RoleStageConfig { + return { + id: config.id ?? generateId(), + kind: StageKind.ROLE, + name: config.name ?? 'Role assignment', + descriptions: config.descriptions ?? createStageTextConfig(), + progress: config.progress ?? createStageProgressConfig(), + roles: config.roles ?? [], + }; +} + +/** Create role item. */ +export function createRoleItem(config: Partial = {}): RoleItem { + return { + id: config.id ?? generateId(), + name: config.name ?? '', + displayLines: config.displayLines ?? [], + minParticipants: config.minParticipants ?? 0, + maxParticipants: config.maxParticipants ?? null, + }; +} + +/** Create role stage public data. */ +export function createRoleStagePublicData( + config: RoleStageConfig, +): RoleStagePublicData { + return { + id: config.id, + kind: StageKind.ROLE, + participantMap: {}, + }; +} diff --git a/utils/src/stages/role_stage.validation.ts b/utils/src/stages/role_stage.validation.ts new file mode 100644 index 000000000..3e0d22996 --- /dev/null +++ b/utils/src/stages/role_stage.validation.ts @@ -0,0 +1,50 @@ +import {Type, type Static} from '@sinclair/typebox'; +import {StageKind} from './stage'; +import { + StageProgressConfigSchema, + StageTextConfigSchema, +} from './stage.validation'; + +/** Shorthand for strict TypeBox object validation */ +const strict = {additionalProperties: false} as const; + +// ************************************************************************* // +// writeExperiment, updateStageConfig endpoints // +// ************************************************************************* // + +/** RoleItem data validation. */ +export const RoleItemData = Type.Object( + { + id: Type.String({minLength: 1}), + name: Type.String(), + displayLines: Type.Array(Type.String()), + minParticipants: Type.Number(), + maxParticipants: Type.Union([Type.Number(), Type.Null()]), + }, + strict, +); + +/** RoleStageConfig input validation. */ +export const RoleStageConfigData = Type.Object( + { + id: Type.String({minLength: 1}), + kind: Type.Literal(StageKind.ROLE), + name: Type.String({minLength: 1}), + descriptions: StageTextConfigSchema, + progress: StageProgressConfigSchema, + roles: Type.Array(RoleItemData), + }, + strict, +); + +/** setParticipantRoles endpoint data validation. */ +export const SetParticipantRolesData = Type.Object( + { + experimentId: Type.String({minLength: 1}), + cohortId: Type.String({minLength: 1}), + stageId: Type.String({minLength: 1}), + }, + strict, +); + +export type SetParticipantRolesData = Static; diff --git a/utils/src/stages/stage.ts b/utils/src/stages/stage.ts index 242618ca6..7350d40a8 100644 --- a/utils/src/stages/stage.ts +++ b/utils/src/stages/stage.ts @@ -31,6 +31,11 @@ import {PayoutStageConfig, PayoutStageParticipantAnswer} from './payout_stage'; import {PrivateChatStageConfig} from './private_chat_stage'; import {ProfileStageConfig} from './profile_stage'; import {RevealStageConfig} from './reveal_stage'; +import { + RoleStageConfig, + RoleStagePublicData, + createRoleStagePublicData, +} from './role_stage'; import { SalespersonStageConfig, SalespersonStagePublicData, @@ -80,6 +85,7 @@ export enum StageKind { SALESPERSON = 'salesperson', // co-op traveling salesperson game STOCKINFO = 'stockinfo', ASSET_ALLOCATION = 'assetAllocation', // asset allocation between stocks + ROLE = 'role', // info stage that assigns different roles to participants SURVEY = 'survey', SURVEY_PER_PARTICIPANT = 'surveyPerParticipant', TRANSFER = 'transfer', @@ -125,6 +131,7 @@ export type StageConfig = | SalespersonStageConfig | StockInfoStageConfig | AssetAllocationStageConfig + | RoleStageConfig | SurveyStageConfig | SurveyPerParticipantStageConfig | TOSStageConfig @@ -172,6 +179,7 @@ export type StagePublicData = | ChipStagePublicData | FlipCardStagePublicData | RankingStagePublicData + | RoleStagePublicData | SalespersonStagePublicData | AssetAllocationStagePublicData | SurveyStagePublicData; @@ -224,6 +232,9 @@ export function createPublicDataFromStageConfigs(stages: StageConfig[]) { case StageKind.RANKING: publicData.push(createRankingStagePublicData(stage.id)); break; + case StageKind.ROLE: + publicData.push(createRoleStagePublicData(stage)); + break; case StageKind.SALESPERSON: publicData.push( createSalespersonStagePublicData(stage.id, stage.board.startCoord), diff --git a/utils/src/stages/stage.validation.ts b/utils/src/stages/stage.validation.ts index a081b6c76..5c4f7d47f 100644 --- a/utils/src/stages/stage.validation.ts +++ b/utils/src/stages/stage.validation.ts @@ -10,6 +10,7 @@ import {PayoutStageConfigData} from './payout_stage.validation'; import {PrivateChatStageConfigData} from './private_chat_stage.validation'; import {ProfileStageConfigData} from './profile_stage.validation'; import {RevealStageConfigData} from './reveal_stage.validation'; +import {RoleStageConfigData} from './role_stage.validation'; import {SalespersonStageConfigData} from './salesperson_stage.validation'; import {StockInfoStageConfigData} from './stockinfo_stage.validation'; import { @@ -36,6 +37,7 @@ export const StageConfigData = Type.Union([ ProfileStageConfigData, RankingStageConfigData, RevealStageConfigData, + RoleStageConfigData, SalespersonStageConfigData, StockInfoStageConfigData, SurveyPerParticipantStageConfigData,