@windward/core 0.22.0 → 0.23.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.
package/CHANGELOG.md CHANGED
@@ -1,20 +1,38 @@
1
1
  # Changelog
2
2
 
3
- ## Release [0.22.0] - 2025-08-25
3
+ ## Release [0.23.0] - 2025-09-18
4
4
 
5
+ * Merged in feature/LE-2099-track-engagement-on-videos-from- (pull request #439)
6
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #438)
7
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #437)
8
+ * Merged in bugfix/LE-2088-open-response-block-remove-edito (pull request #434)
9
+ * Merged in bugfix/MIND-6075-decouple-generateaiquestionbut (pull request #432)
10
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #433)
11
+ * Merged in bugfix/LE-2052-clickable-icon-text-color-should (pull request #426)
12
+ * Merged in bugfix/LE-2088-open-response-block-remove-edito (pull request #430)
13
+ * Merged release/0.23.0 into bugfix/LE-2071-vimeo-videos-not-working
14
+ * Merged release/0.22.0 into bugfix/LE-2052-clickable-icon-text-color-should
15
+
16
+
17
+ ## Release [0.22.0] - 2025-08-28
18
+
19
+ * Merge branch 'master' into release/0.22.0
5
20
  * Merged in bugfix/LE-2075-the-panel-of-the-text-block-isnt (pull request #427)
6
21
  * Merged in bug-fix/LE-2060/llm-character-limit-validation (pull request #421)
7
22
  * Merged in bugfix/LE-2031-open-response-download-collate-q (pull request #422)
8
23
  * Merged in bugfix/LE-1960-video-using-urls-as-a-source (pull request #423)
24
+ * Merged in hotfix/0.21.1 (pull request #425)
9
25
  * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
10
26
  * Merged in bugfix/LE-2057-save-button-disappeared-again-on (pull request #419)
11
27
  * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
12
28
  * Merged in feature/LE-2036/word-jumble-gen (pull request #420)
13
29
  * Merged in feature/LE-1997/scenario-gen (pull request #416)
14
30
  * Merged in bugfix/LE-1928-user-upload-allowed-file-types (pull request #415)
31
+ * Merged in release/0.21.0 (pull request #401)
15
32
  * Merged release/0.21.0 into bugfix/LE-1960-video-using-urls-as-a-source
16
33
  * Merged release/0.21.0 into bugfix/LE-1928-user-upload-allowed-file-types
17
34
 
35
+
18
36
  ## Hotfix [0.21.1] created - 2025-08-20
19
37
 
20
38
 
@@ -54,10 +54,10 @@
54
54
  </v-avatar>
55
55
  <v-icon
56
56
  v-else-if="isIcon(item.icon)"
57
- class="clickable--icon black--text"
58
- >{{ item.icon }}</v-icon
59
- >
60
- <span v-else :class="iconClass + ' black--text'">{{
57
+ class="clickable--icon dark-text--text"
58
+ >{{ item.icon }}
59
+ </v-icon>
60
+ <span v-else :class="iconClass + ' dark-text--text'">{{
61
61
  decode(item.icon)
62
62
  }}</span>
63
63
  </button>
@@ -142,7 +142,7 @@ export default {
142
142
  ) &&
143
143
  this.itemColor(itemIndex)
144
144
  ) {
145
- classes += ' ' + this.itemColor(itemIndex) + ' black--text'
145
+ classes += ' ' + this.itemColor(itemIndex) + ' dark-text--text'
146
146
  }
147
147
  return classes
148
148
  }
@@ -276,6 +276,9 @@ button.button-icon.button-icon--outline {
276
276
  background-color: #fff !important;
277
277
  border-width: 4px;
278
278
  border-style: solid;
279
+ i.clickable--icon.dark-text--text {
280
+ color: var(--v-dark-text-base) !important;
281
+ }
279
282
  }
280
283
 
281
284
  button.button-icon.button-icon--rounded {
@@ -33,6 +33,7 @@
33
33
  v-model="response"
34
34
  :height="200"
35
35
  menubar=""
36
+ :toolbar="'styles | bold italic underline strikethrough removeformat | alignleft aligncenter alignright | table tablerowprops tablecellprops |bullist numlist outdent indent | a11yButton | undo redo'"
36
37
  ></TextEditor>
37
38
  <p class="pa-3 text-center">
38
39
  <v-btn
@@ -390,6 +390,9 @@ export default {
390
390
  this.emitCompleted()
391
391
  }
392
392
  }
393
+
394
+ // Manually emit `hook:updated` on timeupdates since otherwise the DOM isn't actually changing so the state never gets preserved
395
+ this.$emit('hook:updated')
393
396
  },
394
397
  async onBeforeSave() {
395
398
  this.block.body = 'video'
@@ -129,17 +129,21 @@
129
129
  </v-btn>
130
130
  </v-row>
131
131
  </v-container>
132
-
132
+
133
133
  <v-container class="pa-4 mb-6">
134
134
  <v-row>
135
135
  <v-col cols="12">
136
- <GenerateAIQuestionButton
137
- :course="course"
138
- :content="currentContent"
139
- :block="block"
140
- question-type="scenario_game"
141
- @click:generate="onGeneratedScenarioGame"
142
- ></GenerateAIQuestionButton>
136
+ <PluginRef
137
+ target="contentBlockSettingTool"
138
+ :attrs="{
139
+ value: block,
140
+ course: course,
141
+ content: currentContent,
142
+ }"
143
+ :on="{
144
+ append: onPluginAppendBlock,
145
+ }"
146
+ ></PluginRef>
143
147
  </v-col>
144
148
  </v-row>
145
149
  </v-container>
@@ -203,14 +207,14 @@
203
207
  </template>
204
208
  <script>
205
209
  import _ from 'lodash'
206
- import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
207
210
  import { mapGetters } from 'vuex'
211
+ import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
208
212
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
209
213
  import SortableExpansionPanel from '~/components/Core/SortableExpansionPanel.vue'
210
214
  import DialogBox from '~/components/Core/DialogBox.vue'
211
215
  import Uuid from '~/helpers/Uuid'
212
216
  import TextEditor from '~/components/Text/TextEditor'
213
- import GenerateAIQuestionButton from '../utils/GenerateAIQuestionButton.vue'
217
+ import PluginRef from '~/components/Core/PluginRef.vue'
214
218
 
215
219
  export default {
216
220
  name: 'ScenarioChoiceSettings',
@@ -219,7 +223,7 @@ export default {
219
223
  TextEditor,
220
224
  DialogBox,
221
225
  BaseContentBlockSettings,
222
- GenerateAIQuestionButton,
226
+ PluginRef,
223
227
  },
224
228
  extends: BaseContentSettings,
225
229
  data() {
@@ -369,41 +373,52 @@ export default {
369
373
  !_.isEmpty(this.block.metadata.config.link_content_id) &&
370
374
  !_.isEmpty(this.block.metadata.config.link_text)
371
375
  },
372
- onGeneratedScenarioGame(activityData) {
376
+ onPluginAppendBlock(activityData) {
373
377
  // Process the activity data
374
- if (activityData && activityData.metadata &&
378
+ if (
379
+ activityData &&
380
+ activityData.metadata &&
375
381
  activityData.metadata.config &&
376
382
  activityData.metadata.config.items &&
377
- Array.isArray(activityData.metadata.config.items)) {
378
-
383
+ Array.isArray(activityData.metadata.config.items)
384
+ ) {
379
385
  // Clear existing items and replace with generated ones
380
- this.block.metadata.config.items.splice(0, this.block.metadata.config.items.length)
386
+ this.block.metadata.config.items.splice(
387
+ 0,
388
+ this.block.metadata.config.items.length
389
+ )
381
390
 
382
391
  // Add all new choices
383
- activityData.metadata.config.items.forEach(item => {
392
+ activityData.metadata.config.items.forEach((item) => {
384
393
  this.block.metadata.config.items.push({
385
394
  title: item.title || '',
386
395
  body: item.body || '<p></p>',
387
- correct: item.correct || false
396
+ correct: item.correct || false,
388
397
  })
389
398
  })
390
399
 
391
400
  // Update title and instructions if provided
392
401
  if (activityData.metadata.config.title) {
393
- this.block.metadata.config.title = activityData.metadata.config.title
402
+ this.block.metadata.config.title =
403
+ activityData.metadata.config.title
394
404
  }
395
405
 
396
406
  if (activityData.metadata.config.instructions) {
397
- this.block.metadata.config.instructions = activityData.metadata.config.instructions
407
+ this.block.metadata.config.instructions =
408
+ activityData.metadata.config.instructions
398
409
  }
399
410
 
400
411
  this.$toast.success(
401
- this.$t('windward.core.components.settings.scenario_choice.generated_successfully'),
412
+ this.$t(
413
+ 'windward.core.components.settings.scenario_choice.generated_successfully'
414
+ ),
402
415
  { duration: 3000 }
403
416
  )
404
417
  } else {
405
418
  this.$toast.error(
406
- this.$t('windward.core.components.settings.scenario_choice.invalid_response'),
419
+ this.$t(
420
+ 'windward.core.components.settings.scenario_choice.invalid_response'
421
+ ),
407
422
  { duration: 5000 }
408
423
  )
409
424
  }
@@ -3,7 +3,7 @@
3
3
  <ContentBlockAsset
4
4
  :value="source"
5
5
  :assets="assets"
6
- mimes="video/mp4,video/webm,video/youtube,audio/mpeg,audio/ogg,audio/webm,audio/wav"
6
+ mimes="video/mp4,video/webm,video/youtube,video/vimeo,audio/mpeg,audio/ogg,audio/webm,audio/wav"
7
7
  allow-url
8
8
  class="mb-4"
9
9
  :disabled="disabled"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/core",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Windward UI Core Plugins",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "homepage": "https://bitbucket.org/mindedge/windward-ui-plugin-core#readme",
23
23
  "dependencies": {
24
- "@mindedge/vuetify-player": "^0.4.9",
24
+ "@mindedge/vuetify-player": "^0.5.1",
25
25
  "@tinymce/tinymce-vue": "^3.2.8",
26
26
  "accessibility-scanner": "^0.0.1",
27
27
  "eslint": "^8.11.0",
package/plugin.js CHANGED
@@ -294,7 +294,7 @@ export default {
294
294
  },
295
295
  },
296
296
  ],
297
- settings: [
297
+ contentBlockSetting: [
298
298
  {
299
299
  tag: 'core-open-response-settings',
300
300
  template: OpenResponseSettings,
package/utils/index.js CHANGED
@@ -7,7 +7,6 @@ import TinyMCEWrapper from '../components/utils/TinyMCEWrapper.vue'
7
7
  import MathExpressionEditor from '../components/utils/MathExpressionEditor'
8
8
  import CourseGlossary from '../components/utils/glossary/CourseGlossary.vue'
9
9
  import CourseGlossaryForm from '../components/utils/glossary/CourseGlossaryForm.vue'
10
- import GenerateAIQuestionButton from '../components/utils/GenerateAIQuestionButton.vue'
11
10
 
12
11
  export {
13
12
  MathHelper,
@@ -17,5 +16,4 @@ export {
17
16
  TinyMCEWrapper,
18
17
  CourseGlossary,
19
18
  CourseGlossaryForm,
20
- GenerateAIQuestionButton,
21
19
  }
@@ -1,826 +0,0 @@
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="!isBucketGameType"
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
- <div v-if="isBucketGameType" class="text-caption mt-1">
43
- {{ $t('windward.core.components.content.blocks.generate_questions.replaces_content') }}
44
- </div>
45
- </v-col>
46
- <v-col
47
- v-if="isFlashcardType || isMatchingGameType || isSortingGameType || isWordJumbleType"
48
- cols="auto"
49
- class="d-flex align-center"
50
- >
51
- <v-switch
52
- v-model="replaceExisting"
53
- hide-details
54
- dense
55
- color="secondary"
56
- class="mt-0 pt-0"
57
- :label="replaceExistingLabel"
58
- :disabled="isLoading"
59
- ></v-switch>
60
- </v-col>
61
- <v-col>
62
- <v-btn
63
- elevation="0"
64
- color="secondary"
65
- class="mb-1 btn-selector"
66
- :loading="isLoading"
67
- :disabled="isLoading"
68
- @click="generateAIQuestion"
69
- >
70
- <v-icon v-if="!isLoading" class="pr-1"
71
- >mdi-magic-staff</v-icon
72
- >
73
- {{ buttonLabel }}
74
- <template v-slot:loader>
75
- <v-progress-circular
76
- indeterminate
77
- size="23"
78
- ></v-progress-circular>
79
- </template>
80
- </v-btn>
81
- </v-col>
82
- </v-row>
83
- </div>
84
- </template>
85
-
86
- <script>
87
- import _ from 'lodash'
88
- import { mapGetters } from 'vuex'
89
- import AssessmentQuestion from '~/models/AssessmentQuestion'
90
- import Course from '~/models/Course'
91
- import Assessment from '~/models/Assessment'
92
- import Content from '~/models/Content'
93
- import Activity from '../../models/Activity'
94
-
95
- export default {
96
- name: 'GenerateAIQuestionButton',
97
- props: {
98
- course: { type: Object, required: true },
99
- content: { type: Object, required: true },
100
- block: { type: Object, required: true },
101
- questionType: { type: String, required: true },
102
- replaceExistingMode: { type: Boolean, default: false },
103
- },
104
- data() {
105
- return {
106
- isLoading: false,
107
- selectedContent: '',
108
- selectedDifficulty: {
109
- value: 'None',
110
- text: this.$t(
111
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
112
- ),
113
- },
114
- replaceExisting: this.questionType === 'bucket_game' || this.questionType === 'word_jumble' ? true : this.replaceExistingMode,
115
- }
116
- },
117
- computed: {
118
- ...mapGetters({
119
- contentTree: 'content/getTree',
120
- }),
121
- isFlashcardType() {
122
- return this.questionType === 'flashcard'
123
- },
124
- isBucketGameType() {
125
- return this.questionType === 'bucket_game'
126
- },
127
- isMatchingGameType() {
128
-
129
- return this.questionType === 'matching_game'
130
- },
131
- isSortingGameType() {
132
- return this.questionType === 'sorting_game'
133
- },
134
- isScenarioGameType() {
135
- return this.questionType === 'scenario_game'
136
- },
137
- isWordJumbleType() {
138
- return this.questionType === 'word_jumble'
139
- },
140
- replaceExistingLabel() {
141
- if (this.isBucketGameType) {
142
- return this.$t(
143
- 'windward.games.components.settings.bucket_game.form.replace_existing'
144
- )
145
- return this.$t(
146
- 'windward.games.components.settings.bucket_game.form.replace_existing'
147
- )
148
- }
149
- if (this.isMatchingGameType) {
150
- return this.$t(
151
- 'windward.games.components.settings.matching_game.form.replace_existing')
152
- }
153
- if (this.isSortingGameType) {
154
- return this.$t(
155
- 'windward.games.components.settings.sorting_game.form.replace_existing')
156
- }
157
- if (this.isWordJumbleType) {
158
- return this.$t(
159
- 'windward.games.components.settings.word_jumble.form.replace_existing')
160
- }
161
- return this.$t(
162
- 'windward.games.components.settings.flashcard.form.replace_existing'
163
-
164
- )
165
- },
166
- flattenedContent() {
167
- let cloneContentTree = _.cloneDeep(this.contentTree)
168
- const homepage = this.$ContentService.getHomepage()
169
- if (!_.isEmpty(homepage)) {
170
- cloneContentTree.unshift(homepage)
171
- }
172
- let fullTree = []
173
- // flatten content tree to get nested children pages
174
- cloneContentTree.forEach((content) => {
175
- fullTree.push(content)
176
- if (content.children.length > 0) {
177
- fullTree = fullTree.concat(_.flatten(content.children))
178
- // check if children have children
179
- content.children.forEach((child) => {
180
- fullTree = fullTree.concat(_.flatten(child.children))
181
- })
182
- }
183
- })
184
- //
185
- if (_.isEmpty(this.selectedContent)) {
186
- // returns array so hold here to pluck out below
187
- const currentPage = fullTree.filter(
188
- (contentPage) => contentPage.id === this.content.id
189
- )
190
- this.selectedContent = currentPage[0] ? currentPage[0] : ''
191
- }
192
- return fullTree
193
- },
194
- taxonomyLevels() {
195
- // For word jumble games, only show None, Remember, Understand, Apply
196
- if (this.isWordJumbleType) {
197
- return [
198
- {
199
- value: 'None',
200
- text: this.$t(
201
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
202
- ),
203
- },
204
- {
205
- value: 'Remember',
206
- text: this.$t(
207
- 'windward.core.components.content.blocks.generate_questions.blooms.remember'
208
- ),
209
- },
210
- {
211
- value: 'Understand',
212
- text: this.$t(
213
- 'windward.core.components.content.blocks.generate_questions.blooms.understand'
214
- ),
215
- },
216
- {
217
- value: 'Apply',
218
- text: this.$t(
219
- 'windward.core.components.content.blocks.generate_questions.blooms.apply'
220
- ),
221
- },
222
- ]
223
- }
224
-
225
- // For scenario games, only show None, Apply, Analyze, Evaluate
226
- if (this.isScenarioGameType) {
227
- return [
228
- {
229
- value: 'None',
230
- text: this.$t(
231
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
232
- ),
233
- },
234
- {
235
- value: 'Apply',
236
- text: this.$t(
237
- 'windward.core.components.content.blocks.generate_questions.blooms.apply'
238
- ),
239
- },
240
- {
241
- value: 'Analyze',
242
- text: this.$t(
243
- 'windward.core.components.content.blocks.generate_questions.blooms.analyze'
244
- ),
245
- },
246
- {
247
- value: 'Evaluate',
248
- text: this.$t(
249
- 'windward.core.components.content.blocks.generate_questions.blooms.evaluate'
250
- ),
251
- },
252
- ]
253
- }
254
-
255
- // Basic Bloom's taxonomy levels available to all question types
256
- let basicBloomTaxonomy = [
257
- {
258
- value: 'None',
259
- text: this.$t(
260
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
261
- ),
262
- },
263
- {
264
- value: 'Remember',
265
- text: this.$t(
266
- 'windward.core.components.content.blocks.generate_questions.blooms.remember'
267
- ),
268
- },
269
- {
270
- value: 'Understand',
271
- text: this.$t(
272
- 'windward.core.components.content.blocks.generate_questions.blooms.understand'
273
- ),
274
- },
275
- {
276
- value: 'Apply',
277
- text: this.$t(
278
- 'windward.core.components.content.blocks.generate_questions.blooms.apply'
279
- ),
280
- },
281
- ]
282
-
283
- // Only add higher-level Bloom's taxonomy for supported question types
284
- const supportsAdvancedTaxonomy =
285
- this.isSortingGameType ||
286
- this.isScenarioGameType ||
287
- ([
288
- 'multi_choice_single_answer',
289
- 'multi_choice_multi_answer',
290
- 'ordering'
291
- ].includes(this.questionType) &&
292
- !this.isFlashcardType &&
293
- !this.isBucketGameType &&
294
- !this.isMatchingGameType)
295
-
296
- if (supportsAdvancedTaxonomy) {
297
- const multiBlooms = [
298
- {
299
- value: 'Analyze',
300
- text: this.$t(
301
- 'windward.core.components.content.blocks.generate_questions.blooms.analyze'
302
- ),
303
- },
304
- {
305
- value: 'Evaluate',
306
- text: this.$t(
307
- 'windward.core.components.content.blocks.generate_questions.blooms.evaluate'
308
- ),
309
- },
310
- ]
311
- basicBloomTaxonomy = basicBloomTaxonomy.concat(multiBlooms)
312
- }
313
- return basicBloomTaxonomy
314
- },
315
- buttonLabel() {
316
- if (this.questionType === 'flashcard') {
317
- return this.$t(
318
- 'windward.core.components.content.blocks.generate_questions.button_label_flashcard'
319
- )
320
- } else if (this.questionType === 'bucket_game') {
321
- return this.$t(
322
- 'windward.core.components.content.blocks.generate_questions.button_label_bucket_game'
323
- )
324
- } else if (this.questionType === 'matching_game') {
325
- return this.$t(
326
- 'windward.core.components.content.blocks.generate_questions.button_label_matching_game'
327
- )
328
- } else if (this.questionType === 'sorting_game') {
329
- return this.$t(
330
- 'windward.core.components.content.blocks.generate_questions.button_label_sorting_game'
331
- )
332
- } else if (this.questionType === 'scenario_game') {
333
- return this.$t(
334
- 'windward.core.components.content.blocks.generate_questions.button_label_scenario_game'
335
- )
336
- } else if (this.questionType === 'word_jumble') {
337
- return this.$t(
338
- 'windward.core.components.content.blocks.generate_questions.button_label_word_jumble'
339
- )
340
- } else {
341
- return this.$t(
342
- 'windward.core.components.content.blocks.generate_questions.button_label'
343
- )
344
- }
345
- },
346
- },
347
- methods: {
348
- async generateAIQuestion() {
349
- this.isLoading = true
350
- try {
351
- let bloomsRequest = ''
352
- if (
353
- this.selectedDifficulty.text !==
354
- this.$t(
355
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
356
- )
357
- ) {
358
- bloomsRequest = `?blooms_level=${this.selectedDifficulty.value}`
359
- }
360
-
361
- const course = new Course(this.course)
362
- const contentData = this.selectedContent || this.content
363
- const content = new Content(contentData)
364
-
365
- let response
366
- if (this.questionType === 'flashcard') {
367
- // FLASHCARD GENERATION
368
- const activity = new Activity()
369
-
370
- const endpoint = `suggest/flashcard${bloomsRequest}`
371
-
372
- // Call the endpoint exactly like FlashCardSlidesManager does
373
- response = await Activity.custom(
374
- course,
375
- content,
376
- activity,
377
- endpoint
378
- ).get()
379
-
380
- let activityData = null
381
-
382
- if (response && response.activity) {
383
- activityData = response.activity
384
- } else if (
385
- response &&
386
- response.length > 0 &&
387
- response[0] &&
388
- response[0].activity
389
- ) {
390
- activityData = response[0].activity
391
- } else if (Array.isArray(response) && response.length > 0) {
392
- activityData = response[0]
393
- }
394
-
395
- if (
396
- activityData &&
397
- activityData.metadata &&
398
- activityData.metadata.config &&
399
- activityData.metadata.config.cards &&
400
- Array.isArray(activityData.metadata.config.cards)
401
- ) {
402
- // We pass the activity data and the replace flag to the parent component
403
- this.$emit(
404
- 'click:generate',
405
- activityData,
406
- this.replaceExisting
407
- )
408
- } else {
409
- throw new Error(
410
- 'Invalid response from flashcard generation'
411
- )
412
- }
413
- } else if (this.questionType === 'bucket_game') {
414
- // BUCKET GAME GENERATION
415
- const activity = new Activity()
416
-
417
- const endpoint = `suggest/bucket_game${bloomsRequest}`
418
-
419
- response = await Activity.custom(
420
- course,
421
- content,
422
- activity,
423
- endpoint
424
- ).get()
425
-
426
- let activityData = null
427
-
428
- if (response && response.activity) {
429
- activityData = response.activity
430
- } else if (
431
- response &&
432
- response.length > 0 &&
433
- response[0] &&
434
- response[0].activity
435
- ) {
436
- activityData = response[0].activity
437
- } else if (Array.isArray(response) && response.length > 0) {
438
- activityData = response[0]
439
- }
440
-
441
- if (
442
- activityData &&
443
- activityData.metadata &&
444
- activityData.metadata.config &&
445
- activityData.metadata.config.bucket_titles &&
446
- activityData.metadata.config.bucket_answers
447
- ) {
448
- // For bucket games, always use replace mode
449
- this.$emit(
450
- 'click:generate',
451
- activityData,
452
- true
453
- )
454
- } else {
455
- throw new Error(
456
- 'Invalid response from bucket game generation'
457
- )
458
- }
459
- } else if (this.questionType === 'matching_game') {
460
- // MATCHING GAME GENERATION
461
- const activity = new Activity()
462
-
463
- const endpoint = `suggest/matching_game${bloomsRequest}`;
464
-
465
- response = await Activity.custom(
466
- course,
467
- content,
468
- activity,
469
- endpoint
470
- ).get()
471
-
472
- let activityData = null
473
-
474
- if (response && response.activity) {
475
- activityData = response.activity
476
- } else if (
477
- response &&
478
- response.length > 0 &&
479
- response[0] &&
480
- response[0].activity
481
- ) {
482
- activityData = response[0].activity
483
- } else if (Array.isArray(response) && response.length > 0) {
484
- activityData = response[0]
485
- }
486
-
487
- if (activityData && activityData.metadata &&
488
- activityData.metadata.config &&
489
- activityData.metadata.config.answerObjects &&
490
- activityData.metadata.config.prompts
491
- ) {
492
- // We pass the activity data and the replace flag to the parent component
493
- this.$emit(
494
- 'click:generate',
495
- activityData,
496
- this.replaceExisting
497
- )
498
- } else {
499
- throw new Error(
500
- 'Invalid response from matching game generation'
501
- )
502
- }
503
- } else if (this.questionType === 'sorting_game') {
504
- // SORTING GAME GENERATION
505
- const activity = new Activity()
506
-
507
- const endpoint = `suggest/sorting_game${bloomsRequest}`;
508
-
509
- response = await Activity.custom(
510
- course,
511
- content,
512
- activity,
513
- endpoint
514
- ).get()
515
-
516
- let activityData = null
517
-
518
- if (response && response.activity) {
519
- activityData = response.activity
520
- } else if (
521
- response &&
522
- response.length > 0 &&
523
- response[0] &&
524
- response[0].activity
525
- ) {
526
- activityData = response[0].activity
527
- } else if (Array.isArray(response) && response.length > 0) {
528
- activityData = response[0]
529
- } else if (response) {
530
- activityData = response
531
- }
532
-
533
- if (activityData && activityData.metadata &&
534
- activityData.metadata.config &&
535
- activityData.metadata.config.answer &&
536
- Array.isArray(activityData.metadata.config.answer)
537
- ) {
538
- // We pass the activity data and the replace flag to the parent component
539
- this.$emit(
540
- 'click:generate',
541
- activityData,
542
- this.replaceExisting
543
- )
544
- } else {
545
- throw new Error(
546
- 'Invalid response from sorting game generation'
547
- )
548
- }
549
- } else if (this.questionType === 'scenario_game') {
550
- // SCENARIO GAME GENERATION
551
- const activity = new Activity()
552
-
553
- const endpoint = `suggest/scenario_game${bloomsRequest}`;
554
-
555
- response = await Activity.custom(
556
- course,
557
- content,
558
- activity,
559
- endpoint
560
- ).get()
561
-
562
- let activityData = null
563
-
564
- if (response && response.activity) {
565
- activityData = response.activity
566
- } else if (
567
- response &&
568
- response.length > 0 &&
569
- response[0] &&
570
- response[0].activity
571
- ) {
572
- activityData = response[0].activity
573
- } else if (Array.isArray(response) && response.length > 0) {
574
- activityData = response[0]
575
- } else if (response) {
576
- activityData = response
577
- }
578
-
579
- if (activityData && activityData.metadata &&
580
- activityData.metadata.config &&
581
- activityData.metadata.config.items &&
582
- Array.isArray(activityData.metadata.config.items)
583
- ) {
584
- // For scenario games, always replace existing content
585
- this.$emit(
586
- 'click:generate',
587
- activityData,
588
- true
589
- )
590
- } else {
591
- throw new Error('activity.error.content_mismatch')
592
- }
593
- } else if (this.questionType === 'word_jumble') {
594
- // WORD JUMBLE GENERATION
595
- const activity = new Activity()
596
-
597
- const endpoint = `suggest/word_jumble${bloomsRequest}`
598
-
599
- response = await Activity.custom(
600
- course,
601
- content,
602
- activity,
603
- endpoint
604
- ).get()
605
-
606
- let activityData = null
607
-
608
- if (response && response.activity) {
609
- activityData = response.activity
610
- } else if (
611
- response &&
612
- response.length > 0 &&
613
- response[0] &&
614
- response[0].activity
615
- ) {
616
- activityData = response[0].activity
617
- } else if (Array.isArray(response) && response.length > 0) {
618
- activityData = response[0]
619
- }
620
-
621
- if (
622
- activityData &&
623
- activityData.metadata &&
624
- activityData.metadata.config &&
625
- activityData.metadata.config.words &&
626
- Array.isArray(activityData.metadata.config.words)
627
- ) {
628
- // We pass the activity data and the replace flag to the parent component
629
- this.$emit(
630
- 'click:generate',
631
- activityData,
632
- this.replaceExisting
633
- )
634
- } else {
635
- throw new Error(
636
- 'Invalid response from word jumble generation'
637
- )
638
- }
639
- } else {
640
- // ASSESSMENT QUESTION GENERATION
641
- const assessment = new Assessment({ id: this.block.id })
642
- const question = new AssessmentQuestion()
643
-
644
- response = await AssessmentQuestion.custom(
645
- course,
646
- content,
647
- assessment,
648
- question,
649
- `suggest/${this.questionType}${bloomsRequest}`
650
- ).get()
651
-
652
- if (response && response.length > 0) {
653
- const generatedQuestion = response[0]
654
- this.$emit('click:generate', generatedQuestion)
655
- } else {
656
- throw new Error(
657
- 'Invalid response from question generation'
658
- )
659
- }
660
- }
661
- } catch (error) {
662
- const errorMessage =
663
- error.response?.data?.error?.message ||
664
- error.message ||
665
- 'assessment.error.technical'
666
- const errorType = errorMessage.split('.').pop()
667
- const basePath =
668
- 'windward.core.components.content.blocks.generate_questions.error'
669
-
670
- let errorText = ''
671
-
672
- // Check for character limit error by structured data first
673
- const errorSubtype = error.response?.data?.error?.details?.error_subtype
674
- const errorDetails = error.response?.data?.error?.details
675
-
676
- if (errorSubtype === 'CHARACTER_LIMIT_EXCEEDED') {
677
- // Use structured data if available
678
- const field = errorDetails?.field || 'content'
679
- const limit = errorDetails?.limit
680
- const actual = errorDetails?.actual
681
-
682
- // Use parameterized message if we have the data
683
- if (limit && actual) {
684
- errorText = this.$t(`${basePath}.character_limit_detailed`, { field, limit, actual })
685
- } else {
686
- // Fallback to generic message
687
- errorText = this.$t(`${basePath}.character_limit`)
688
- }
689
- }
690
- // Keep backward compatibility - check for old message pattern
691
- else if (errorDetails?.message === 'An error occurred while generating content. Try generating again.') {
692
- errorText = this.$t(`${basePath}.character_limit`)
693
- } else if (
694
- // Check for content mismatch error specifically for bucket games
695
- (errorMessage === 'activity.error.content_mismatch' ||
696
- errorType === 'content_mismatch') &&
697
- this.questionType === 'bucket_game'
698
- ) {
699
- errorText =
700
- this.$t(`${basePath}.content_mismatch_bucket_game`) +
701
- '\n\n' +
702
- this.$t(
703
- `${basePath}.content_mismatch_bucket_game_support`
704
- )
705
- } else if (
706
- (errorMessage === 'activity.error.content_mismatch' ||
707
- errorType === 'content_mismatch') &&
708
- this.questionType === 'matching_game'
709
- ) {
710
- errorText =
711
- this.$t(`${basePath}.content_mismatch_matching_game`) +
712
- '\n\n' +
713
- this.$t(
714
- `${basePath}.content_mismatch_matching_game_support`
715
- )
716
- } else if (
717
- (errorMessage === 'activity.error.content_mismatch' ||
718
- errorType === 'content_mismatch') &&
719
- this.questionType === 'sorting_game'
720
- ) {
721
- errorText =
722
- this.$t(`${basePath}.content_mismatch_sorting_game`) +
723
- '\n\n' +
724
- this.$t(
725
- `${basePath}.content_mismatch_sorting_game_support`
726
- )
727
- } else if (
728
- (errorMessage === 'activity.error.content_mismatch' ||
729
- errorType === 'content_mismatch') &&
730
- this.questionType === 'scenario_game'
731
- ) {
732
- errorText =
733
- this.$t(`${basePath}.content_mismatch_scenario_game`) +
734
- '\n\n' +
735
- this.$t(
736
- `${basePath}.content_mismatch_scenario_game_support`
737
- )
738
- } else if (
739
- (errorMessage === 'activity.error.content_mismatch' ||
740
- errorType === 'content_mismatch') &&
741
- this.questionType === 'word_jumble'
742
- ) {
743
- errorText =
744
- this.$t(`${basePath}.content_mismatch_word_jumble`) +
745
- '\n\n' +
746
- this.$t(
747
- `${basePath}.content_mismatch_word_jumble_support`
748
- )
749
- } else if (errorType === 'insufficient_content' && error.response?.data?.error?.details) {
750
- // Handle dynamic Bloom's taxonomy insufficient content errors
751
- const details = error.response.data.error.details
752
- const bloomsLevel = details.blooms_level
753
- const minimumRequired = details.minimum_required
754
-
755
- if (bloomsLevel && bloomsLevel !== 'None') {
756
- // Message with Bloom's level
757
- errorText =
758
- this.$t(`${basePath}.insufficient_content_blooms`, { bloomsLevel }) +
759
- '\n\n' +
760
- this.$t(`${basePath}.insufficient_content_blooms_support`, { minimumRequired })
761
- } else if (minimumRequired) {
762
- // Message without Bloom's level but with word count
763
- errorText =
764
- this.$t(`${basePath}.insufficient_content_dynamic`) +
765
- '\n\n' +
766
- this.$t(`${basePath}.insufficient_content_dynamic_support`, { minimumRequired })
767
- } else {
768
- // Fallback to static translation
769
- errorText =
770
- this.$t(`${basePath}.${errorType}`) +
771
- '\n\n' +
772
- this.$t(`${basePath}.${errorType}_support`)
773
- }
774
- } else {
775
- // Check if the error type is a valid i18n key
776
- const errorKey = `${basePath}.${errorType}`
777
- const supportKey = `${basePath}.${errorType}_support`
778
-
779
- // Try to get the translation, fall back to default error if not found
780
- const hasTranslation = this.$te(errorKey)
781
-
782
- if (hasTranslation) {
783
- errorText =
784
- this.$t(errorKey) +
785
- '\n\n' +
786
- this.$t(supportKey)
787
- } else {
788
- // Fall back to default error message
789
- errorText =
790
- this.$t(`${basePath}.default`) +
791
- '\n\n' +
792
- this.$t(`${basePath}.default_support`)
793
- }
794
-
795
- if (errorType === 'technical') {
796
- const errorCode =
797
- error.response?.data?.error?.details?.error_type ||
798
- 'UNKNOWN'
799
- errorText = errorText.replace('[ERROR_CODE]', errorCode)
800
- }
801
- }
802
-
803
- this.$dialog.error(errorText, {
804
- duration: 5000,
805
- keepOnHover: true,
806
- singleton: true,
807
- type: 'error',
808
- })
809
- } finally {
810
- this.isLoading = false
811
- }
812
- },
813
- },
814
- }
815
- </script>
816
-
817
- <style scoped>
818
- .btn-selector {
819
- width: 100%;
820
- }
821
- .container-generate-ai {
822
- outline: 1px solid var(--v-secondary-base);
823
- border-radius: 15px;
824
- }
825
- </style>
826
-