Skip to content

Commit 73a8d58

Browse files
committed
feat: finished attention check and face name association
1 parent e4f9828 commit 73a8d58

File tree

14 files changed

+195
-64
lines changed

14 files changed

+195
-64
lines changed

src/app/models/ParticipantData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export class FaceNameAssociationTaskData extends BaseParticipantData {
173173
isCorrect: boolean;
174174
responseTime: number;
175175
blockNum: number;
176+
attentionCheck: string;
176177
}
177178

178179
export class PLTTaskData extends BaseParticipantData {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div class="container-fluid task-text wrapper flex-column-center">
2+
<div class="full-box flex-column-center">
3+
<h1 class="color-primary responsive-font-size-xl">{{ promptDisplayed }}</h1>
4+
</div>
5+
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import '../../../../sass/mixins';
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Component, HostListener } from '@angular/core';
2+
import { Subject } from 'rxjs';
3+
import { ComponentName } from 'src/app/services/component-factory.service';
4+
import { Navigation } from '../../shared/navigation-buttons/navigation-buttons.component';
5+
import { Playable } from '../playable';
6+
import { TaskPlayerState } from '../task-player/task-player.component';
7+
import { TranslateService } from '@ngx-translate/core';
8+
9+
interface AttentionCheckMetadata {
10+
component: ComponentName;
11+
componentConfig: {
12+
numbersDisplayed: number[];
13+
durationStimulusPresented: number;
14+
maxResponseTime: number;
15+
};
16+
}
17+
18+
export enum AttentionCheckCache {
19+
USER_ANSWERS = 'attention-check-user-answers',
20+
}
21+
22+
@Component({
23+
selector: 'app-attention-check',
24+
templateUrl: './attention-check.component.html',
25+
styleUrls: ['./attention-check.component.scss'],
26+
})
27+
export class AttentionCheckComponent implements Playable {
28+
numbersDisplayed: number[] = [];
29+
userAnswers: string[] = [];
30+
maxResponseTime: number = 0;
31+
config: TaskPlayerState;
32+
33+
// stimulus related variables
34+
showStimulus: boolean = false;
35+
numberDisplayed: number;
36+
promptDisplayed: string = '';
37+
private responseAllowed: boolean = false;
38+
private timer: any;
39+
40+
onComplete = new Subject<{ navigation: Navigation }>();
41+
42+
private translationMapping = {
43+
promptBeginningSegment: {
44+
en: 'Please press',
45+
fr: '',
46+
},
47+
promptEndSegment: {
48+
en: 'on your keyboard',
49+
fr: '',
50+
},
51+
};
52+
53+
constructor(private translateService: TranslateService) {}
54+
55+
configure(metadata: AttentionCheckMetadata, config?: TaskPlayerState): void {
56+
this.config = config;
57+
this.numbersDisplayed = metadata.componentConfig.numbersDisplayed;
58+
this.maxResponseTime = metadata.componentConfig.maxResponseTime;
59+
}
60+
61+
private setTimer(delay: number, cbFunc?: () => void) {
62+
this.timer = window.setTimeout(() => {
63+
if (cbFunc) cbFunc();
64+
}, delay);
65+
}
66+
67+
afterInit(): void {
68+
this.begin();
69+
}
70+
71+
private setStimulus() {
72+
this.showStimulus = true;
73+
this.numberDisplayed = this.numbersDisplayed[this.userAnswers.length];
74+
const currentLang = this.translateService.currentLang;
75+
this.promptDisplayed = `${this.translationMapping.promptBeginningSegment[currentLang]} ${this.numberDisplayed} ${this.translationMapping.promptEndSegment[currentLang]}`;
76+
}
77+
78+
begin() {
79+
this.setTimer(this.maxResponseTime, () => {
80+
this.handleRoundInteraction(null);
81+
});
82+
this.responseAllowed = true;
83+
this.setStimulus();
84+
}
85+
86+
@HostListener('window:keydown', ['$event'])
87+
handleRoundInteraction(event: KeyboardEvent | null) {
88+
if (!this.responseAllowed) return;
89+
if (event === null) {
90+
// we hit our maximum response time
91+
this.userAnswers.push(null);
92+
} else {
93+
this.userAnswers.push(event.key);
94+
}
95+
this.completeRound();
96+
}
97+
98+
completeRound() {
99+
this.responseAllowed = false;
100+
if (this.userAnswers.length >= this.numbersDisplayed.length) {
101+
this.handleComplete();
102+
} else {
103+
this.begin();
104+
}
105+
}
106+
107+
handleComplete(): void {
108+
this.config.setCacheValue(AttentionCheckCache.USER_ANSWERS, this.userAnswers);
109+
this.onComplete.next({ navigation: Navigation.NEXT });
110+
}
111+
}

src/app/pages/tasks/task-playables/face-name-association/face-name-association.component.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
font-size: 50px !important;
55
}
66
.stimulus {
7-
width: 600px;
7+
width: 50vw;
8+
max-width: 1000px;
89
height: auto;
910
}
1011

src/app/pages/tasks/task-playables/face-name-association/face-name-association.component.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TimerService } from 'src/app/services/timer.service';
1313
import { AbstractBaseTaskComponent } from '../base-task';
1414
import { TaskPlayerState } from '../task-player/task-player.component';
1515
import { DataGenerationService } from 'src/app/services/data-generation/data-generation.service';
16+
import { AttentionCheckCache } from '../attention-check/attention-check.component';
1617

1718
interface FaceNameAssociationMetadata {
1819
componentName: ComponentName;
@@ -32,7 +33,6 @@ interface FaceNameAssociationMetadata {
3233

3334
export enum FaceNameAssociationCache {
3435
STIMULI = 'facenameassociation-stimuli',
35-
BLOCK_NUM = 'facenameassociation-block-num',
3636
}
3737

3838
@Component({
@@ -46,7 +46,7 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
4646
* This task involves two phases. In the first phase, the participant sees a bunch of images and associated names. This is the learning phase.
4747
* In the second phase, the participant is tested on the images. Half of them are correct, and are half of them are recombined.
4848
*
49-
* The stimuli are hard coded.
49+
* The face images are taken from a set number of images, and names are automatically (and randomly) assigned depending on the counterbalance.
5050
*/
5151

5252
// config variables
@@ -127,7 +127,7 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
127127
'counterbalance not defined'
128128
);
129129
this.stimulusSet = this.counterbalance;
130-
this.blockNum = this.config.getCacheValue(FaceNameAssociationCache.BLOCK_NUM) || 1; // set to 1 if not defined
130+
this.blockNum = metadata.componentConfig.blockNum || 1;
131131

132132
if (config.getCacheValue(FaceNameAssociationCache.STIMULI)) {
133133
this.stimuli = config.getCacheValue(FaceNameAssociationCache.STIMULI) as FaceNameAssociationStimulus[];
@@ -167,6 +167,10 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
167167
this.allowResponse = false;
168168
this.currentName = '';
169169

170+
const attentionCheckAnswers: string = (
171+
(this.config.getCacheValue(AttentionCheckCache.USER_ANSWERS) as string[]) || []
172+
).reduce((acc, curr, index) => (index === 0 ? curr : `${acc}, ${curr}`), '');
173+
170174
this.taskData.push({
171175
userID: this.userID,
172176
studyId: this.studyId,
@@ -185,6 +189,7 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
185189
responseTime: 0,
186190
blockNum: this.blockNum,
187191
submitted: this.timerService.getCurrentTimestamp(),
192+
attentionCheck: attentionCheckAnswers,
188193
});
189194

190195
if (this.phase === 'learning-phase') {
@@ -280,7 +285,6 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
280285

281286
if (finishedLastStimulus) {
282287
super.decideToRepeat();
283-
this.config.setCacheValue(FaceNameAssociationCache.BLOCK_NUM, ++this.blockNum);
284288
return;
285289
} else {
286290
this.currentStimuliIndex++;

src/app/pages/tasks/task-playables/task-display/task-display.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ <h3 *ngIf="subtitle" class="subtitle" [innerHTML]="subtitle"></h3>
1616
<div>
1717
<ng-container *ngFor="let section of displaySections">
1818
<ng-container *ngIf="section.sectionType === 'text'">
19-
<div class="section" [innerHTML]="injectString(section)"></div>
19+
<div
20+
class="section"
21+
[innerHTML]="injectString(section.textContent, section.injection, section.cacheKey)"
22+
></div>
2023
</ng-container>
2124
<div
2225
class="image-container {{ section.imageAlignment }}"
@@ -29,7 +32,7 @@ <h3 *ngIf="subtitle" class="subtitle" [innerHTML]="subtitle"></h3>
2932
>
3033
<img
3134
class="{{ section.sectionType }}"
32-
[src]="getTranslation(section.imagePath)"
35+
[src]="injectString(getTranslation(section.imagePath), section.injection, section.cacheKey)"
3336
alt="Instruction Image"
3437
/>
3538
</div>

src/app/pages/tasks/task-playables/task-display/task-display.component.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,16 @@ export class TaskDisplayComponent implements OnDestroy, Playable {
100100
this.handleComplete(nav);
101101
}
102102

103-
injectString(section: DisplaySection): string {
104-
const text = getTextForLang(this.translateService.currentLang as SupportedLangs, section.textContent);
105-
106-
switch (section.injection) {
103+
injectString(
104+
textContent: string,
105+
injection: 'cached-string' | 'counterbalance' | 'counterbalance-alternative',
106+
cacheKey: string
107+
): string {
108+
const text = getTextForLang(this.translateService.currentLang as SupportedLangs, textContent);
109+
110+
switch (injection) {
107111
case 'cached-string':
108-
const cachedVar = thisOrDefault(this.config.getCacheValue(section.cacheKey), '');
112+
const cachedVar = thisOrDefault(this.config.getCacheValue(cacheKey), '');
109113
return text.replace('???', cachedVar);
110114
case 'counterbalance':
111115
return text.replace('???', this.counterbalanceStr);
@@ -143,7 +147,8 @@ export class TaskDisplayComponent implements OnDestroy, Playable {
143147

144148
// search the display sections to see if counterbalance alt needs to be injected
145149
if (!!metadata.componentConfig.sections.find((section) => section.injection === 'counterbalance-alternative')) {
146-
// 3 - 1 == 2, and 3 - 2 == 1. This gives us the value that is not the counterbalance target value
150+
// 3 - 1 == 2, and 3 - 2 == 1. This gives us the value that is not the counterbalance target value.
151+
// Note that this only works if the counterbalance groups = 2. If more, we need to modify this logic
147152
this.counterbalanceAltStr = config.counterBalanceGroups[3 - config.counterbalanceNumber];
148153
}
149154

src/app/pages/tasks/task.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { MatrixComponent } from './task-playables/questionnaire/matrix/matrix.co
4848
import { SliderControlComponent } from './task-playables/questionnaire/slider-control/slider-control.component';
4949
import { RadioButtonsComponent } from './task-playables/questionnaire/radio-buttons/radio-buttons.component';
5050
import { SdmtComponent } from './task-playables/sdmt/sdmt.component';
51+
import { AttentionCheckComponent } from './task-playables/attention-check/attention-check.component';
5152

5253
@NgModule({
5354
declarations: [
@@ -83,6 +84,7 @@ import { SdmtComponent } from './task-playables/sdmt/sdmt.component';
8384
SelectOptionComponent,
8485
InfoDisplayComponent,
8586
SkipButtonComponent,
87+
AttentionCheckComponent,
8688

8789
RotateDirective,
8890

src/app/services/component-factory.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import { ProbabilisticLearningTaskComponent } from '../pages/tasks/task-playable
2222
import { IowaGamblingTaskComponent } from '../pages/tasks/task-playables/iowa-gambling-task/iowa-gambling-task.component';
2323
import { InformationTaskComponent } from '../pages/tasks/task-playables/information-task/information-task.component';
2424
import { SdmtComponent } from '../pages/tasks/task-playables/sdmt/sdmt.component';
25+
import { AttentionCheckComponent } from '../pages/tasks/task-playables/attention-check/attention-check.component';
2526

2627
export enum ComponentName {
2728
// Generic components
2829
DISPLAY_COMPONENT = 'DISPLAYCOMPONENT',
2930
SELECT_OPTION_COMPONENT = 'SELECTOPTIONCOMPONENT',
31+
ATTENTIONCHECKCOMPONENT = 'ATTENTIONCHECKCOMPONENT',
3032

3133
// Task related components
3234
RATING_COMPONENT = 'RATINGCOMPONENT',
@@ -78,6 +80,7 @@ const ComponentMap = {
7880
[ComponentName.IOWA_GAMBLING_COMPONENT]: IowaGamblingTaskComponent,
7981
[ComponentName.INFORMATION_TASK_COMPONENT]: InformationTaskComponent,
8082
[ComponentName.SDMT_COMPONENT]: SdmtComponent,
83+
[ComponentName.ATTENTIONCHECKCOMPONENT]: AttentionCheckComponent,
8184
};
8285

8386
@Injectable({

0 commit comments

Comments
 (0)