Skip to content

Commit 5ffc228

Browse files
authored
Merge pull request #1102 from mkbehr/survey-validation-enhancements
Range surveys: require integers and improve validation
2 parents 0ea4f4e + 689c708 commit 5ffc228

11 files changed

Lines changed: 631 additions & 51 deletions

File tree

docs/assets/api/schemas.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2322,13 +2322,13 @@
23222322
"type": "string"
23232323
},
23242324
"upperValue": {
2325-
"type": "number"
2325+
"type": "integer"
23262326
},
23272327
"upperText": {
23282328
"type": "string"
23292329
},
23302330
"lowerValue": {
2331-
"type": "number"
2331+
"type": "integer"
23322332
},
23332333
"lowerText": {
23342334
"type": "string"
@@ -2341,7 +2341,7 @@
23412341
},
23422342
"stepSize": {
23432343
"minimum": 1,
2344-
"type": ["null", "number"]
2344+
"type": ["null", "integer"]
23452345
},
23462346
"condition": {
23472347
"anyOf": [

frontend/src/components/header/header.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,13 @@ export class Header extends MobxLitElement {
280280
if (this.experimentManager.isEditingFull) {
281281
if (!this.experimentManager.isCreator) return nothing;
282282

283+
const editorErrors =
284+
this.experimentEditor.getExperimentConfigErrors();
285+
283286
return html`
287+
${editorErrors.length === 0
288+
? nothing
289+
: html` <div class="error">⚠️ ${editorErrors.join(', ')}</div> `}
284290
<pr-button
285291
color="tertiary"
286292
variant="default"
@@ -291,7 +297,8 @@ export class Header extends MobxLitElement {
291297
<pr-button
292298
color="tertiary"
293299
variant="tonal"
294-
?disabled=${!this.experimentManager.isCreator}
300+
?disabled=${!this.experimentManager.isCreator ||
301+
editorErrors.length > 0}
295302
@click=${() => {
296303
this.analyticsService.trackButtonClick(
297304
ButtonClick.EXPERIMENT_SAVE_EXISTING,

frontend/src/components/stages/survey_editor.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -463,19 +463,18 @@ export class SurveyEditor extends MobxLitElement {
463463
};
464464

465465
const updateLowerValue = (e: InputEvent) => {
466-
const lowerValue =
467-
parseInt((e.target as HTMLInputElement).value, 10) || 0;
466+
const lowerValue = parseFloat((e.target as HTMLInputElement).value) || 0;
468467
this.updateQuestion({...question, lowerValue}, index);
469468
};
470469

471470
const updateUpperValue = (e: InputEvent) => {
472-
const upperValue =
473-
parseInt((e.target as HTMLInputElement).value, 10) || 10;
471+
const upperValue = parseFloat((e.target as HTMLInputElement).value) || 10;
474472
this.updateQuestion({...question, upperValue}, index);
475473
};
476474

477475
const updateStepSize = (e: InputEvent) => {
478-
const stepSize = parseInt((e.target as HTMLInputElement).value, 10) || 1;
476+
const val = parseFloat((e.target as HTMLInputElement).value);
477+
const stepSize = isNaN(val) ? 1 : val;
479478
this.updateQuestion({...question, stepSize}, index);
480479
};
481480

frontend/src/services/experiment.editor.ts

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
AgentMediatorTemplate,
33
AgentMediatorPersonaConfig,
44
AgentParticipantPersonaConfig,
5-
AgentPersonaType,
65
AgentParticipantTemplate,
76
ApiKeyType,
87
CohortParticipantConfig,
@@ -15,18 +14,17 @@ import {
1514
ProlificConfig,
1615
StageConfig,
1716
StageKind,
18-
StageManager,
1917
VariableConfig,
2018
MultiValueVariableConfigType,
2119
requiresValues,
2220
STAGE_MANAGER,
2321
checkApiKeyExists,
22+
SurveyQuestion,
23+
SurveyQuestionKind,
24+
MultipleChoiceItem,
2425
createAgentMediatorPersonaConfig,
2526
createAgentParticipantPersonaConfig,
2627
createExperimentConfig,
27-
createMetadataConfig,
28-
createPermissionsConfig,
29-
createProlificConfig,
3028
generateId,
3129
} from '@deliberation-lab/utils';
3230
import {Timestamp} from 'firebase/firestore';
@@ -106,6 +104,76 @@ export class ExperimentEditor extends Service {
106104
);
107105
};
108106

107+
const validateSurveyQuestion = (
108+
question: SurveyQuestion,
109+
stageName: string,
110+
index: number,
111+
): string[] => {
112+
const errors: string[] = [];
113+
const questionPrefix = `${stageName} question ${index + 1}`;
114+
115+
if (question.questionTitle === '') {
116+
errors.push(`${questionPrefix} is missing a title`);
117+
}
118+
119+
if (question.kind === SurveyQuestionKind.SCALE) {
120+
if (
121+
!Number.isInteger(question.lowerValue) ||
122+
!Number.isInteger(question.upperValue) ||
123+
(question.stepSize !== undefined &&
124+
!Number.isInteger(question.stepSize))
125+
) {
126+
errors.push(
127+
`${questionPrefix} ("${question.questionTitle}"): values must be integers`,
128+
);
129+
return errors;
130+
}
131+
if (question.lowerValue >= question.upperValue) {
132+
errors.push(
133+
`${questionPrefix} ("${question.questionTitle}"): lower value must be less than upper value`,
134+
);
135+
return errors;
136+
}
137+
const range = question.upperValue - question.lowerValue;
138+
const step = question.stepSize ?? 1;
139+
if (step <= 0) {
140+
errors.push(
141+
`${questionPrefix} ("${question.questionTitle}"): step size must be greater than 0`,
142+
);
143+
return errors;
144+
}
145+
if (range % step !== 0) {
146+
errors.push(
147+
`${questionPrefix} ("${question.questionTitle}"): step size must divide max-min (${range}) exactly`,
148+
);
149+
}
150+
}
151+
152+
if (question.kind === SurveyQuestionKind.MULTIPLE_CHOICE) {
153+
if (!question.options || question.options.length === 0) {
154+
errors.push(
155+
`${questionPrefix} ("${question.questionTitle}"): must have at least one option`,
156+
);
157+
return errors;
158+
}
159+
if (
160+
question.correctAnswerId != null &&
161+
question.correctAnswerId !== ''
162+
) {
163+
const hasOption = question.options.some(
164+
(opt: MultipleChoiceItem) => opt.id === question.correctAnswerId,
165+
);
166+
if (!hasOption) {
167+
errors.push(
168+
`${questionPrefix} ("${question.questionTitle}"): correct answer ID doesn't match any option ID`,
169+
);
170+
}
171+
}
172+
}
173+
174+
return errors;
175+
};
176+
109177
const renderApiErrorMessage = (apiType: ApiKeyType) => {
110178
if (hasAgentsWithApiType(apiType)) {
111179
errors.push(
@@ -118,17 +186,16 @@ export class ExperimentEditor extends Service {
118186
renderApiErrorMessage(ApiKeyType.OLLAMA_CUSTOM_URL);
119187

120188
for (const stage of this.stages) {
121-
switch (stage.kind) {
122-
case StageKind.SURVEY:
123-
// Ensure all questions have a non-empty title
124-
stage.questions.forEach((question, index) => {
125-
if (question.questionTitle === '') {
126-
errors.push(
127-
`${stage.name} question ${index + 1} is missing a title`,
128-
);
129-
}
130-
});
131-
break;
189+
if (
190+
stage.kind === StageKind.SURVEY ||
191+
stage.kind === StageKind.SURVEY_PER_PARTICIPANT
192+
) {
193+
if (!stage.questions || stage.questions.length === 0) {
194+
errors.push(`${stage.name} must contain at least one question`);
195+
}
196+
stage.questions.forEach((question: SurveyQuestion, index: number) => {
197+
errors.push(...validateSurveyQuestion(question, stage.name, index));
198+
});
132199
}
133200
}
134201

0 commit comments

Comments
 (0)