Skip to content

Commit fd72a44

Browse files
authored
Feat 149 update face name association task (#150)
* feat: update face name assoc, added initial attention check files * feat: finished attention check and face name association * fix: updating data for prod
1 parent adee458 commit fd72a44

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+414
-429
lines changed

src/app/models/ParticipantData.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,14 @@ export class FaceNameAssociationTaskData extends BaseParticipantData {
166166
namePresented: string;
167167
actualName: string;
168168
stimulusSet: number;
169-
maleFemale: 'male' | 'female';
169+
gender: 'M' | 'F';
170170
trialType: FaceNameAssociationTaskTrialtype;
171171
userAnswer: UserResponse;
172172
actualAnswer: UserResponse;
173173
isCorrect: boolean;
174174
responseTime: number;
175+
blockNum: number;
176+
attentionCheck: string;
175177
}
176178

177179
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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
maxResponseTime: number;
14+
};
15+
}
16+
17+
export enum AttentionCheckCache {
18+
USER_ANSWERS = 'attention-check-user-answers',
19+
}
20+
21+
@Component({
22+
selector: 'app-attention-check',
23+
templateUrl: './attention-check.component.html',
24+
styleUrls: ['./attention-check.component.scss'],
25+
})
26+
export class AttentionCheckComponent implements Playable {
27+
numbersDisplayed: number[] = [];
28+
userAnswers: string[] = [];
29+
maxResponseTime: number = 0;
30+
config: TaskPlayerState;
31+
32+
// stimulus related variables
33+
showStimulus: boolean = false;
34+
numberDisplayed: number;
35+
promptDisplayed: string = '';
36+
private responseAllowed: boolean = false;
37+
private timer: any;
38+
39+
onComplete = new Subject<{ navigation: Navigation }>();
40+
41+
private translationMapping = {
42+
promptBeginningSegment: {
43+
en: 'Please press',
44+
fr: '',
45+
},
46+
promptEndSegment: {
47+
en: 'on your keyboard',
48+
fr: '',
49+
},
50+
};
51+
52+
constructor(private translateService: TranslateService) {}
53+
54+
configure(metadata: AttentionCheckMetadata, config?: TaskPlayerState): void {
55+
this.config = config;
56+
this.numbersDisplayed = metadata.componentConfig.numbersDisplayed;
57+
this.maxResponseTime = metadata.componentConfig.maxResponseTime;
58+
}
59+
60+
private setTimer(delay: number, cbFunc?: () => void) {
61+
this.timer = window.setTimeout(() => {
62+
if (cbFunc) cbFunc();
63+
}, delay);
64+
}
65+
66+
afterInit(): void {
67+
this.begin();
68+
}
69+
70+
private setStimulus() {
71+
this.showStimulus = true;
72+
this.numberDisplayed = this.numbersDisplayed[this.userAnswers.length];
73+
const currentLang = this.translateService.currentLang;
74+
this.promptDisplayed = `${this.translationMapping.promptBeginningSegment[currentLang]} ${this.numberDisplayed} ${this.translationMapping.promptEndSegment[currentLang]}`;
75+
}
76+
77+
begin() {
78+
this.setTimer(this.maxResponseTime, () => {
79+
this.handleRoundInteraction(null);
80+
});
81+
this.responseAllowed = true;
82+
this.setStimulus();
83+
}
84+
85+
@HostListener('window:keydown', ['$event'])
86+
handleRoundInteraction(event: KeyboardEvent | null) {
87+
if (!this.responseAllowed) return;
88+
if (event === null) {
89+
// we hit our maximum response time
90+
this.userAnswers.push(null);
91+
} else {
92+
this.userAnswers.push(event.key);
93+
}
94+
this.completeRound();
95+
}
96+
97+
completeRound() {
98+
this.responseAllowed = false;
99+
if (this.userAnswers.length >= this.numbersDisplayed.length) {
100+
this.handleComplete();
101+
} else {
102+
this.begin();
103+
}
104+
}
105+
106+
handleComplete(): void {
107+
this.config.setCacheValue(AttentionCheckCache.USER_ANSWERS, this.userAnswers);
108+
this.onComplete.next({ navigation: Navigation.NEXT });
109+
}
110+
}

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: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,28 @@ 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 { ImageService } from 'src/app/services/image.service';
16+
import { AttentionCheckCache } from '../attention-check/attention-check.component';
1717

1818
interface FaceNameAssociationMetadata {
1919
componentName: ComponentName;
2020
componentConfig: {
2121
isPractice: boolean;
2222
phase: 'learning-phase' | 'test-phase';
2323
maxResponseTime: number;
24-
stimulusSet: number;
2524
interTrialDelay: number;
2625
durationStimulusPresented: number;
26+
blockNum: number;
2727
stimuliConfig: {
2828
type: StimuliProvidedType;
2929
stimuli: FaceNameAssociationStimulus[];
3030
};
3131
};
3232
}
3333

34+
export enum FaceNameAssociationCache {
35+
STIMULI = 'facenameassociation-stimuli',
36+
}
37+
3438
@Component({
3539
selector: 'app-face-name-association',
3640
templateUrl: './face-name-association.component.html',
@@ -42,7 +46,7 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
4246
* This task involves two phases. In the first phase, the participant sees a bunch of images and associated names. This is the learning phase.
4347
* In the second phase, the participant is tested on the images. Half of them are correct, and are half of them are recombined.
4448
*
45-
* 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.
4650
*/
4751

4852
// config variables
@@ -53,6 +57,8 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
5357
private interTrialDelay = 500;
5458
private durationStimulusPresented = 3000;
5559
private durationOfFeedback = 1000;
60+
private counterbalance: 1 | 2;
61+
private blockNum: number;
5662

5763
// high level variables
5864
taskData: FaceNameAssociationTaskData[];
@@ -62,12 +68,13 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
6268
// local state variables
6369
trialNum = 0;
6470
currentName = '';
71+
stimulusShown = '';
6572
showStimulus = false;
6673
allowResponse = false;
67-
stimulusShown: string | ArrayBuffer = null;
6874
blobs: { [key: string]: Blob } = {};
6975
feedback: string = '';
7076
showFeedback: boolean = false;
77+
imagePath: string = '';
7178

7279
YES = UserResponse.YES;
7380
NO = UserResponse.NO;
@@ -86,8 +93,7 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
8693
constructor(
8794
protected timerService: TimerService,
8895
protected loaderService: LoaderService,
89-
private dataGenService: DataGenerationService,
90-
private imageService: ImageService
96+
private dataGenService: DataGenerationService
9197
) {
9298
super(loaderService);
9399
}
@@ -107,44 +113,51 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
107113
'duration stimulus presented not defined'
108114
);
109115

116+
this.config = config;
117+
110118
this.phase = throwErrIfNotDefined(metadata.componentConfig.phase, 'phase not defined');
111119
} catch (error) {
112120
throw new Error('values not defined, cannot start study');
113121
}
114122
this.isPractice = metadata.componentConfig.isPractice;
115-
this.stimulusSet = metadata.componentConfig.stimulusSet || 1;
116123
this.interTrialDelay = metadata.componentConfig.interTrialDelay || 500;
117124
this.durationStimulusPresented = metadata.componentConfig.durationStimulusPresented || 3000;
118-
119-
if (metadata.componentConfig.stimuliConfig.type === StimuliProvidedType.HARDCODED)
125+
this.counterbalance = throwErrIfNotDefined(
126+
config.counterBalanceGroups[config.counterbalanceNumber] as 1 | 2,
127+
'counterbalance not defined'
128+
);
129+
this.stimulusSet = this.counterbalance;
130+
this.blockNum = metadata.componentConfig.blockNum || 1;
131+
132+
if (config.getCacheValue(FaceNameAssociationCache.STIMULI)) {
133+
this.stimuli = config.getCacheValue(FaceNameAssociationCache.STIMULI) as FaceNameAssociationStimulus[];
134+
} else if (metadata.componentConfig.stimuliConfig.type === StimuliProvidedType.HARDCODED) {
120135
this.stimuli = metadata.componentConfig.stimuliConfig.stimuli;
136+
}
121137
}
122138

123139
async start() {
124140
this.taskData = [];
125141
this.currentStimuliIndex = 0;
126142
this.trialNum = 0;
127143

128-
if (!this.stimuli) {
129-
this.stimuli = this.dataGenService.generateFaceNameAssociationTaskStimuli(this.phase);
130-
const fileNames = this.stimuli.map((x) => `/assets/images/stimuli/facenameassociation/${x.imageName}.png`);
131-
this.imageService.loadImagesAsBlobs(fileNames).subscribe((res) => {
132-
res.forEach((blob, index) => {
133-
const imageName = this.stimuli[index].imageName;
134-
this.blobs[imageName] = blob;
135-
});
136-
super.start();
137-
});
138-
} else {
139-
super.start();
144+
this.stimuli = this.dataGenService.generateFaceNameAssociationTaskStimuli(
145+
this.phase,
146+
this.counterbalance,
147+
this.stimuli
148+
);
149+
if (!this.config.getCacheValue(FaceNameAssociationCache.STIMULI)) {
150+
// store in cache for next block
151+
this.config.setCacheValue(FaceNameAssociationCache.STIMULI, this.stimuli);
140152
}
153+
super.start();
141154
}
142155

143156
private getActualAnswer(stimulus: FaceNameAssociationStimulus): UserResponse {
144157
if (this.phase === 'learning-phase') {
145158
return UserResponse.NA;
146159
} else {
147-
return stimulus.personName === stimulus.correctPersonName ? UserResponse.YES : UserResponse.NO;
160+
return stimulus.trialType === FaceNameAssociationTaskTrialtype.INTACT ? UserResponse.YES : UserResponse.NO;
148161
}
149162
}
150163

@@ -153,7 +166,10 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
153166
this.showStimulus = false;
154167
this.allowResponse = false;
155168
this.currentName = '';
156-
this.stimulusShown = null;
169+
170+
const attentionCheckAnswers: string = (
171+
(this.config.getCacheValue(AttentionCheckCache.USER_ANSWERS) as string[]) || []
172+
).reduce((acc, curr, index) => (index === 0 ? curr : `${acc}, ${curr}`), '');
157173

158174
this.taskData.push({
159175
userID: this.userID,
@@ -162,19 +178,18 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
162178
trial: ++this.trialNum,
163179
phase: this.phase,
164180
imagePresented: this.currentStimulus.imageName,
165-
namePresented: this.currentStimulus.personName,
166-
actualName: this.currentStimulus.correctPersonName,
181+
namePresented: this.currentStimulus.displayedPersonName,
182+
actualName: this.currentStimulus.actualPersonName,
167183
stimulusSet: this.stimulusSet,
168-
maleFemale: this.currentStimulus.isFemale ? 'female' : 'male',
169-
trialType:
170-
this.currentStimulus.personName === this.currentStimulus.correctPersonName
171-
? FaceNameAssociationTaskTrialtype.INTACT
172-
: FaceNameAssociationTaskTrialtype.RECOMBINED,
184+
gender: this.currentStimulus.gender,
185+
trialType: this.currentStimulus.trialType,
173186
userAnswer: UserResponse.NA,
174187
isCorrect: false,
175188
actualAnswer: this.getActualAnswer(this.currentStimulus),
176189
responseTime: 0,
190+
blockNum: this.blockNum,
177191
submitted: this.timerService.getCurrentTimestamp(),
192+
attentionCheck: attentionCheckAnswers,
178193
});
179194

180195
if (this.phase === 'learning-phase') {
@@ -196,8 +211,8 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
196211
}
197212

198213
private async setStimuliUI() {
199-
this.currentName = this.currentStimulus.personName;
200-
await this.showImage(this.blobs[this.currentStimulus.imageName]);
214+
this.currentName = this.currentStimulus.displayedPersonName;
215+
this.stimulusShown = this.currentStimulus.imagePath;
201216
}
202217

203218
private setMaxResponseTimer(delay: number, cbFunc?: () => void) {
@@ -279,17 +294,4 @@ export class FaceNameAssociationComponent extends AbstractBaseTaskComponent {
279294
return;
280295
}
281296
}
282-
283-
private showImage(blob: Blob): Promise<void> {
284-
return new Promise((resolve) => {
285-
const fr = new FileReader();
286-
const handler = () => {
287-
this.stimulusShown = fr.result;
288-
fr.removeEventListener('load', handler);
289-
resolve();
290-
};
291-
fr.addEventListener('load', handler);
292-
fr.readAsDataURL(blob);
293-
});
294-
}
295297
}

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>

0 commit comments

Comments
 (0)