@windward/core 0.15.0 → 0.17.0

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.
@@ -0,0 +1,345 @@
1
+ <template>
2
+ <div>
3
+ <v-row class="container-generate-ai mt-2">
4
+ <v-col>{{
5
+ $t(
6
+ 'windward.core.components.content.blocks.generate_questions.ai_assistance'
7
+ )
8
+ }}</v-col>
9
+ <v-col>
10
+ <v-select
11
+ v-model="selectedContent"
12
+ :items="flattenedContent"
13
+ class="btn-selector"
14
+ outlined
15
+ hide-details
16
+ dense
17
+ :label="
18
+ $t(
19
+ 'windward.core.components.content.blocks.generate_questions.selected_pages'
20
+ )
21
+ "
22
+ item-text="content.name"
23
+ return-object
24
+ ></v-select>
25
+ </v-col>
26
+ <v-col>
27
+ <v-select
28
+ v-model="selectedDifficulty"
29
+ :items="taxonomyLevels"
30
+ item-text="text"
31
+ class="btn-selector"
32
+ outlined
33
+ hide-details
34
+ dense
35
+ :label="
36
+ $t(
37
+ 'windward.core.components.content.blocks.generate_questions.blooms.blooms_taxonomy'
38
+ )
39
+ "
40
+ return-object
41
+ ></v-select>
42
+ </v-col>
43
+ <v-col
44
+ cols="auto"
45
+ class="d-flex align-center"
46
+ v-if="isFlashcardType"
47
+ >
48
+ <v-switch
49
+ v-model="replaceExisting"
50
+ hide-details
51
+ dense
52
+ color="secondary"
53
+ class="mt-0 pt-0"
54
+ :label="
55
+ $t(
56
+ 'windward.games.components.settings.flashcard.form.replace_existing'
57
+ )
58
+ "
59
+ :disabled="isLoading"
60
+ ></v-switch>
61
+ </v-col>
62
+ <v-col>
63
+ <v-btn
64
+ elevation="0"
65
+ color="secondary"
66
+ class="mb-1 btn-selector"
67
+ :loading="isLoading"
68
+ :disabled="isLoading"
69
+ @click="generateAIQuestion"
70
+ >
71
+ <v-icon v-if="!isLoading" class="pr-1"
72
+ >mdi-magic-staff</v-icon
73
+ >
74
+ {{
75
+ $t(
76
+ 'windward.core.components.content.blocks.generate_questions.button_label'
77
+ )
78
+ }}
79
+ <template v-slot:loader>
80
+ <v-progress-circular
81
+ indeterminate
82
+ size="23"
83
+ ></v-progress-circular>
84
+ </template>
85
+ </v-btn>
86
+ </v-col>
87
+ </v-row>
88
+ </div>
89
+ </template>
90
+
91
+ <script>
92
+ import _ from 'lodash'
93
+ import { mapGetters } from 'vuex'
94
+ import AssessmentQuestion from '~/models/AssessmentQuestion'
95
+ import Course from '~/models/Course'
96
+ import Assessment from '~/models/Assessment'
97
+ import Content from '~/models/Content'
98
+ import Activity from '../../models/Activity'
99
+
100
+ export default {
101
+ name: 'GenerateAIQuestionButton',
102
+ props: {
103
+ course: { type: Object, required: true },
104
+ content: { type: Object, required: true },
105
+ block: { type: Object, required: true },
106
+ questionType: { type: String, required: true },
107
+ replaceExistingMode: { type: Boolean, default: false },
108
+ },
109
+ data() {
110
+ return {
111
+ isLoading: false,
112
+ selectedContent: '',
113
+ selectedDifficulty: {
114
+ value: 'None',
115
+ text: this.$t(
116
+ 'windward.core.components.content.blocks.generate_questions.blooms.none'
117
+ ),
118
+ },
119
+ replaceExisting: this.replaceExistingMode,
120
+ }
121
+ },
122
+ computed: {
123
+ ...mapGetters({
124
+ contentTree: 'content/getTree',
125
+ }),
126
+ isFlashcardType() {
127
+ return this.questionType === 'flashcard'
128
+ },
129
+ flattenedContent() {
130
+ let cloneContentTree = _.cloneDeep(this.contentTree)
131
+ const homepage = this.$ContentService.getHomepage()
132
+ if (!_.isEmpty(homepage)) {
133
+ cloneContentTree.unshift(homepage)
134
+ }
135
+ let fullTree = []
136
+ // flatten content tree to get nested children pages
137
+ cloneContentTree.forEach((content) => {
138
+ fullTree.push(content)
139
+ if (content.children.length > 0) {
140
+ fullTree = fullTree.concat(_.flatten(content.children))
141
+ // check if children have children
142
+ content.children.forEach((child) => {
143
+ fullTree = fullTree.concat(_.flatten(child.children))
144
+ })
145
+ }
146
+ })
147
+ //
148
+ if (_.isEmpty(this.selectedContent)) {
149
+ // returns array so hold here to pluck out below
150
+ const currentPage = fullTree.filter(
151
+ (contentPage) => contentPage.id === this.content.id
152
+ )
153
+ this.selectedContent = currentPage[0] ? currentPage[0] : ''
154
+ }
155
+ return fullTree
156
+ },
157
+ taxonomyLevels() {
158
+ // Basic Bloom's taxonomy levels available to all question types
159
+ let basicBloomTaxonomy = [
160
+ {
161
+ value: 'None',
162
+ text: this.$t(
163
+ 'windward.core.components.content.blocks.generate_questions.blooms.none'
164
+ ),
165
+ },
166
+ {
167
+ value: 'Remember',
168
+ text: this.$t(
169
+ 'windward.core.components.content.blocks.generate_questions.blooms.remember'
170
+ ),
171
+ },
172
+ {
173
+ value: 'Understand',
174
+ text: this.$t(
175
+ 'windward.core.components.content.blocks.generate_questions.blooms.understand'
176
+ ),
177
+ },
178
+ {
179
+ value: 'Apply',
180
+ text: this.$t(
181
+ 'windward.core.components.content.blocks.generate_questions.blooms.apply'
182
+ ),
183
+ },
184
+ ]
185
+
186
+ // Only add higher-level Bloom's taxonomy for supported question types
187
+ // Flashcards use only basic levels
188
+ if (
189
+ !this.isFlashcardType &&
190
+ (this.questionType === 'multi_choice_single_answer' ||
191
+ this.questionType === 'ordering' ||
192
+ this.questionType === 'multi_choice_multi_answer')
193
+ ) {
194
+ const multiBlooms = [
195
+ {
196
+ value: 'Analyze',
197
+ text: this.$t(
198
+ 'windward.core.components.content.blocks.generate_questions.blooms.analyze'
199
+ ),
200
+ },
201
+ {
202
+ value: 'Evaluate',
203
+ text: this.$t(
204
+ 'windward.core.components.content.blocks.generate_questions.blooms.evaluate'
205
+ ),
206
+ },
207
+ ]
208
+ basicBloomTaxonomy = basicBloomTaxonomy.concat(multiBlooms)
209
+ }
210
+ return basicBloomTaxonomy
211
+ },
212
+ },
213
+ methods: {
214
+ async generateAIQuestion() {
215
+ this.isLoading = true
216
+ try {
217
+ let bloomsRequest = ''
218
+ if (
219
+ this.selectedDifficulty.text !==
220
+ this.$t(
221
+ 'windward.core.components.content.blocks.generate_questions.blooms.none'
222
+ )
223
+ ) {
224
+ bloomsRequest = `?blooms_level=${this.selectedDifficulty.value}`
225
+ }
226
+
227
+ const course = new Course(this.course)
228
+ const content = new Content(
229
+ this.selectedContent || this.content
230
+ )
231
+
232
+ let response
233
+ if (this.questionType === 'flashcard') {
234
+ // FLASHCARD GENERATION
235
+ const activity = new Activity()
236
+
237
+ const endpoint = `suggest/flashcard${bloomsRequest}`
238
+
239
+ // Call the endpoint exactly like FlashCardSlidesManager does
240
+ response = await Activity.custom(
241
+ course,
242
+ content,
243
+ activity,
244
+ endpoint
245
+ ).get()
246
+
247
+ let activityData = null
248
+
249
+ if (response && response.activity) {
250
+ activityData = response.activity
251
+ } else if (
252
+ response &&
253
+ response.length > 0 &&
254
+ response[0] &&
255
+ response[0].activity
256
+ ) {
257
+ activityData = response[0].activity
258
+ } else if (Array.isArray(response) && response.length > 0) {
259
+ activityData = response[0]
260
+ }
261
+
262
+ if (
263
+ activityData &&
264
+ activityData.metadata &&
265
+ activityData.metadata.config &&
266
+ activityData.metadata.config.cards &&
267
+ Array.isArray(activityData.metadata.config.cards)
268
+ ) {
269
+ // We pass the activity data and the replace flag to the parent component
270
+ this.$emit(
271
+ 'click:generate',
272
+ activityData,
273
+ this.replaceExisting
274
+ )
275
+ } else {
276
+ throw new Error(
277
+ 'Invalid response from flashcard generation'
278
+ )
279
+ }
280
+ } else {
281
+ // ASSESSMENT QUESTION GENERATION
282
+ const assessment = new Assessment({ id: this.block.id })
283
+ const question = new AssessmentQuestion()
284
+
285
+ response = await AssessmentQuestion.custom(
286
+ course,
287
+ content,
288
+ assessment,
289
+ question,
290
+ `suggest/${this.questionType}${bloomsRequest}`
291
+ ).get()
292
+
293
+ if (response && response.length > 0) {
294
+ const generatedQuestion = response[0]
295
+ this.$emit('click:generate', generatedQuestion)
296
+ } else {
297
+ throw new Error(
298
+ 'Invalid response from question generation'
299
+ )
300
+ }
301
+ }
302
+ } catch (error) {
303
+ const errorMessage =
304
+ error.response?.data?.error?.message ||
305
+ error.message ||
306
+ 'assessment.error.technical'
307
+ const errorType = errorMessage.split('.').pop()
308
+ const basePath =
309
+ 'windward.core.components.content.blocks.generate_questions.error'
310
+
311
+ let errorText =
312
+ this.$t(`${basePath}.${errorType}`) +
313
+ '\n\n' +
314
+ this.$t(`${basePath}.${errorType}_support`)
315
+
316
+ if (errorType === 'technical') {
317
+ const errorCode =
318
+ error.response?.data?.error?.details?.error_type ||
319
+ 'UNKNOWN'
320
+ errorText = errorText.replace('[ERROR_CODE]', errorCode)
321
+ }
322
+
323
+ this.$dialog.error(errorText, {
324
+ duration: 5000,
325
+ keepOnHover: true,
326
+ singleton: true,
327
+ type: 'error',
328
+ })
329
+ } finally {
330
+ this.isLoading = false
331
+ }
332
+ },
333
+ },
334
+ }
335
+ </script>
336
+
337
+ <style scoped>
338
+ .btn-selector {
339
+ width: 100%;
340
+ }
341
+ .container-generate-ai {
342
+ outline: 1px solid var(--v-secondary-base);
343
+ border-radius: 15px;
344
+ }
345
+ </style>
@@ -138,7 +138,7 @@ export default {
138
138
  type: String,
139
139
  required: false,
140
140
  default:
141
- 'undo redo | styles | bold italic underline strikethrough removeformat | alignleft aligncenter alignright | table tablerowprops tablecellprops |bullist numlist outdent indent |glossaryButton fibFormatButton mathButton a11yButton',
141
+ 'styles | bold italic underline strikethrough removeformat | alignleft aligncenter alignright | table tablerowprops tablecellprops |bullist numlist outdent indent |glossaryButton fibFormatButton mathButton a11yButton | undo redo',
142
142
  },
143
143
  rootBlock: { type: String, required: false, default: 'div' },
144
144
  label: { type: String, required: false, default: '' },
@@ -212,7 +212,7 @@ export default {
212
212
  },
213
213
  format: {
214
214
  title: 'Format',
215
- items: ' bold italic underline strikethrough superscript subscript codeformat | formats align | language | removeformat',
215
+ items: ' bold italic underline strikethrough superscript subscript codeformat | formats align | language | removeformat | glossary',
216
216
  },
217
217
  },
218
218
  autoresize_min_height: 100,
@@ -337,6 +337,9 @@ export default {
337
337
  },
338
338
  formats: {
339
339
  glossary: {
340
+ title: this.$t(
341
+ 'windward.core.components.utils.tiny_mce_wrapper.glossary'
342
+ ),
340
343
  inline: 'span',
341
344
  attributes: {
342
345
  'aria-label': this.$t(
@@ -378,67 +381,22 @@ export default {
378
381
  ],
379
382
  },
380
383
  style_formats: [
381
- {
382
- title: 'Headings',
383
- items: [
384
- { title: 'Heading 2', format: 'h2' },
385
- { title: 'Heading 3', format: 'h3' },
386
- { title: 'Heading 4', format: 'h4' },
387
- { title: 'Heading 5', format: 'h5' },
388
- { title: 'Heading 6', format: 'h6' },
389
- ],
390
- },
391
- {
392
- title: 'Inline',
393
- items: [
394
- { title: 'Bold', format: 'bold' },
395
- { title: 'Italic', format: 'italic' },
396
- { title: 'Underline', format: 'underline' },
397
- { title: 'Strikethrough', format: 'strikethrough' },
398
- { title: 'Superscript', format: 'superscript' },
399
- { title: 'Subscript', format: 'subscript' },
400
- { title: 'Code', format: 'code' },
401
- ],
402
- },
403
- {
404
- title: 'Blocks',
405
- items: [{ title: 'Paragraph', format: 'p' }],
406
- },
407
- {
408
- title: 'Align',
409
- items: [
410
- { title: 'Left', format: 'alignleft' },
411
- { title: 'Center', format: 'aligncenter' },
412
- { title: 'Right', format: 'alignright' },
413
- { title: 'Justify', format: 'alignjustify' },
414
- ],
415
- },
416
- {
417
- title: 'LearningEdge',
418
- items: [
419
- {
420
- title: this.$t(
421
- 'windward.core.components.utils.tiny_mce_wrapper.term'
422
- ),
423
- format: 'glossary',
424
- },
425
- {
426
- title: this.$t(
427
- 'windward.core.components.utils.tiny_mce_wrapper.fill_blank'
428
- ),
429
- format: 'fib',
430
- },
431
- ],
432
- },
384
+ { title: 'Normal Text', format: 'div' },
385
+ { title: 'Paragraph', format: 'p' },
386
+ { title: 'Heading 2', format: 'h2' },
387
+ { title: 'Heading 3', format: 'h3' },
388
+ { title: 'Heading 4', format: 'h4' },
389
+ { title: 'Heading 5', format: 'h5' },
390
+ { title: 'Heading 6', format: 'h6' },
433
391
  ],
434
392
  placeholder: this.label
435
393
  ? this.label
436
394
  : this.$t('components.content.settings.base.placeholder'),
437
- //required as it will be displayed as inline style in tinymce renderer
395
+ // required as it will be displayed as inline style in tinymce renderer
438
396
  skin: false,
439
397
  content_css: this.$vuetify.theme.isDark ? 'dark' : 'default',
440
398
 
441
- //we need to inject the glossary style directly
399
+ // we need to inject the glossary style directly
442
400
  content_style:
443
401
  ContentCss +
444
402
  EditorCss +
@@ -477,7 +435,6 @@ export default {
477
435
  this.text = this.value
478
436
  }
479
437
  },
480
-
481
438
  methods: {
482
439
  getEditor() {
483
440
  if (this.$refs.editor && this.$refs.editor.editor) {
@@ -1,11 +1,14 @@
1
1
  <template>
2
- <v-container justify="center">
2
+ <v-container justify="center" class="pa-0">
3
3
  <v-row justify="center" align="center">
4
4
  <v-col cols="12">
5
5
  <v-data-table
6
6
  :key="tableKey"
7
7
  :headers="headers"
8
8
  :items="glossaryTerms"
9
+ :footer-props="{
10
+ 'items-per-page-options': [5, -1],
11
+ }"
9
12
  :search="search"
10
13
  class="elevation-1"
11
14
  >
@@ -35,9 +38,13 @@
35
38
  >
36
39
  <template #title>
37
40
  {{
38
- $t(
39
- 'windward.core.pages.glossary.add_term'
40
- )
41
+ selectedTerm.id
42
+ ? $t(
43
+ 'windward.core.pages.glossary.edit_term'
44
+ )
45
+ : $t(
46
+ 'windward.core.pages.glossary.add_term'
47
+ )
41
48
  }}
42
49
  </template>
43
50
  <template #trigger>
@@ -159,18 +166,16 @@
159
166
  <script>
160
167
  import { mapGetters, mapMutations, mapActions } from 'vuex'
161
168
  import _ from 'lodash'
162
- import { encode } from 'he'
169
+ import CourseGlossaryTerm from '../../../models/CourseGlossaryTerm'
163
170
  import CourseGlossaryForm from './CourseGlossaryForm'
164
171
  import DialogBox from '~/components/Core/DialogBox.vue'
165
172
  import Crypto from '~/helpers/Crypto'
166
173
  import Course from '~/models/Course'
167
- import CourseGlossaryTerm from '../../../models/CourseGlossaryTerm'
168
174
  import SpeedDial from '~/components/Core/SpeedDial.vue'
169
- import TextViewer from '~/components/Text/TextViewer.vue'
170
175
 
171
176
  export default {
172
177
  name: 'CourseGlossary',
173
- components: { DialogBox, CourseGlossaryForm, SpeedDial, TextViewer },
178
+ components: { DialogBox, CourseGlossaryForm, SpeedDial },
174
179
  layout: 'course',
175
180
  middleware: ['auth'],
176
181
  props: {
@@ -142,7 +142,17 @@ export class WindwardPlugins {
142
142
  'glossaryIcon',
143
143
  this.$t('windward.core.components.utils.tiny_mce_wrapper.glossary'),
144
144
  () => {
145
- this.editor.formatter.apply('glossary')
145
+ if (
146
+ this.editor.selection
147
+ .getSel()
148
+ .anchorNode.parentElement.classList.contains(
149
+ 'glossary-word'
150
+ )
151
+ ) {
152
+ this.editor.formatter.remove('glossary')
153
+ } else {
154
+ this.editor.formatter.apply('glossary')
155
+ }
146
156
  }
147
157
  )
148
158
  this.addButtonToEditor(
@@ -210,13 +220,29 @@ export class WindwardPlugins {
210
220
  itemKey: string,
211
221
  itemText: string,
212
222
  command: string,
213
- icon: string
223
+ icon: string,
224
+ type: string = 'command'
214
225
  ): void {
215
226
  this.editor.ui.registry.addMenuItem(itemKey, {
216
227
  text: itemText,
217
228
  icon,
218
229
  onAction: () => {
219
- this.editor.execCommand(command, true)
230
+ switch (type) {
231
+ case 'format':
232
+ if (
233
+ this.editor.selection
234
+ .getNode()
235
+ .classList.contains('glossary-word')
236
+ ) {
237
+ this.editor.formatter.remove(command)
238
+ } else {
239
+ this.editor.formatter.apply(command)
240
+ }
241
+ break
242
+ default:
243
+ this.editor.execCommand(command, true)
244
+ break
245
+ }
220
246
  },
221
247
  })
222
248
  }
@@ -233,6 +259,13 @@ export class WindwardPlugins {
233
259
  'fib-window',
234
260
  'fibIcon'
235
261
  )
262
+ this.addEditorMenuItem(
263
+ 'glossary',
264
+ 'Glossary',
265
+ 'glossary',
266
+ 'glossaryIcon',
267
+ 'format'
268
+ )
236
269
  }
237
270
 
238
271
  /**
@@ -1,20 +1,35 @@
1
1
  export default {
2
2
  error: {
3
3
  default: 'Could not generate question from provided content.',
4
- default_support: 'Please try again or contact support if the issue persists.',
5
-
4
+ default_support:
5
+ 'Please try again or contact support if the issue persists.',
6
+
6
7
  insufficient_content: 'More content needed to generate questions.',
7
- insufficient_content_support: 'Please add more text, examples, or explanations to this section. We recommend at least 2-3 paragraphs of content to generate relevant questions.',
8
-
9
- content_mismatch: 'Content doesn\'t match question type.',
10
- content_mismatch_support: 'The current content isn\'t suitable for this type of question. Consider adding more specific examples, numerical data, or comparable items depending on your desired question type.',
11
-
8
+ insufficient_content_support:
9
+ 'Please add more text, examples, or explanations to this section. We recommend at least 50 words of relevant content to generate appropriate questions.',
10
+
11
+ content_mismatch: "Content doesn't match question type.",
12
+ content_mismatch_support:
13
+ "The current content isn't suitable for this type of question. Consider adding more specific examples, numerical data, or comparable items depending on your desired question type.",
14
+
12
15
  llm_unavailable: 'Question generation temporarily unavailable.',
13
- llm_unavailable_support: 'We\'re unable to connect to our AI service at the moment. Please try again in a few minutes or contact support if the issue persists.',
14
-
16
+ llm_unavailable_support:
17
+ "We're unable to connect to our AI service at the moment. Please try again in a few minutes or contact support if the issue persists.",
18
+
15
19
  technical: 'Unable to process request.',
16
- technical_support: 'Something went wrong on our end. Please try again or contact support if this continues. Reference code: [ERROR_CODE]'
20
+ technical_support:
21
+ 'Something went wrong on our end. Please try again or contact support if this continues. Reference code: [ERROR_CODE]',
17
22
  },
18
23
  button_label: 'Generate Question',
19
- selected_pages: 'Selected Page'
24
+ selected_pages: 'Selected Page',
25
+ ai_assistance: 'AI Assistance',
26
+ blooms: {
27
+ blooms_taxonomy: "Bloom's Taxonomy Level",
28
+ none: 'None selected',
29
+ remember: 'Remember',
30
+ understand: 'Understand',
31
+ apply: 'Apply',
32
+ analyze: 'Analyze',
33
+ evaluate: 'Evaluate',
34
+ },
20
35
  }
@@ -8,7 +8,7 @@ export default {
8
8
  math: 'Math',
9
9
  accordion: 'Accordion',
10
10
  open_response: 'Open Response',
11
- open_response_collate: 'Open Response Collate',
11
+ open_response_collate: 'Open Response Download',
12
12
  image: 'Image',
13
13
  user_upload: 'User Upload',
14
14
  clickable_icons: 'Clickable Icons',
@@ -2,7 +2,7 @@ export default {
2
2
  title: {
3
3
  assessment: 'Assessment Settings',
4
4
  open_response: 'Open Response Settings',
5
- open_response_collate: 'Open Response Collate Settings',
5
+ open_response_collate: 'Open Response Download Settings',
6
6
  image: 'Image Settings',
7
7
  user_upload: 'User Upload Settings',
8
8
  file_download: 'File Download Settings',
@@ -12,7 +12,7 @@ export default {
12
12
 
13
13
  content_mismatch: 'El contenido no coincide con el tipo de pregunta.',
14
14
  content_mismatch_support:
15
- 'El contenido actual no es adecuado para este tipo de pregunta. Considere agregar ejemplos más específicos o elementos comparables.',
15
+ 'Por favor, añada más texto, ejemplos o explicaciones a esta sección. Recomendamos al menos 50 palabras de contenido relevante para generar preguntas apropiadas.',
16
16
 
17
17
  llm_unavailable:
18
18
  'La generación de preguntas no está disponible temporalmente.',
@@ -25,4 +25,14 @@ export default {
25
25
  },
26
26
  button_label: 'Generar pregunta',
27
27
  selected_pages: 'Página seleccionada',
28
+ ai_assistance: 'Asistencia de IA',
29
+ blooms: {
30
+ blooms_taxonomy: 'Nivel de taxonomía de Bloom',
31
+ none: 'Ninguno seleccionado',
32
+ remember: 'Recordar',
33
+ understand: 'Entender',
34
+ apply: 'Aplicar',
35
+ analyze: 'Analizar',
36
+ evaluate: 'Evaluar',
37
+ },
28
38
  }
@@ -8,7 +8,7 @@ export default {
8
8
  math: 'Matemáticas',
9
9
  accordion: 'Acordeón',
10
10
  open_response: 'Respuesta abierta',
11
- open_response_collate: 'Intercalación de respuesta abierta',
11
+ open_response_collate: 'Módulo de descarga de respuestas',
12
12
  image: 'Imagen',
13
13
  user_upload: 'Carga de usuario',
14
14
  clickable_icons: 'Iconos en los que se puede hacer clic',