@vue-skuilder/common-ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index.css +10 -0
- package/dist/common-ui.es.js +16404 -0
- package/dist/common-ui.es.js.map +1 -0
- package/dist/common-ui.umd.js +9 -0
- package/dist/common-ui.umd.js.map +1 -0
- package/dist/components/HeatMap.types.d.ts +13 -0
- package/dist/components/PaginatingToolbar.types.d.ts +40 -0
- package/dist/components/SkMouseTrap.types.d.ts +3 -0
- package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
- package/dist/components/SnackbarService.d.ts +11 -0
- package/dist/components/StudySession.types.d.ts +6 -0
- package/dist/components/auth/index.d.ts +4 -0
- package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
- package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
- package/dist/composables/CompositionViewable.d.ts +33 -0
- package/dist/composables/Displayable.d.ts +47 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/index.d.ts +36 -0
- package/dist/plugins/pinia.d.ts +5 -0
- package/dist/stores/useAuthStore.d.ts +225 -0
- package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
- package/dist/stores/useConfigStore.d.ts +11 -0
- package/dist/utils/SkldrMouseTrap.d.ts +32 -0
- package/package.json +67 -0
- package/src/components/HeatMap.types.ts +15 -0
- package/src/components/HeatMap.vue +354 -0
- package/src/components/PaginatingToolbar.types.ts +48 -0
- package/src/components/PaginatingToolbar.vue +75 -0
- package/src/components/SkMouseTrap.types.ts +3 -0
- package/src/components/SkMouseTrap.vue +70 -0
- package/src/components/SkMouseTrapToolTip.types.ts +41 -0
- package/src/components/SkMouseTrapToolTip.vue +316 -0
- package/src/components/SnackbarService.ts +39 -0
- package/src/components/SnackbarService.vue +71 -0
- package/src/components/StudySession.types.ts +6 -0
- package/src/components/StudySession.vue +670 -0
- package/src/components/StudySessionTimer.vue +121 -0
- package/src/components/auth/UserChip.vue +106 -0
- package/src/components/auth/UserLogin.vue +141 -0
- package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
- package/src/components/auth/UserRegistration.vue +181 -0
- package/src/components/auth/index.ts +4 -0
- package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
- package/src/components/cardRendering/CardLoader.vue +123 -0
- package/src/components/cardRendering/CardViewer.vue +101 -0
- package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
- package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
- package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
- package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
- package/src/components/studentInputs/BaseUserInput.ts +71 -0
- package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
- package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
- package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
- package/src/components/studentInputs/TrueFalse.vue +27 -0
- package/src/components/studentInputs/UserInputNumber.vue +63 -0
- package/src/components/studentInputs/UserInputString.vue +89 -0
- package/src/components/studentInputs/fillInInput.vue +71 -0
- package/src/composables/CompositionViewable.ts +180 -0
- package/src/composables/Displayable.ts +133 -0
- package/src/composables/index.ts +2 -0
- package/src/index.ts +79 -0
- package/src/plugins/pinia.ts +24 -0
- package/src/stores/useAuthStore.ts +92 -0
- package/src/stores/useCardPreviewModeStore.ts +32 -0
- package/src/stores/useConfigStore.ts +60 -0
- package/src/utils/SkldrMouseTrap.ts +141 -0
- package/src/vue-shims.d.ts +5 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="sessionPrepared" class="StudySession">
|
|
3
|
+
<v-row align="center">
|
|
4
|
+
<v-col>
|
|
5
|
+
<h1 class="text-h3">
|
|
6
|
+
{{ courseNames[courseID] }}:
|
|
7
|
+
<v-progress-circular v-if="loading" color="primary" indeterminate size="32" width="4" />
|
|
8
|
+
</h1>
|
|
9
|
+
<v-spacer></v-spacer>
|
|
10
|
+
</v-col>
|
|
11
|
+
</v-row>
|
|
12
|
+
|
|
13
|
+
<br />
|
|
14
|
+
|
|
15
|
+
<div v-if="sessionFinished" class="text-h4">
|
|
16
|
+
<p>Study session finished! Great job!</p>
|
|
17
|
+
<p v-if="sessionController">{{ sessionController.report }}</p>
|
|
18
|
+
<p>
|
|
19
|
+
Start <a @click="$emit('session-finished')">another study session</a>, or try
|
|
20
|
+
<router-link :to="`/edit/${courseID}`">adding some new content</router-link> to challenge yourself and others!
|
|
21
|
+
</p>
|
|
22
|
+
<heat-map :activity-records-getter="() => user.getActivityRecords()" />
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div v-else ref="shadowWrapper">
|
|
26
|
+
<card-viewer
|
|
27
|
+
ref="cardViewer"
|
|
28
|
+
:class="loading ? 'muted' : ''"
|
|
29
|
+
:view="view"
|
|
30
|
+
:data="data"
|
|
31
|
+
:card_id="cardID"
|
|
32
|
+
:course_id="courseID"
|
|
33
|
+
:session-order="cardCount"
|
|
34
|
+
:user_elo="user_elo(courseID)"
|
|
35
|
+
:card_elo="card_elo"
|
|
36
|
+
@emit-response="processResponse($event)"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<br />
|
|
41
|
+
<div v-if="sessionController">
|
|
42
|
+
<span v-for="i in sessionController.failedCount" :key="i" class="text-h5">•</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!--
|
|
46
|
+
todo: reinstate tag editing at session-time ?
|
|
47
|
+
|
|
48
|
+
<div v-if="!sessionFinished && editTags">
|
|
49
|
+
<p>Add tags to this card:</p>
|
|
50
|
+
<sk-tags-input :course-i-d="courseID" :card-i-d="cardID" />
|
|
51
|
+
</div> -->
|
|
52
|
+
|
|
53
|
+
<v-row align="center" class="footer-controls pa-5">
|
|
54
|
+
<v-col cols="auto" class="d-flex flex-grow-0 mr-auto">
|
|
55
|
+
<StudySessionTimer
|
|
56
|
+
:time-remaining="timeRemaining"
|
|
57
|
+
:session-time-limit="sessionTimeLimit"
|
|
58
|
+
@add-time="incrementSessionClock"
|
|
59
|
+
/>
|
|
60
|
+
</v-col>
|
|
61
|
+
|
|
62
|
+
<v-spacer></v-spacer>
|
|
63
|
+
|
|
64
|
+
<v-col cols="auto" class="footer-right">
|
|
65
|
+
<SkMouseTrap />
|
|
66
|
+
</v-col>
|
|
67
|
+
</v-row>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script lang="ts">
|
|
72
|
+
import { defineComponent, PropType } from 'vue';
|
|
73
|
+
import { isQuestionView } from '../composables/CompositionViewable';
|
|
74
|
+
import { alertUser } from './SnackbarService';
|
|
75
|
+
import { ViewComponent } from '../composables';
|
|
76
|
+
import SkMouseTrap from './SkMouseTrap.vue';
|
|
77
|
+
import StudySessionTimer from './StudySessionTimer.vue';
|
|
78
|
+
import HeatMap from './HeatMap.vue';
|
|
79
|
+
import CardViewer from './cardRendering/CardViewer.vue';
|
|
80
|
+
|
|
81
|
+
import {
|
|
82
|
+
ContentSourceID,
|
|
83
|
+
getStudySource,
|
|
84
|
+
isReview,
|
|
85
|
+
StudyContentSource,
|
|
86
|
+
StudySessionItem,
|
|
87
|
+
docIsDeleted,
|
|
88
|
+
CardData,
|
|
89
|
+
CardHistory,
|
|
90
|
+
CardRecord,
|
|
91
|
+
DisplayableData,
|
|
92
|
+
isQuestionRecord,
|
|
93
|
+
CourseRegistrationDoc,
|
|
94
|
+
DataLayerProvider,
|
|
95
|
+
UserDBInterface,
|
|
96
|
+
ClassroomDBInterface,
|
|
97
|
+
} from '@vue-skuilder/db';
|
|
98
|
+
import { SessionController, StudySessionRecord } from '@vue-skuilder/db';
|
|
99
|
+
import { newInterval } from '@vue-skuilder/db';
|
|
100
|
+
import {
|
|
101
|
+
adjustCourseScores,
|
|
102
|
+
CourseElo,
|
|
103
|
+
toCourseElo,
|
|
104
|
+
isCourseElo,
|
|
105
|
+
displayableDataToViewData,
|
|
106
|
+
ViewData,
|
|
107
|
+
Status,
|
|
108
|
+
} from '@vue-skuilder/common';
|
|
109
|
+
import confetti from 'canvas-confetti';
|
|
110
|
+
import moment from 'moment';
|
|
111
|
+
|
|
112
|
+
import { StudySessionConfig } from './StudySession.types';
|
|
113
|
+
|
|
114
|
+
interface StudyRefs {
|
|
115
|
+
shadowWrapper: HTMLDivElement;
|
|
116
|
+
cardViewer: InstanceType<typeof CardViewer>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type StudyInstance = ReturnType<typeof defineComponent> & {
|
|
120
|
+
$refs: StudyRefs;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default defineComponent({
|
|
124
|
+
name: 'StudySession',
|
|
125
|
+
|
|
126
|
+
ref: {} as StudyRefs,
|
|
127
|
+
|
|
128
|
+
components: {
|
|
129
|
+
CardViewer,
|
|
130
|
+
StudySessionTimer,
|
|
131
|
+
SkMouseTrap,
|
|
132
|
+
HeatMap,
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
props: {
|
|
136
|
+
sessionTimeLimit: {
|
|
137
|
+
type: Number,
|
|
138
|
+
required: true,
|
|
139
|
+
},
|
|
140
|
+
contentSources: {
|
|
141
|
+
type: Array as PropType<ContentSourceID[]>,
|
|
142
|
+
required: true,
|
|
143
|
+
},
|
|
144
|
+
user: {
|
|
145
|
+
type: Object as PropType<UserDBInterface>,
|
|
146
|
+
required: true,
|
|
147
|
+
},
|
|
148
|
+
dataLayer: {
|
|
149
|
+
type: Object as PropType<DataLayerProvider>,
|
|
150
|
+
required: true,
|
|
151
|
+
},
|
|
152
|
+
sessionConfig: {
|
|
153
|
+
type: Object as PropType<StudySessionConfig>,
|
|
154
|
+
default: () => ({ likesConfetti: false }),
|
|
155
|
+
},
|
|
156
|
+
getViewComponent: {
|
|
157
|
+
type: Function as PropType<(viewId: string) => ViewComponent>,
|
|
158
|
+
required: true,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
emits: [
|
|
163
|
+
'session-finished',
|
|
164
|
+
'session-started',
|
|
165
|
+
'card-loaded',
|
|
166
|
+
'card-response',
|
|
167
|
+
'time-changed',
|
|
168
|
+
'session-prepared',
|
|
169
|
+
'session-error',
|
|
170
|
+
],
|
|
171
|
+
|
|
172
|
+
data() {
|
|
173
|
+
return {
|
|
174
|
+
// editTags: false,
|
|
175
|
+
cardID: '',
|
|
176
|
+
view: null as ViewComponent | null,
|
|
177
|
+
data: [] as ViewData[],
|
|
178
|
+
courseID: '',
|
|
179
|
+
card_elo: 1000,
|
|
180
|
+
courseNames: {} as { [courseID: string]: string },
|
|
181
|
+
cardCount: 1,
|
|
182
|
+
sessionController: null as SessionController | null,
|
|
183
|
+
sessionPrepared: false,
|
|
184
|
+
sessionFinished: false,
|
|
185
|
+
sessionRecord: [] as StudySessionRecord[],
|
|
186
|
+
percentageRemaining: 100,
|
|
187
|
+
timerIsActive: true,
|
|
188
|
+
loading: false,
|
|
189
|
+
userCourseRegDoc: null as CourseRegistrationDoc | null,
|
|
190
|
+
sessionContentSources: [] as StudyContentSource[],
|
|
191
|
+
timeRemaining: 300, // 5 minutes * 60 seconds
|
|
192
|
+
intervalHandler: null as NodeJS.Timeout | null,
|
|
193
|
+
cardType: '',
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
computed: {
|
|
198
|
+
currentCard(): StudySessionRecord {
|
|
199
|
+
return this.sessionRecord[this.sessionRecord.length - 1];
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// watch: {
|
|
204
|
+
// editCard: {
|
|
205
|
+
// async handler(value: boolean) {
|
|
206
|
+
// if (value) {
|
|
207
|
+
// this.dataInputFormStore.dataInputForm.dataShape = await getCardDataShape(this.courseID, this.cardID);
|
|
208
|
+
|
|
209
|
+
// const cfg = await getCredentialledCourseConfig(this.courseID);
|
|
210
|
+
// this.dataInputFormStore.dataInputForm.course = cfg!;
|
|
211
|
+
|
|
212
|
+
// this.editCardReady = true;
|
|
213
|
+
|
|
214
|
+
// for (const oldField in this.dataInputFormStore.dataInputForm.localStore) {
|
|
215
|
+
// if (oldField) {
|
|
216
|
+
// console.log(`[Study] Removing old data: ${oldField}`);
|
|
217
|
+
// delete this.dataInputFormStore.dataInputForm.localStore[oldField];
|
|
218
|
+
// }
|
|
219
|
+
// }
|
|
220
|
+
|
|
221
|
+
// for (const field in this.data[0]) {
|
|
222
|
+
// if (field) {
|
|
223
|
+
// console.log(`[Study] Writing ${field}: ${this.data[0][field]} to the dataInputForm state...`);
|
|
224
|
+
// this.dataInputFormStore.dataInputForm.localStore[field] = this.data[0][field];
|
|
225
|
+
// }
|
|
226
|
+
// }
|
|
227
|
+
// } else {
|
|
228
|
+
// this.editCardReady = false;
|
|
229
|
+
// }
|
|
230
|
+
// },
|
|
231
|
+
// },
|
|
232
|
+
// },
|
|
233
|
+
|
|
234
|
+
async created() {
|
|
235
|
+
this.userCourseRegDoc = await this.user.getCourseRegistrationsDoc();
|
|
236
|
+
console.log('[StudySession] Created lifecycle hook - starting initSession');
|
|
237
|
+
await this.initSession();
|
|
238
|
+
console.log('[StudySession] InitSession completed in created hook');
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
methods: {
|
|
242
|
+
user_elo(courseID: string): CourseElo {
|
|
243
|
+
const courseDoc = this.userCourseRegDoc!.courses.find((c) => c.courseID === courseID);
|
|
244
|
+
if (courseDoc) {
|
|
245
|
+
return toCourseElo(courseDoc.elo);
|
|
246
|
+
}
|
|
247
|
+
return toCourseElo(undefined);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
handleClassroomMessage() {
|
|
251
|
+
return (v: unknown) => {
|
|
252
|
+
alertUser({
|
|
253
|
+
text: this.user?.getUsername() || '[Unknown user]',
|
|
254
|
+
status: Status.ok,
|
|
255
|
+
});
|
|
256
|
+
console.log(`[StudySession] There was a change in the classroom DB:`);
|
|
257
|
+
console.log(`[StudySession] change: ${v}`);
|
|
258
|
+
console.log(`[StudySession] Stringified change: ${JSON.stringify(v)}`);
|
|
259
|
+
return {};
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
incrementSessionClock() {
|
|
264
|
+
const max = 60 * this.sessionTimeLimit - this.timeRemaining;
|
|
265
|
+
this.sessionController!.addTime(Math.min(max, 60));
|
|
266
|
+
this.tick();
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
tick() {
|
|
270
|
+
this.timeRemaining = this.sessionController!.secondsRemaining;
|
|
271
|
+
|
|
272
|
+
this.percentageRemaining =
|
|
273
|
+
this.timeRemaining > 60
|
|
274
|
+
? 100 * (this.timeRemaining / (60 * this.sessionTimeLimit))
|
|
275
|
+
: 100 * (this.timeRemaining / 60);
|
|
276
|
+
|
|
277
|
+
this.$emit('time-changed', this.timeRemaining);
|
|
278
|
+
|
|
279
|
+
if (this.timeRemaining === 0) {
|
|
280
|
+
clearInterval(this.intervalHandler!);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
async initSession() {
|
|
285
|
+
let sessionClassroomDBs: ClassroomDBInterface[] = [];
|
|
286
|
+
try {
|
|
287
|
+
console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);
|
|
288
|
+
console.log('[StudySession] Beginning preparation process');
|
|
289
|
+
|
|
290
|
+
this.sessionContentSources = (
|
|
291
|
+
await Promise.all(
|
|
292
|
+
this.contentSources.map(async (s) => {
|
|
293
|
+
try {
|
|
294
|
+
return await getStudySource(s, this.user);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error(`Failed to load study source: ${s.type}/${s.id}`, e);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
)
|
|
301
|
+
).filter((s) => s !== null);
|
|
302
|
+
|
|
303
|
+
this.timeRemaining = this.sessionTimeLimit * 60;
|
|
304
|
+
|
|
305
|
+
sessionClassroomDBs = await Promise.all(
|
|
306
|
+
this.contentSources
|
|
307
|
+
.filter((s) => s.type === 'classroom')
|
|
308
|
+
.map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
sessionClassroomDBs.forEach((db) => {
|
|
312
|
+
// db.setChangeFcn(this.handleClassroomMessage());
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
this.sessionController = new SessionController(this.sessionContentSources, 60 * this.sessionTimeLimit);
|
|
316
|
+
this.sessionController.sessionRecord = this.sessionRecord;
|
|
317
|
+
|
|
318
|
+
await this.sessionController.prepareSession();
|
|
319
|
+
this.intervalHandler = setInterval(this.tick, 1000);
|
|
320
|
+
|
|
321
|
+
this.sessionPrepared = true;
|
|
322
|
+
|
|
323
|
+
console.log('[StudySession] Session preparation complete, emitting session-prepared event');
|
|
324
|
+
this.$emit('session-prepared');
|
|
325
|
+
console.log('[StudySession] Event emission completed');
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('[StudySession] Error during session preparation:', error);
|
|
328
|
+
// Notify parent component about the error
|
|
329
|
+
this.$emit('session-error', { message: 'Failed to prepare study session', error });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
this.contentSources
|
|
334
|
+
.filter((s) => s.type === 'course')
|
|
335
|
+
.forEach(
|
|
336
|
+
async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
console.log(`[StudySession] Session created:
|
|
340
|
+
${this.sessionController?.toString() || 'Session controller not initialized'}
|
|
341
|
+
User courses: ${this.contentSources
|
|
342
|
+
.filter((s) => s.type === 'course')
|
|
343
|
+
.map((c) => c.id)
|
|
344
|
+
.toString()}
|
|
345
|
+
User classrooms: ${sessionClassroomDBs.map((db: any) => db._id).toString() || 'No classrooms'}
|
|
346
|
+
`);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('[StudySession] Error during final session setup:', error);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (this.sessionController) {
|
|
352
|
+
try {
|
|
353
|
+
this.$emit('session-started');
|
|
354
|
+
this.loadCard(this.sessionController.nextCard());
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error('[StudySession] Error loading next card:', error);
|
|
357
|
+
this.$emit('session-error', { message: 'Failed to load study card', error });
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
console.error('[StudySession] Cannot load card: session controller not initialized');
|
|
361
|
+
this.$emit('session-error', { message: 'Study session initialization failed' });
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
countCardViews(course_id: string, card_id: string): number {
|
|
366
|
+
return this.sessionRecord.filter((r) => r.card.course_id === course_id && r.card.card_id === card_id).length;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
async processResponse(this: StudyInstance, r: CardRecord) {
|
|
370
|
+
this.$emit('card-response', r);
|
|
371
|
+
|
|
372
|
+
this.timerIsActive = true;
|
|
373
|
+
|
|
374
|
+
r.cardID = this.cardID;
|
|
375
|
+
r.courseID = this.courseID;
|
|
376
|
+
this.currentCard.records.push(r);
|
|
377
|
+
|
|
378
|
+
console.log(`[StudySession] StudySession.processResponse is running...`);
|
|
379
|
+
const cardHistory = this.logCardRecord(r);
|
|
380
|
+
|
|
381
|
+
if (isQuestionRecord(r)) {
|
|
382
|
+
console.log(`[StudySession] Question is ${r.isCorrect ? '' : 'in'}correct`);
|
|
383
|
+
if (r.isCorrect) {
|
|
384
|
+
try {
|
|
385
|
+
if (this.$refs.shadowWrapper) {
|
|
386
|
+
this.$refs.shadowWrapper.setAttribute(
|
|
387
|
+
'style',
|
|
388
|
+
`--r: ${255 * (1 - (r.performance as number))}; --g:${255}`
|
|
389
|
+
);
|
|
390
|
+
this.$refs.shadowWrapper.classList.add('correct');
|
|
391
|
+
}
|
|
392
|
+
} catch (e) {
|
|
393
|
+
// swallow error
|
|
394
|
+
console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (this.sessionConfig.likesConfetti) {
|
|
398
|
+
confetti({
|
|
399
|
+
origin: {
|
|
400
|
+
y: 1,
|
|
401
|
+
x: 0.25 + 0.5 * Math.random(),
|
|
402
|
+
},
|
|
403
|
+
disableForReducedMotion: true,
|
|
404
|
+
angle: 60 + 60 * Math.random(),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (r.priorAttemps === 0) {
|
|
409
|
+
const item: StudySessionItem = {
|
|
410
|
+
...this.currentCard.item,
|
|
411
|
+
};
|
|
412
|
+
this.loadCard(this.sessionController!.nextCard('dismiss-success'));
|
|
413
|
+
|
|
414
|
+
cardHistory.then((history: CardHistory<CardRecord>) => {
|
|
415
|
+
this.scheduleReview(history, item);
|
|
416
|
+
if (history.records.length === 1) {
|
|
417
|
+
this.updateUserAndCardElo(0.5 + (r.performance as number) / 2, this.courseID, this.cardID);
|
|
418
|
+
} else {
|
|
419
|
+
const k = Math.ceil(32 / history.records.length);
|
|
420
|
+
this.updateUserAndCardElo(0.5 + (r.performance as number) / 2, this.courseID, this.cardID, k);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
this.loadCard(this.sessionController!.nextCard('marked-failed'));
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
try {
|
|
428
|
+
if (this.$refs.shadowWrapper) {
|
|
429
|
+
this.$refs.shadowWrapper.classList.add('incorrect');
|
|
430
|
+
}
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// swallow error
|
|
433
|
+
console.warn(`[StudySession] Error setting shadowWrapper style: ${e}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
cardHistory.then((history: CardHistory<CardRecord>) => {
|
|
437
|
+
if (history.records.length !== 1 && r.priorAttemps === 0) {
|
|
438
|
+
this.updateUserAndCardElo(0, this.courseID, this.cardID);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// [ ] v3 version. Keep an eye on this -
|
|
443
|
+
if (isQuestionView(this.$refs.cardViewer?.$refs.activeView)) {
|
|
444
|
+
const view = this.$refs.cardViewer.$refs.activeView;
|
|
445
|
+
|
|
446
|
+
if (this.currentCard.records.length >= view.maxAttemptsPerView) {
|
|
447
|
+
const sessionViews: number = this.countCardViews(this.courseID, this.cardID);
|
|
448
|
+
if (sessionViews >= view.maxSessionViews) {
|
|
449
|
+
this.loadCard(this.sessionController!.nextCard('dismiss-failed'));
|
|
450
|
+
this.updateUserAndCardElo(0, this.courseID, this.cardID);
|
|
451
|
+
} else {
|
|
452
|
+
this.loadCard(this.sessionController!.nextCard('marked-failed'));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
this.loadCard(this.sessionController!.nextCard('dismiss-success'));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.clearFeedbackShadow();
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
async updateUserAndCardElo(userScore: number, course_id: string, card_id: string, k?: number) {
|
|
465
|
+
if (k) {
|
|
466
|
+
console.warn(`k value interpretation not currently implemented`);
|
|
467
|
+
}
|
|
468
|
+
const courseDB = this.dataLayer.getCourseDB(this.currentCard.card.course_id);
|
|
469
|
+
const userElo = toCourseElo(this.userCourseRegDoc!.courses.find((c) => c.courseID === course_id)!.elo);
|
|
470
|
+
const cardElo = (await courseDB.getCardEloData([this.currentCard.card.card_id]))[0];
|
|
471
|
+
|
|
472
|
+
if (cardElo && userElo) {
|
|
473
|
+
const eloUpdate = adjustCourseScores(userElo, cardElo, userScore);
|
|
474
|
+
this.userCourseRegDoc!.courses.find((c) => c.courseID === course_id)!.elo = eloUpdate.userElo;
|
|
475
|
+
|
|
476
|
+
Promise.all([
|
|
477
|
+
this.user!.updateUserElo(course_id, eloUpdate.userElo),
|
|
478
|
+
courseDB.updateCardElo(card_id, eloUpdate.cardElo),
|
|
479
|
+
]).then((results) => {
|
|
480
|
+
const user = results[0];
|
|
481
|
+
const card = results[1];
|
|
482
|
+
|
|
483
|
+
if (user.ok && card && card.ok) {
|
|
484
|
+
console.log(
|
|
485
|
+
`[StudySession] Updated ELOS:
|
|
486
|
+
\tUser: ${JSON.stringify(eloUpdate.userElo)})
|
|
487
|
+
\tCard: ${JSON.stringify(eloUpdate.cardElo)})
|
|
488
|
+
`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
clearFeedbackShadow() {
|
|
496
|
+
setTimeout(() => {
|
|
497
|
+
try {
|
|
498
|
+
if (this.$refs.shadowWrapper) {
|
|
499
|
+
(this.$refs.shadowWrapper as HTMLElement).classList.remove('correct', 'incorrect');
|
|
500
|
+
}
|
|
501
|
+
} catch (e) {
|
|
502
|
+
// swallow error
|
|
503
|
+
console.warn(`[StudySession] Error clearing shadowWrapper style: ${e}`);
|
|
504
|
+
}
|
|
505
|
+
}, 1250);
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
async logCardRecord(r: CardRecord): Promise<CardHistory<CardRecord>> {
|
|
509
|
+
return await this.user!.putCardRecord(r);
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
async scheduleReview(history: CardHistory<CardRecord>, item: StudySessionItem) {
|
|
513
|
+
const nextInterval = newInterval(this.$props.user, history);
|
|
514
|
+
const nextReviewTime = moment.utc().add(nextInterval, 'seconds');
|
|
515
|
+
|
|
516
|
+
if (isReview(item)) {
|
|
517
|
+
console.log(`[StudySession] Removing previously scheduled review for: ${item.cardID}`);
|
|
518
|
+
this.user!.removeScheduledCardReview(item.reviewID);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.user!.scheduleCardReview({
|
|
522
|
+
user: this.user!.getUsername(),
|
|
523
|
+
course_id: history.courseID,
|
|
524
|
+
card_id: history.cardID,
|
|
525
|
+
time: nextReviewTime,
|
|
526
|
+
scheduledFor: item.contentSourceType,
|
|
527
|
+
schedulingAgentId: item.contentSourceID,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
async loadCard(item: StudySessionItem | null) {
|
|
532
|
+
if (this.loading) {
|
|
533
|
+
console.warn(`Attempted to load card while loading another...`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
console.log(`[StudySession] loading: ${JSON.stringify(item)}`);
|
|
538
|
+
if (item === null) {
|
|
539
|
+
this.sessionFinished = true;
|
|
540
|
+
this.$emit('session-finished', this.sessionRecord);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
this.cardType = item.status;
|
|
544
|
+
|
|
545
|
+
const qualified_id = item.qualifiedID;
|
|
546
|
+
this.loading = true;
|
|
547
|
+
const [_courseID, _cardID] = qualified_id.split('-');
|
|
548
|
+
|
|
549
|
+
console.log(`[StudySession] Now displaying: ${qualified_id}`);
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const tmpCardData = await this.dataLayer.getCourseDB(_courseID).getCourseDoc<CardData>(_cardID);
|
|
553
|
+
|
|
554
|
+
if (!isCourseElo(tmpCardData.elo)) {
|
|
555
|
+
tmpCardData.elo = toCourseElo(tmpCardData.elo);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const tmpView: ViewComponent = this.getViewComponent(tmpCardData.id_view);
|
|
559
|
+
const tmpDataDocs = tmpCardData.id_displayable_data.map((id) => {
|
|
560
|
+
return this.dataLayer.getCourseDB(_courseID).getCourseDoc<DisplayableData>(id, {
|
|
561
|
+
attachments: true,
|
|
562
|
+
binary: true,
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const tmpData = [];
|
|
567
|
+
|
|
568
|
+
for (const docPromise of tmpDataDocs) {
|
|
569
|
+
const doc = await docPromise;
|
|
570
|
+
tmpData.unshift(displayableDataToViewData(doc));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
this.cardCount++;
|
|
574
|
+
this.data = tmpData;
|
|
575
|
+
this.view = tmpView;
|
|
576
|
+
this.cardID = _cardID;
|
|
577
|
+
this.courseID = _courseID;
|
|
578
|
+
this.card_elo = tmpCardData.elo.global.score;
|
|
579
|
+
|
|
580
|
+
this.sessionRecord.push({
|
|
581
|
+
card: {
|
|
582
|
+
course_id: _courseID,
|
|
583
|
+
card_id: _cardID,
|
|
584
|
+
card_elo: tmpCardData.elo.global.score,
|
|
585
|
+
},
|
|
586
|
+
item: item,
|
|
587
|
+
records: [],
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
this.$emit('card-loaded', {
|
|
591
|
+
courseID: _courseID,
|
|
592
|
+
cardID: _cardID,
|
|
593
|
+
cardCount: this.cardCount,
|
|
594
|
+
});
|
|
595
|
+
} catch (e) {
|
|
596
|
+
console.warn(`[StudySession] Error loading card ${JSON.stringify(item)}:\n\t${JSON.stringify(e)}, ${e}`);
|
|
597
|
+
this.loading = false;
|
|
598
|
+
|
|
599
|
+
const err = e as Error;
|
|
600
|
+
if (docIsDeleted(err) && isReview(item)) {
|
|
601
|
+
console.warn(`Card was deleted: ${qualified_id}`);
|
|
602
|
+
this.user!.removeScheduledCardReview(item.reviewID);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
this.loadCard(this.sessionController!.nextCard('dismiss-error'));
|
|
606
|
+
} finally {
|
|
607
|
+
this.loading = false;
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
</script>
|
|
613
|
+
|
|
614
|
+
<style scoped>
|
|
615
|
+
.footer-controls {
|
|
616
|
+
position: fixed;
|
|
617
|
+
bottom: 0;
|
|
618
|
+
background-color: var(--v-background); /* Match your app's background color */
|
|
619
|
+
z-index: 100;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.footer-right {
|
|
623
|
+
position: fixed;
|
|
624
|
+
bottom: 0;
|
|
625
|
+
right: 0;
|
|
626
|
+
background-color: var(--v-background); /* Match your app's background color */
|
|
627
|
+
z-index: 100;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.correct {
|
|
631
|
+
animation: varFade 1250ms ease-out;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.incorrect {
|
|
635
|
+
animation: purpleFade 1250ms ease-out;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
a {
|
|
639
|
+
text-decoration: underline;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
@keyframes varFade {
|
|
643
|
+
0% {
|
|
644
|
+
box-shadow: rgba(var(--r), var(--g), 0, 0.25) 0px 7px 8px -4px, rgba(var(--r), var(--g), 0, 0.25) 0px 12px 17px 2px,
|
|
645
|
+
rgba(var(--r), var(--g), 0, 0.25) 0px 5px 22px 4px;
|
|
646
|
+
}
|
|
647
|
+
100% {
|
|
648
|
+
box-shadow: rgba(0, 150, 0, 0) 0px 0px;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
@keyframes greenFade {
|
|
653
|
+
0% {
|
|
654
|
+
box-shadow: rgba(0, 150, 0, 0.25) 0px 7px 8px -4px, rgba(0, 150, 0, 0.25) 0px 12px 17px 2px,
|
|
655
|
+
rgba(0, 150, 0, 0.25) 0px 5px 22px 4px;
|
|
656
|
+
}
|
|
657
|
+
100% {
|
|
658
|
+
box-shadow: rgba(0, 150, 0, 0) 0px 0px;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
@keyframes purpleFade {
|
|
662
|
+
0% {
|
|
663
|
+
box-shadow: rgba(115, 0, 75, 0.25) 0px 7px 8px -4px, rgba(115, 0, 75, 0.25) 0px 12px 17px 2px,
|
|
664
|
+
rgba(115, 0, 75, 0.25) 0px 5px 22px 4px;
|
|
665
|
+
}
|
|
666
|
+
100% {
|
|
667
|
+
box-shadow: rgba(115, 0, 75, 0) 0px 0px;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
</style>
|