Skip to content

Commit 7084ff2

Browse files
committed
[wip] add example question impls
1 parent 8f8735f commit 7084ff2

File tree

10 files changed

+454
-0
lines changed

10 files changed

+454
-0
lines changed

packages/standalone-ui/src/main.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import { useAuthStore } from '@vue-skuilder/common-ui';
2323
import '@vue-skuilder/courses/style';
2424
import '@vue-skuilder/common-ui/style';
2525

26+
// Import allCourses singleton and exampleCourse
27+
import { allCourses } from '@vue-skuilder/courses';
28+
import { exampleCourse } from './questions/exampleCourse';
29+
30+
// Add the example course to the allCourses singleton
31+
allCourses.courses.push(exampleCourse);
32+
2633
// theme configuration
2734
import config from '../skuilder.config.json';
2835

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Question, ViewData, Answer } from '@vue-skuilder/courses';
2+
import { FieldType, DataShape } from '@vue-skuilder/common';
3+
import MultipleChoiceQuestionView from './MultipleChoiceQuestionView.vue';
4+
5+
export class MultipleChoiceQuestion extends Question {
6+
public static dataShapes: DataShape[] = [
7+
{
8+
name: 'MultipleChoiceQuestion',
9+
fields: [
10+
{ name: 'questionText', type: FieldType.STRING },
11+
{ name: 'options', type: FieldType.STRING }, // Comma-separated string of options
12+
{ name: 'correctAnswer', type: FieldType.STRING },
13+
],
14+
},
15+
];
16+
17+
public static views = [
18+
{ name: 'MultipleChoiceQuestionView', component: MultipleChoiceQuestionView },
19+
];
20+
21+
private questionText: string;
22+
private options: string[];
23+
private correctAnswer: string;
24+
25+
constructor(data: ViewData[]) {
26+
super(data);
27+
this.questionText = data[0].questionText as string;
28+
this.options = (data[0].options as string).split(',').map(s => s.trim());
29+
this.correctAnswer = data[0].correctAnswer as string;
30+
}
31+
32+
public dataShapes(): DataShape[] {
33+
return MultipleChoiceQuestion.dataShapes;
34+
}
35+
36+
public views() {
37+
// This will be dynamically populated or imported
38+
return MultipleChoiceQuestion.views;
39+
}
40+
41+
protected isCorrect(answer: Answer): boolean {
42+
return (answer.response as string) === this.correctAnswer;
43+
}
44+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<div>
3+
<p>{{ questionText }}</p>
4+
<div v-for="(option, index) in options" :key="index">
5+
<input
6+
type="radio"
7+
:id="`option-${index}`"
8+
:value="option"
9+
v-model="selectedAnswer"
10+
/>
11+
<label :for="`option-${index}`">{{ option }}</label>
12+
</div>
13+
<button @click="submitAnswer">Submit</button>
14+
</div>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import { ref, PropType } from 'vue';
19+
import { useViewable, useQuestionView } from '@vue-skuilder/common-ui';
20+
import { MultipleChoiceQuestion } from './MultipleChoiceQuestion';
21+
import { ViewData } from '@vue-skuilder/common';
22+
23+
const props = defineProps({
24+
questionText: {
25+
type: String,
26+
required: true,
27+
},
28+
options: {
29+
type: Array as () => string[],
30+
required: true,
31+
},
32+
data: {
33+
type: Array as PropType<ViewData[]>,
34+
required: true,
35+
},
36+
});
37+
38+
const selectedAnswer = ref('');
39+
40+
const viewableUtils = useViewable(props, () => {}, 'MultipleChoiceQuestionView');
41+
const questionUtils = useQuestionView<MultipleChoiceQuestion>(viewableUtils);
42+
43+
// Initialize question
44+
questionUtils.question.value = new MultipleChoiceQuestion(props.data);
45+
46+
const submitAnswer = () => {
47+
if (selectedAnswer.value) {
48+
questionUtils.submitAnswer({ response: selectedAnswer.value });
49+
}
50+
};
51+
</script>
52+
53+
<style scoped>
54+
/* Add some basic styling if needed */
55+
</style>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Question, ViewData, Answer } from '@vue-skuilder/courses';
2+
import { FieldType, DataShape } from '@vue-skuilder/common';
3+
import NumberRangeQuestionView from './NumberRangeQuestionView.vue';
4+
5+
export class NumberRangeQuestion extends Question {
6+
public static dataShapes: DataShape[] = [
7+
{
8+
name: 'NumberRangeQuestion',
9+
fields: [
10+
{ name: 'questionText', type: FieldType.STRING },
11+
{ name: 'min', type: FieldType.NUMBER },
12+
{ name: 'max', type: FieldType.NUMBER },
13+
],
14+
},
15+
];
16+
17+
public static views = [
18+
{ name: 'NumberRangeQuestionView', component: NumberRangeQuestionView },
19+
];
20+
21+
private questionText: string;
22+
private min: number;
23+
private max: number;
24+
25+
constructor(data: ViewData[]) {
26+
super(data);
27+
this.questionText = data[0].questionText as string;
28+
this.min = data[0].min as number;
29+
this.max = data[0].max as number;
30+
}
31+
32+
public dataShapes(): DataShape[] {
33+
return NumberRangeQuestion.dataShapes;
34+
}
35+
36+
public views() {
37+
// This will be dynamically populated or imported
38+
return NumberRangeQuestion.views;
39+
}
40+
41+
protected isCorrect(answer: Answer): boolean {
42+
const userAnswer = answer.response as number;
43+
return userAnswer >= this.min && userAnswer <= this.max;
44+
}
45+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
<div>
3+
<p>{{ questionText }}</p>
4+
<input type="number" v-model.number="userAnswer" @keyup.enter="submitAnswer" placeholder="Enter a number" />
5+
<button @click="submitAnswer">Submit</button>
6+
</div>
7+
</template>
8+
9+
<script setup lang="ts">
10+
import { ref, PropType } from 'vue';
11+
import { useViewable, useQuestionView } from '@vue-skuilder/common-ui';
12+
import { NumberRangeQuestion } from './NumberRangeQuestion';
13+
import { ViewData } from '@vue-skuilder/common';
14+
15+
const props = defineProps({
16+
questionText: {
17+
type: String,
18+
required: true,
19+
},
20+
data: {
21+
type: Array as PropType<ViewData[]>,
22+
required: true,
23+
},
24+
});
25+
26+
const userAnswer = ref<number | null>(null);
27+
28+
const viewableUtils = useViewable(props, () => {}, 'NumberRangeQuestionView');
29+
const questionUtils = useQuestionView<NumberRangeQuestion>(viewableUtils);
30+
31+
// Initialize question
32+
questionUtils.question.value = new NumberRangeQuestion(props.data);
33+
34+
const submitAnswer = () => {
35+
if (userAnswer.value !== null) {
36+
questionUtils.submitAnswer({ response: userAnswer.value });
37+
}
38+
};
39+
</script>
40+
41+
<style scoped>
42+
/* Add some basic styling if needed */
43+
</style>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Custom Questions in Standalone UI
2+
3+
This directory contains example implementations of custom question types for the Vue Skuilder platform. These examples demonstrate how to create new `Question` subclasses and their corresponding Vue components, and how to integrate them into your application.
4+
5+
## Example Questions Provided
6+
7+
- **SimpleTextQuestion**: A basic question that asks for a text input and checks for an exact string match.
8+
- **MultipleChoiceQuestion**: Presents a question with multiple options and checks for the correct selection.
9+
- **NumberRangeQuestion**: Asks for a numeric input and validates if it falls within a specified range.
10+
11+
## How to Use These Examples
12+
13+
Each question type consists of two main parts:
14+
1. A TypeScript file (`.ts`) defining the `Question` subclass, which handles the question logic, data shapes, and answer evaluation.
15+
2. A Vue component file (`.vue`) that provides the user interface for the question.
16+
17+
These examples are already integrated into the `exampleCourse.ts` file, which you can use to see them in action.
18+
19+
## Integrating Custom Questions into Your Course at Runtime
20+
21+
To use your custom questions in a course, you need to:
22+
23+
1. **Define your Question Class**: Create a new TypeScript file (e.g., `MyCustomQuestion.ts`) that extends the `Question` class from `@vue-skuilder/courses`. Define its `dataShapes` and `views` static properties.
24+
25+
```typescript
26+
// MyCustomQuestion.ts
27+
import { Question, DataShape, ViewData, Answer } from '@vue-skuilder/courses';
28+
import { FieldType } from '@vue-skuilder/common';
29+
import MyCustomQuestionView from './MyCustomQuestionView.vue';
30+
31+
export class MyCustomQuestion extends Question {
32+
public static dataShapes: DataShape[] = [
33+
new DataShape('MyCustomQuestion', [
34+
{ name: 'myField', type: FieldType.STRING },
35+
]),
36+
];
37+
38+
public static views = [
39+
{ name: 'MyCustomQuestionView', component: MyCustomQuestionView },
40+
];
41+
42+
constructor(data: ViewData[]) {
43+
super(data);
44+
// Initialize your question data from `data`
45+
}
46+
47+
public dataShapes(): DataShape[] {
48+
return MyCustomQuestion.dataShapes;
49+
}
50+
51+
public views() {
52+
return MyCustomQuestion.views;
53+
}
54+
55+
protected isCorrect(answer: Answer): boolean {
56+
// Implement your answer evaluation logic here
57+
return false;
58+
}
59+
}
60+
```
61+
62+
2. **Create Your Vue Component**: Create a Vue component (e.g., `MyCustomQuestionView.vue`) that will render your question and allow user interaction. This component will receive props based on the `ViewData` you define for your question.
63+
64+
```vue
65+
<!-- MyCustomQuestionView.vue -->
66+
<template>
67+
<div>
68+
<p>{{ questionData.myField }}</p>
69+
<!-- Your input elements and UI -->
70+
<button @click="submitAnswer">Submit</button>
71+
</div>
72+
</template>
73+
74+
<script setup lang="ts">
75+
import { ref } from 'vue';
76+
import { useStudySessionStore } from '@vue-skuilder/common-ui';
77+
78+
const props = defineProps({
79+
// Define props based on your question's data
80+
questionData: { type: Object, required: true },
81+
});
82+
83+
const studySessionStore = useStudySessionStore();
84+
const userAnswer = ref('');
85+
86+
const submitAnswer = () => {
87+
// Collect user's answer and submit it
88+
studySessionStore.submitAnswer({ response: userAnswer.value });
89+
};
90+
</script>
91+
```
92+
93+
3. **Register Your Question and Course**: In your application's entry point (e.g., `src/main.ts` or `src/App.vue`), you need to import your custom question and include it in a `Course` instance. Then, register this course with the `allCourses` list.
94+
95+
```typescript
96+
// src/main.ts (example)
97+
import { createApp } from 'vue';
98+
import App from './App.vue';
99+
import { createPinia } from 'pinia';
100+
import { allCourses, Course } from '@vue-skuilder/courses';
101+
102+
// Import your custom question
103+
import { MyCustomQuestion } from './questions/MyCustomQuestion';
104+
105+
// Create a new Course instance with your custom question
106+
const myCustomCourse = new Course('MyCustomCourse', [
107+
new MyCustomQuestion([{ myField: 'Hello Custom Question!' }]),
108+
]);
109+
110+
// Add your custom course to the global allCourses list
111+
allCourses.courses.push(myCustomCourse);
112+
113+
const app = createApp(App);
114+
app.use(createPinia());
115+
app.mount('#app');
116+
```
117+
118+
**Note**: The `allCourses` object is a singleton that manages all available courses and their associated questions and views. By adding your custom course to `allCourses.courses`, it becomes discoverable by the `CardViewer` and other components that rely on the course registry.
119+
120+
## Developing New Questions
121+
122+
When developing new questions, consider the following:
123+
124+
- **DataShape Definition**: Carefully define the `DataShape` for your question. This dictates the structure of the data that will be passed to your question's constructor and Vue component.
125+
- **Answer Evaluation**: Implement the `isCorrect` method in your `Question` subclass to define how user answers are evaluated.
126+
- **Vue Component Props**: Ensure your Vue component's `props` match the data fields defined in your `DataShape` and any additional data you pass from your `Question` instance.
127+
- **StudySessionStore**: Use the `useStudySessionStore()` composable from `@vue-skuilder/common-ui` to submit user answers and interact with the study session logic.
128+
129+
Feel free to modify and extend the provided examples to suit your needs.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { SimpleTextQuestion } from './SimpleTextQuestion';
3+
4+
describe('SimpleTextQuestion', () => {
5+
it('should correctly evaluate a correct answer', () => {
6+
const question = new SimpleTextQuestion([
7+
{ questionText: 'What is the capital of France?', correctAnswer: 'Paris' },
8+
]);
9+
expect(question.evaluate({ response: 'Paris' }, 0).isCorrect).toBe(true);
10+
});
11+
12+
it('should correctly evaluate an incorrect answer', () => {
13+
const question = new SimpleTextQuestion([
14+
{ questionText: 'What is the capital of France?', correctAnswer: 'Paris' },
15+
]);
16+
expect(question.evaluate({ response: 'London' }, 0).isCorrect).toBe(false);
17+
});
18+
19+
it('should be case-insensitive', () => {
20+
const question = new SimpleTextQuestion([
21+
{ questionText: 'What is the capital of France?', correctAnswer: 'Paris' },
22+
]);
23+
expect(question.evaluate({ response: 'paris' }, 0).isCorrect).toBe(true);
24+
});
25+
});

0 commit comments

Comments
 (0)