@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,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container class="pa-0">
|
|
3
|
+
<v-text-field
|
|
4
|
+
ref="input"
|
|
5
|
+
v-model="answer"
|
|
6
|
+
prepend-icon="edit"
|
|
7
|
+
:autofocus="autofocus"
|
|
8
|
+
row-height="24"
|
|
9
|
+
toggle-keys="[13,32]"
|
|
10
|
+
class="text-h5"
|
|
11
|
+
:rules="[isNumeric]"
|
|
12
|
+
@keyup.enter="submitAnswer(makeNumeric(answer))"
|
|
13
|
+
></v-text-field>
|
|
14
|
+
</v-container>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import { Answer } from '@vue-skuilder/common';
|
|
19
|
+
import UserInput from './BaseUserInput';
|
|
20
|
+
import { defineComponent } from 'vue';
|
|
21
|
+
|
|
22
|
+
type InputNumberRefs = {
|
|
23
|
+
input: HTMLInputElement;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type InputNumberInstance = ReturnType<typeof defineComponent> & {
|
|
27
|
+
$refs: InputNumberRefs;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default defineComponent({
|
|
31
|
+
name: 'UserInputNumber',
|
|
32
|
+
|
|
33
|
+
ref: {} as InputNumberRefs,
|
|
34
|
+
|
|
35
|
+
extends: UserInput,
|
|
36
|
+
|
|
37
|
+
methods: {
|
|
38
|
+
mounted(this: InputNumberInstance) {
|
|
39
|
+
this.$refs.input.focus();
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
isNumeric(s: string): boolean {
|
|
43
|
+
return !isNaN(Number.parseFloat(s));
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
makeNumeric(num: Answer): number {
|
|
47
|
+
if (typeof num === 'string') {
|
|
48
|
+
return Number.parseFloat(num);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error('Expected a string, got ' + typeof num);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<style scoped>
|
|
58
|
+
.userInput {
|
|
59
|
+
border: none;
|
|
60
|
+
text-align: center;
|
|
61
|
+
border-bottom: 1px black;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="user-input-container">
|
|
3
|
+
<input
|
|
4
|
+
v-model="answer"
|
|
5
|
+
:autofocus="autofocus"
|
|
6
|
+
type="text"
|
|
7
|
+
class="user-input-string"
|
|
8
|
+
ref="input"
|
|
9
|
+
@keyup.enter="submitAnswer(answer)"
|
|
10
|
+
/>
|
|
11
|
+
</span>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
// @compositionstart="onCompositionStart"
|
|
16
|
+
// @compositionend="onCompositionEnd"
|
|
17
|
+
import UserInput from './BaseUserInput';
|
|
18
|
+
import { defineComponent } from 'vue';
|
|
19
|
+
|
|
20
|
+
export default defineComponent({
|
|
21
|
+
name: 'UserInputString',
|
|
22
|
+
extends: UserInput,
|
|
23
|
+
|
|
24
|
+
props: {
|
|
25
|
+
icon: {
|
|
26
|
+
type: Boolean,
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
computed: {
|
|
32
|
+
prependIcon(): string {
|
|
33
|
+
return this.icon ? 'edit' : '';
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
mounted() {
|
|
38
|
+
(this.$refs.input as HTMLInputElement)?.focus();
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
methods: {
|
|
42
|
+
// debugInput(e: Event) {
|
|
43
|
+
// const target = e.target as HTMLInputElement;
|
|
44
|
+
// console.log('Input event:', {
|
|
45
|
+
// value: target.value,
|
|
46
|
+
// currentAnswer: this.answer,
|
|
47
|
+
// target,
|
|
48
|
+
// });
|
|
49
|
+
// },
|
|
50
|
+
// debugKeyup(e: KeyboardEvent) {
|
|
51
|
+
// const target = e.target as HTMLInputElement;
|
|
52
|
+
// console.log('Keyup event:', {
|
|
53
|
+
// key: e.key,
|
|
54
|
+
// value: target.value,
|
|
55
|
+
// currentAnswer: this.answer,
|
|
56
|
+
// target,
|
|
57
|
+
// });
|
|
58
|
+
// },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
.user-input-container {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
min-width: 6em;
|
|
67
|
+
vertical-align: baseline;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.user-input-string {
|
|
71
|
+
display: inline-block;
|
|
72
|
+
background: transparent;
|
|
73
|
+
border: none;
|
|
74
|
+
border-bottom: 1px solid currentColor;
|
|
75
|
+
color: currentColor;
|
|
76
|
+
font-family: inherit;
|
|
77
|
+
font-size: inherit;
|
|
78
|
+
line-height: inherit;
|
|
79
|
+
padding: 0;
|
|
80
|
+
margin: 0;
|
|
81
|
+
text-align: center;
|
|
82
|
+
width: 100%;
|
|
83
|
+
outline: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.user-input-string:focus {
|
|
87
|
+
border-bottom: 2px solid currentColor;
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span v-if="radioType" class="text-h5 underline"
|
|
3
|
+
> </span
|
|
4
|
+
>
|
|
5
|
+
<user-input-string v-else id="input" :icon="false" type="text" :value="processedText" />
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { defineComponent, ref, computed, onMounted } from 'vue';
|
|
10
|
+
import UserInputString from './UserInputString.vue';
|
|
11
|
+
|
|
12
|
+
export default defineComponent({
|
|
13
|
+
name: 'FillInInput',
|
|
14
|
+
|
|
15
|
+
components: {
|
|
16
|
+
UserInputString,
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
props: {
|
|
20
|
+
text: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
setup(props) {
|
|
27
|
+
// State
|
|
28
|
+
const inputType = ref<'text' | 'radio'>('text');
|
|
29
|
+
const processedText = ref('');
|
|
30
|
+
|
|
31
|
+
// Computed
|
|
32
|
+
const radioType = computed(() => {
|
|
33
|
+
return props.text.split('||').length > 1;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Process text on mount
|
|
37
|
+
onMounted(() => {
|
|
38
|
+
console.log(`fillinCreated w/ text: ${props.text}`);
|
|
39
|
+
|
|
40
|
+
// Remove mustache syntax
|
|
41
|
+
processedText.value = props.text.substring(2, props.text.length - 2);
|
|
42
|
+
|
|
43
|
+
console.log(`fillin text trimmed to: ${processedText.value}`);
|
|
44
|
+
|
|
45
|
+
const split = processedText.value.split('||');
|
|
46
|
+
if (split.length > 1) {
|
|
47
|
+
inputType.value = 'radio';
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
inputType,
|
|
53
|
+
radioType,
|
|
54
|
+
processedText,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<style scoped>
|
|
61
|
+
#input {
|
|
62
|
+
display: inline-block;
|
|
63
|
+
min-width: 4em;
|
|
64
|
+
text-align: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.underline {
|
|
68
|
+
text-decoration: underline;
|
|
69
|
+
text-decoration-style: solid 14px;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/base-course/CompositionViewable.ts
|
|
2
|
+
|
|
3
|
+
import moment from 'moment';
|
|
4
|
+
import { computed, ComputedRef, ref, Ref } from 'vue';
|
|
5
|
+
import { CardRecord, QuestionRecord } from '@vue-skuilder/db';
|
|
6
|
+
import { HotKey } from '../utils/SkldrMouseTrap';
|
|
7
|
+
import { Question } from './Displayable';
|
|
8
|
+
import { ViewData, Answer } from '@vue-skuilder/common';
|
|
9
|
+
|
|
10
|
+
// Core interfaces to ensure type safety
|
|
11
|
+
export interface ViewableUtils {
|
|
12
|
+
startTime: Ref<moment.Moment>;
|
|
13
|
+
hotKeys: Ref<HotKey[]>;
|
|
14
|
+
timeSpent: ComputedRef<number>;
|
|
15
|
+
logger: ViewableLogger;
|
|
16
|
+
getURL: (item: string, dataShapeIndex?: number) => string;
|
|
17
|
+
emitResponse: (record: CardRecord) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ViewableLogger {
|
|
21
|
+
log: (message?: unknown, ...params: unknown[]) => void;
|
|
22
|
+
error: (message?: unknown, ...params: unknown[]) => void;
|
|
23
|
+
warn: (message?: unknown, ...params: unknown[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface QuestionViewUtils<Q extends Question> {
|
|
27
|
+
priorSessionViews: Ref<number>;
|
|
28
|
+
priorAttempts: Ref<number>;
|
|
29
|
+
priorAnswers: Ref<[Answer, string][]>;
|
|
30
|
+
maxAttemptsPerView: Ref<number>;
|
|
31
|
+
maxSessionViews: Ref<number>;
|
|
32
|
+
question: Ref<Q | undefined>;
|
|
33
|
+
submitAnswer: (answer: Answer, submittingClass?: string) => QuestionRecord;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Base composable for viewable functionality
|
|
37
|
+
export function useViewable(
|
|
38
|
+
props: { data: ViewData[] },
|
|
39
|
+
emit: (event: string, ...args: unknown[]) => void,
|
|
40
|
+
componentName: string
|
|
41
|
+
): ViewableUtils {
|
|
42
|
+
const startTime = ref(moment.utc());
|
|
43
|
+
const hotKeys = ref<HotKey[]>([]);
|
|
44
|
+
|
|
45
|
+
const logger: ViewableLogger = {
|
|
46
|
+
log: (message?: unknown, ...params: unknown[]) =>
|
|
47
|
+
console.log(`[${componentName}]: `, message, ...params),
|
|
48
|
+
error: (message?: unknown, ...params: unknown[]) =>
|
|
49
|
+
console.error(`[${componentName}]: `, message, ...params),
|
|
50
|
+
warn: (message?: unknown, ...params: unknown[]) =>
|
|
51
|
+
console.warn(`[${componentName}]: `, message, ...params),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const timeSpent = computed(() => Math.abs(moment.utc().diff(startTime.value, 'milliseconds')));
|
|
55
|
+
|
|
56
|
+
const getURL = (item: string, dataShapeIndex = 0): string => {
|
|
57
|
+
try {
|
|
58
|
+
if (props.data[dataShapeIndex]?.[item]) {
|
|
59
|
+
return URL.createObjectURL(props.data[dataShapeIndex][item] as Blob);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error('Error creating URL for item:', item, error);
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const emitResponse = (record: CardRecord) => {
|
|
68
|
+
emit('emitResponse', record);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
startTime,
|
|
73
|
+
hotKeys,
|
|
74
|
+
timeSpent,
|
|
75
|
+
logger,
|
|
76
|
+
getURL,
|
|
77
|
+
emitResponse,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Question view composable
|
|
82
|
+
export function useQuestionView<Q extends Question>(
|
|
83
|
+
viewableUtils: ViewableUtils
|
|
84
|
+
// modifyDifficulty?: number
|
|
85
|
+
): QuestionViewUtils<Q> {
|
|
86
|
+
const priorSessionViews = ref(0);
|
|
87
|
+
const priorAttempts = ref(0);
|
|
88
|
+
const priorAnswers = ref<[Answer, string][]>([]);
|
|
89
|
+
const maxAttemptsPerView = ref(3);
|
|
90
|
+
const maxSessionViews = ref(1);
|
|
91
|
+
const question = ref<Q>();
|
|
92
|
+
|
|
93
|
+
const submitAnswer = (answer: Answer, submittingClass?: string): QuestionRecord => {
|
|
94
|
+
viewableUtils.logger.log('submitAnswer called...');
|
|
95
|
+
|
|
96
|
+
if (!question.value) {
|
|
97
|
+
throw new Error('Question not initialized');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
priorAnswers.value.push([answer, submittingClass ?? '']);
|
|
101
|
+
|
|
102
|
+
const evaluation = question.value.evaluate(answer, viewableUtils.timeSpent.value);
|
|
103
|
+
|
|
104
|
+
viewableUtils.logger.log(`evaluation of answer ${answer}:`, evaluation);
|
|
105
|
+
|
|
106
|
+
const record: QuestionRecord = {
|
|
107
|
+
...evaluation,
|
|
108
|
+
priorAttemps: priorAttempts.value,
|
|
109
|
+
courseID: '',
|
|
110
|
+
cardID: '',
|
|
111
|
+
timeSpent: viewableUtils.timeSpent.value,
|
|
112
|
+
timeStamp: viewableUtils.startTime.value,
|
|
113
|
+
userAnswer: answer,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (!evaluation.isCorrect) {
|
|
117
|
+
priorAttempts.value++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
viewableUtils.emitResponse(record);
|
|
121
|
+
return record;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
priorSessionViews,
|
|
126
|
+
priorAttempts,
|
|
127
|
+
priorAnswers,
|
|
128
|
+
maxAttemptsPerView,
|
|
129
|
+
maxSessionViews,
|
|
130
|
+
question,
|
|
131
|
+
submitAnswer,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Helper functions
|
|
136
|
+
export function isQuestionView(component: any): component is QuestionViewUtils<Question> {
|
|
137
|
+
return 'submitAnswer' in component && 'priorAttempts' in component;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Example usage in a child component:
|
|
141
|
+
/*
|
|
142
|
+
import { defineComponent, PropType } from 'vue';
|
|
143
|
+
import { useViewable, useQuestionView } from './CompositionViewable';
|
|
144
|
+
import { MyCustomQuestion } from './types';
|
|
145
|
+
|
|
146
|
+
export default defineComponent({
|
|
147
|
+
name: 'MyCustomQuestionView',
|
|
148
|
+
props: {
|
|
149
|
+
data: {
|
|
150
|
+
type: Array as PropType<ViewData[]>,
|
|
151
|
+
required: true,
|
|
152
|
+
},
|
|
153
|
+
modifyDifficulty: Number,
|
|
154
|
+
},
|
|
155
|
+
setup(props, { emit }) {
|
|
156
|
+
const viewableUtils = useViewable(props, emit, 'MyCustomQuestionView');
|
|
157
|
+
const questionUtils = useQuestionView<MyCustomQuestion>(viewableUtils, props.modifyDifficulty);
|
|
158
|
+
|
|
159
|
+
// Initialize question
|
|
160
|
+
questionUtils.question.value = new MyCustomQuestion(props.data);
|
|
161
|
+
|
|
162
|
+
// Add custom logic here
|
|
163
|
+
const customMethod = () => {
|
|
164
|
+
viewableUtils.logger.log('Custom method called');
|
|
165
|
+
// ...
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
...viewableUtils,
|
|
170
|
+
...questionUtils,
|
|
171
|
+
customMethod,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
template: `
|
|
175
|
+
<div data-viewable="MyCustomQuestionView">
|
|
176
|
+
<!-- Your template here -->
|
|
177
|
+
</div>
|
|
178
|
+
`
|
|
179
|
+
});
|
|
180
|
+
*/
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type DefineComponent, defineComponent } from 'vue';
|
|
2
|
+
import { FieldType, DataShape, ViewData, Answer, Evaluation } from '@vue-skuilder/common';
|
|
3
|
+
|
|
4
|
+
// [ ] #vue3 - post migration, specify this more precisely (no longer a hodge-podge)
|
|
5
|
+
export type ViewComponent =
|
|
6
|
+
| DefineComponent<unknown, unknown, unknown>
|
|
7
|
+
| ReturnType<typeof defineComponent>;
|
|
8
|
+
|
|
9
|
+
export function isDefineComponent(
|
|
10
|
+
v: ViewComponent
|
|
11
|
+
): v is DefineComponent<unknown, unknown, unknown> {
|
|
12
|
+
return (v as DefineComponent<unknown, unknown, unknown>).__isFragment !== undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// export function isComponentOptions(v: ViewComponent): v is ComponentOptions<any> {
|
|
16
|
+
// return (v as ComponentOptions<Vue>).name !== undefined;
|
|
17
|
+
// }
|
|
18
|
+
|
|
19
|
+
// tslint:disable-next-line:max-classes-per-file
|
|
20
|
+
export abstract class Displayable {
|
|
21
|
+
public static dataShapes: DataShape[];
|
|
22
|
+
|
|
23
|
+
public static views: Array<ViewComponent>;
|
|
24
|
+
public static seedData?: Array<any>;
|
|
25
|
+
/**
|
|
26
|
+
* True if this displayable content type is meant to have
|
|
27
|
+
* user-submitted questions. False if supplied seedData array
|
|
28
|
+
* is comprehensive for the content type. EG, a SingleDigitAddition
|
|
29
|
+
* type may comprehensively supply 0+0,0+1,...,9+9 as its seed
|
|
30
|
+
* data, and not want any user input.
|
|
31
|
+
*/
|
|
32
|
+
public static acceptsUserData: boolean = true;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
constructor(viewData: ViewData[]) {
|
|
38
|
+
if (viewData.length === 0) {
|
|
39
|
+
throw new Error(`
|
|
40
|
+
Displayable Constructor was called with no view Data.
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
validateData(this.dataShapes(), viewData);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public abstract dataShapes(): DataShape[];
|
|
47
|
+
public abstract views(): Array<ViewComponent>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateData(shape: DataShape[], data: ViewData[]) {
|
|
51
|
+
for (let i = 0; i < shape.length; i++) {
|
|
52
|
+
shape[i].fields.forEach((field, j) => {
|
|
53
|
+
console.log(`[Displayable] shape[${i}].field[${j}]:\n ${JSON.stringify(field)}`);
|
|
54
|
+
if (data[i][field.name] === undefined && field.type !== FieldType.MEDIA_UPLOADS) {
|
|
55
|
+
// throw new Error(`field validation failed:\n\t${field.name}, (${field.type})`);
|
|
56
|
+
console.warn(`[Displayable] missing data`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// tslint:disable-next-line:max-classes-per-file
|
|
63
|
+
export abstract class Question extends Displayable {
|
|
64
|
+
/**
|
|
65
|
+
* returns a yes/no evaluation of a user's answer. Informs the SRS
|
|
66
|
+
* algorithm's decision to expand or reset a card's spacing
|
|
67
|
+
* @param answer
|
|
68
|
+
*/
|
|
69
|
+
protected abstract isCorrect(answer: Answer): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* returns a number from [0,1] representing the user's performance on the question,
|
|
72
|
+
* which informs elo adjustments and SRS multipliers
|
|
73
|
+
*
|
|
74
|
+
* @param answer the user's answer
|
|
75
|
+
* @param timeSpent the time the user spent on the card in milliseconds
|
|
76
|
+
* @returns a rating of the user's displayed skill, from 0-1
|
|
77
|
+
*/
|
|
78
|
+
protected displayedSkill(answer: Answer, timeSpent: number): number {
|
|
79
|
+
console.warn(`Question is running the reference implementation of displayedSkill.
|
|
80
|
+
Consider overriding!`);
|
|
81
|
+
// experts should answer this question in <= 5 secnods (5000 ms)
|
|
82
|
+
const expertSpeed = 5000;
|
|
83
|
+
const userSpeed = Math.min(timeSpent, 10 * expertSpeed);
|
|
84
|
+
|
|
85
|
+
// if userResponse is > 10 x expertSpeed, discount as probably afk / distracted ?
|
|
86
|
+
|
|
87
|
+
const speedPenalty = userSpeed / expertSpeed;
|
|
88
|
+
const speedPenaltyMultiplier = userSpeed > expertSpeed ? Math.pow(0.8, speedPenalty) : 1;
|
|
89
|
+
|
|
90
|
+
let ret = this.isCorrect(answer) ? 1 : 0;
|
|
91
|
+
|
|
92
|
+
ret = ret * speedPenaltyMultiplier;
|
|
93
|
+
|
|
94
|
+
return Math.min(ret, 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
*
|
|
99
|
+
* @param answer the student's answer
|
|
100
|
+
* @param timeSpent the amount of time spent in ms
|
|
101
|
+
* @returns
|
|
102
|
+
*/
|
|
103
|
+
public evaluate(answer: Answer, timeSpent: number): Evaluation {
|
|
104
|
+
return {
|
|
105
|
+
isCorrect: this.isCorrect(answer),
|
|
106
|
+
performance: this.displayedSkill(answer, timeSpent),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
TODO:
|
|
112
|
+
|
|
113
|
+
This class and its interface are critical in this app - it defines the app's
|
|
114
|
+
methodology, or its architecture of methodologies, for making inferences based on
|
|
115
|
+
student performance.
|
|
116
|
+
|
|
117
|
+
Some future directions:
|
|
118
|
+
|
|
119
|
+
displayedSkill() should receive contextual data as well as the direct report of
|
|
120
|
+
a user's interaction with this card. Context includes, eg, the status of the current
|
|
121
|
+
study session (is the user on tilt? have there been leading Qs? have there been distractors?),
|
|
122
|
+
the status of the user's interaction with the card (new? mature? history of resets?)
|
|
123
|
+
|
|
124
|
+
displayedSkill() should reach out to a course (or just the card, or card-type?) in order to receive
|
|
125
|
+
custom skill-dimension evaluators. EG: a card in the ear training course tagged 'articulation'
|
|
126
|
+
could trigger a fetch of a particular evaluator fcn for articulation. In general, we
|
|
127
|
+
want high-dimension evaluations.
|
|
128
|
+
|
|
129
|
+
answers should be processed for the likelihood of non-substantive-incorrectness. eg,
|
|
130
|
+
7 * 4 = 8 is much more likely to be a typo than a substantive error when compared
|
|
131
|
+
with 7 * 4 = 21
|
|
132
|
+
*/
|
|
133
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// // Import and re-export components
|
|
2
|
+
// import './styles/index.scss';
|
|
3
|
+
|
|
4
|
+
export { default as HeatMap } from './components/HeatMap.vue';
|
|
5
|
+
export type { DayData, Color, ActivityRecord } from './components/HeatMap.types';
|
|
6
|
+
|
|
7
|
+
// global keybinding controller
|
|
8
|
+
export { default as SkMouseTrap } from './components/SkMouseTrap.vue';
|
|
9
|
+
export { default as SkMouseTrapToolTip } from './components/SkMouseTrapToolTip.vue';
|
|
10
|
+
export * from './components/SkMouseTrap.types';
|
|
11
|
+
export * from './components/SkMouseTrapToolTip.types';
|
|
12
|
+
export { SkldrMouseTrap } from './utils/SkldrMouseTrap';
|
|
13
|
+
export type { HotKey, HotKeyMetaData } from './utils/SkldrMouseTrap';
|
|
14
|
+
|
|
15
|
+
// snackbar service - user notifications
|
|
16
|
+
export { default as SnackbarService } from './components/SnackbarService.vue';
|
|
17
|
+
export { alertUser, type SnackbarOptions } from './components/SnackbarService';
|
|
18
|
+
|
|
19
|
+
export { default as PaginatingToolbar } from './components/PaginatingToolbar.vue';
|
|
20
|
+
export * from './components/PaginatingToolbar.types';
|
|
21
|
+
|
|
22
|
+
// Composables
|
|
23
|
+
export * from './composables/CompositionViewable';
|
|
24
|
+
export * from './composables/Displayable';
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
Study Session Components
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export { default as StudySession } from './components/StudySession.vue';
|
|
31
|
+
export { default as StudySessionTimer } from './components/StudySessionTimer.vue';
|
|
32
|
+
export type { StudySessionConfig } from './components/StudySession.types';
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
studentInputs
|
|
36
|
+
|
|
37
|
+
These components
|
|
38
|
+
- embed into cards
|
|
39
|
+
- accept inputs (answers) from students
|
|
40
|
+
- emit / pass answers to other components in the system
|
|
41
|
+
*/
|
|
42
|
+
export * from './components/studentInputs/BaseUserInput';
|
|
43
|
+
export { default as RadioMultipleChoice } from './components/studentInputs/RadioMultipleChoice.vue';
|
|
44
|
+
export { default as MultipleChoiceOption } from './components/studentInputs/MultipleChoiceOption.vue';
|
|
45
|
+
export { default as TFSelect } from './components/studentInputs/TrueFalse.vue';
|
|
46
|
+
export { default as UserInputNumber } from './components/studentInputs/UserInputNumber.vue';
|
|
47
|
+
export { default as UserInputString } from './components/studentInputs/UserInputString.vue';
|
|
48
|
+
export { default as FillInInput } from './components/studentInputs/fillInInput.vue';
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
cardRendering
|
|
52
|
+
|
|
53
|
+
These components are used to render course content
|
|
54
|
+
*/
|
|
55
|
+
export { default as CardViewer } from './components/cardRendering/CardViewer.vue';
|
|
56
|
+
export { default as CardLoader } from './components/cardRendering/CardLoader.vue';
|
|
57
|
+
|
|
58
|
+
export * from './components/cardRendering/MarkdownRendererHelpers';
|
|
59
|
+
export { default as AudioAutoPlayer } from './components/cardRendering/AudioAutoPlayer.vue';
|
|
60
|
+
export { default as CodeBlockRenderer } from './components/cardRendering/CodeBlockRenderer.vue';
|
|
61
|
+
export { default as MdTokenRenderer } from './components/cardRendering/MdTokenRenderer.vue';
|
|
62
|
+
export { default as MarkdownRenderer } from './components/cardRendering/MarkdownRenderer.vue';
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
Authentication Components
|
|
66
|
+
*/
|
|
67
|
+
export * from './components/auth';
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
stores
|
|
71
|
+
*/
|
|
72
|
+
export * from './stores/useCardPreviewModeStore';
|
|
73
|
+
export * from './stores/useAuthStore';
|
|
74
|
+
export * from './stores/useConfigStore';
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
plugins
|
|
78
|
+
*/
|
|
79
|
+
export { piniaPlugin } from './plugins/pinia';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// common-ui/src/plugins/pinia.ts
|
|
2
|
+
import { Plugin } from 'vue';
|
|
3
|
+
import { Pinia } from 'pinia';
|
|
4
|
+
|
|
5
|
+
// This will hold the Pinia instance provided by the main app
|
|
6
|
+
let _pinia: Pinia | null = null;
|
|
7
|
+
|
|
8
|
+
export const setPinia = (pinia: Pinia) => {
|
|
9
|
+
_pinia = pinia;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getPinia = (): Pinia | null => {
|
|
13
|
+
return _pinia;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Create a plugin that the main app can use
|
|
17
|
+
export const piniaPlugin: Plugin = {
|
|
18
|
+
install(app, options) {
|
|
19
|
+
const pinia = options?.pinia;
|
|
20
|
+
if (pinia) {
|
|
21
|
+
setPinia(pinia);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|