@windward/core 0.21.1 → 0.22.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,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## Release [0.22.0] - 2025-08-25
4
+
5
+ * Merged in bugfix/LE-2075-the-panel-of-the-text-block-isnt (pull request #427)
6
+ * Merged in bug-fix/LE-2060/llm-character-limit-validation (pull request #421)
7
+ * Merged in bugfix/LE-2031-open-response-download-collate-q (pull request #422)
8
+ * Merged in bugfix/LE-1960-video-using-urls-as-a-source (pull request #423)
9
+ * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
10
+ * Merged in bugfix/LE-2057-save-button-disappeared-again-on (pull request #419)
11
+ * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
12
+ * Merged in feature/LE-2036/word-jumble-gen (pull request #420)
13
+ * Merged in feature/LE-1997/scenario-gen (pull request #416)
14
+ * Merged in bugfix/LE-1928-user-upload-allowed-file-types (pull request #415)
15
+ * Merged release/0.21.0 into bugfix/LE-1960-video-using-urls-as-a-source
16
+ * Merged release/0.21.0 into bugfix/LE-1928-user-upload-allowed-file-types
17
+
3
18
  ## Hotfix [0.21.1] created - 2025-08-20
4
19
 
5
20
 
@@ -82,10 +82,9 @@ export default {
82
82
  .get()
83
83
 
84
84
  let collated = ''
85
- // sort by order on the page
86
- const sortedStates = userState.sort((a, b) =>
87
- a.content_block.order < b.content_block.order ? -1 : 1
88
- )
85
+
86
+ // sort by states by page order in course then by order on the page
87
+ const sortedStates = this.sortStates(userState)
89
88
 
90
89
  sortedStates.forEach((state) => {
91
90
  // Prepend the prompt from the state if include prompts is enabled
@@ -150,6 +149,30 @@ export default {
150
149
  )
151
150
  }
152
151
  },
152
+ sortStates(states) {
153
+ // order by appearance on page
154
+ states = states.sort((a, b) =>
155
+ a.content_block.order < b.content_block.order ? -1 : 1
156
+ )
157
+ const contentBlocksByPage = []
158
+ // get page order in course
159
+ const contentBlockTree = this.$ContentService.getFlatTree()
160
+ // get homepage and add to front
161
+ const homePage = this.$ContentService.getHomepage()
162
+ if (homePage !== null) {
163
+ contentBlockTree.unshift(homePage)
164
+ }
165
+ // order states by page in the course
166
+ contentBlockTree.forEach((block) => {
167
+ states.forEach((state) => {
168
+ if (state.content_block?.content_id === block.content?.id) {
169
+ contentBlocksByPage.push(state)
170
+ }
171
+ })
172
+ })
173
+
174
+ return contentBlocksByPage
175
+ },
153
176
  generateDocument(htmlBody, filename = '') {
154
177
  // Specify file name. If one isn't supplied then a default name of `exported_document_YYYY-MM-DD.doc` is used
155
178
  filename = filename
@@ -55,24 +55,24 @@
55
55
  @seeking="onSeeking"
56
56
  @timeupdate="onTimeupdate"
57
57
  >
58
- <template #no-source>
59
- <v-card>
60
- <v-card-title class="justify-center">
61
- <v-icon class="mr-2">mdi-cloud-question</v-icon>
62
- {{
58
+ <template #no-source>
59
+ <v-card>
60
+ <v-card-title class="justify-center">
61
+ <v-icon class="mr-2">mdi-cloud-question</v-icon>
62
+ {{
63
+ $t(
64
+ 'windward.core.components.content.blocks.video.not_configured_title'
65
+ )
66
+ }}
67
+ </v-card-title>
68
+ <v-card-text>{{
63
69
  $t(
64
- 'windward.core.components.content.blocks.video.not_configured_title'
70
+ 'windward.core.components.content.blocks.video.edit_prompt'
65
71
  )
66
- }}
67
- </v-card-title>
68
- <v-card-text>{{
69
- $t(
70
- 'windward.core.components.content.blocks.video.edit_prompt'
71
- )
72
- }}</v-card-text>
73
- </v-card>
74
- </template>
75
- </VuetifyPlayer>
72
+ }}</v-card-text>
73
+ </v-card>
74
+ </template>
75
+ </VuetifyPlayer>
76
76
  </div>
77
77
  <!-- display first note in the playlist for now -->
78
78
  <v-alert
@@ -103,6 +103,45 @@ export default {
103
103
  VuetifyPlayer,
104
104
  },
105
105
  extends: BaseContentBlock,
106
+ data() {
107
+ return {
108
+ fileTab: null,
109
+ // Default config settings
110
+ defaultConfig: {
111
+ title: '',
112
+ instructions: '',
113
+ // Default settings for new blocks
114
+ // This will be overridden in beforeMount()
115
+ type: 'auto', // Allowed auto|video|audio. In audio mode the player has a max-height of 40px. Auto will switch between the other types when needed
116
+ attributes: {
117
+ autoplay: false, // Autoplay on load. It's in the spec but DON'T USE THIS
118
+ autopictureinpicture: false, // Start with picture in picture mode
119
+ controls: true, // Show video controls. When false only play/pause allowed but clicking on the video itself
120
+ controlslist: 'nodownload noremoteplayback', // Space separated string per <video>. Allowed 'nodownload nofullscreen noremoteplayback'
121
+ crossorigin: 'anonymous',
122
+ disablepictureinpicture: true, // Shows the picture in picture button
123
+ disableremoteplayback: true, // Shows the remote playback button but functionality does not exist when clicked
124
+ height: 'auto',
125
+ width: '100%',
126
+ rewind: true, // Enabled the rewind 10s button
127
+ loop: false, // Loop the video on completion
128
+ muted: false, // Start the video muted
129
+ playsinline: false, // Force inline & disable fullscreen
130
+ poster: '', // Overridden with the playlist.poster if one is set there
131
+ preload: '',
132
+ captionsmenu: true, // Show the captions below the video
133
+ playlistmenu: true, // Show the playlist menu if there's multiple videos
134
+ playlistautoadvance: true, // Play the next source group
135
+ playbackrates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], // Default playback speeds
136
+ },
137
+ playlist: [],
138
+ },
139
+ tracking: {
140
+ hasSkipped: false,
141
+ percentComplete: 0,
142
+ },
143
+ }
144
+ },
106
145
  computed: {
107
146
  hasSource() {
108
147
  // __isDirty is used to communicate from the settings panel that a source / caption
@@ -312,83 +351,6 @@ export default {
312
351
  },
313
352
  },
314
353
  },
315
- data() {
316
- return {
317
- fileTab: null,
318
- // Default config settings
319
- defaultConfig: {
320
- title: '',
321
- instructions: '',
322
- // Default settings for new blocks
323
- // This will be overridden in beforeMount()
324
- type: 'auto', // Allowed auto|video|audio. In audio mode the player has a max-height of 40px. Auto will switch between the other types when needed
325
- attributes: {
326
- autoplay: false, // Autoplay on load. It's in the spec but DON'T USE THIS
327
- autopictureinpicture: false, // Start with picture in picture mode
328
- controls: true, // Show video controls. When false only play/pause allowed but clicking on the video itself
329
- controlslist: 'nodownload noremoteplayback', // Space separated string per <video>. Allowed 'nodownload nofullscreen noremoteplayback'
330
- crossorigin: 'anonymous',
331
- disablepictureinpicture: true, // Shows the picture in picture button
332
- disableremoteplayback: true, // Shows the remote playback button but functionality does not exist when clicked
333
- height: 'auto',
334
- width: '100%',
335
- rewind: true, // Enabled the rewind 10s button
336
- loop: false, // Loop the video on completion
337
- muted: false, // Start the video muted
338
- playsinline: false, // Force inline & disable fullscreen
339
- poster: '', // Overridden with the playlist.poster if one is set there
340
- preload: '',
341
- captionsmenu: true, // Show the captions below the video
342
- playlistmenu: true, // Show the playlist menu if there's multiple videos
343
- playlistautoadvance: true, // Play the next source group
344
- playbackrates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], // Default playback speeds
345
- },
346
- playlist: [
347
- /*{
348
- name: '', // The video name to appear on playlists
349
- poster: '', // A video specific poster in the playlist
350
- ads: [
351
- {
352
- play_at_percent: 0,
353
- sources: [
354
- {
355
- src: "https://domain.test/ad_example.mp4",
356
- type: "video/mp4",
357
- },
358
- ],
359
- tracks: [
360
- {
361
- src: "https://domain.test/ad_example-US.vtt",
362
- kind: "captions",
363
- srclang: "en-US",
364
- default: true,
365
- },
366
- ],
367
- },
368
- ],
369
- sources: [
370
- {
371
- src: "https://domain.test/example.mp4",
372
- type: "video/mp4",
373
- },
374
- ],
375
- tracks: [
376
- {
377
- src: "https://domain.test/example_en-US.vtt",
378
- kind: "captions",
379
- srclang: "en-US",
380
- default: true,
381
- },
382
- ],
383
- },*/
384
- ],
385
- },
386
- tracking: {
387
- hasSkipped: false,
388
- percentComplete: 0,
389
- },
390
- }
391
- },
392
354
  beforeMount() {
393
355
  // Apply the default config
394
356
  if (_.isEmpty(this.block.metadata.config)) {
@@ -129,6 +129,20 @@
129
129
  </v-btn>
130
130
  </v-row>
131
131
  </v-container>
132
+
133
+ <v-container class="pa-4 mb-6">
134
+ <v-row>
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>
143
+ </v-col>
144
+ </v-row>
145
+ </v-container>
132
146
 
133
147
  <DialogBox
134
148
  v-model="showLinkDialog"
@@ -196,6 +210,7 @@ import SortableExpansionPanel from '~/components/Core/SortableExpansionPanel.vue
196
210
  import DialogBox from '~/components/Core/DialogBox.vue'
197
211
  import Uuid from '~/helpers/Uuid'
198
212
  import TextEditor from '~/components/Text/TextEditor'
213
+ import GenerateAIQuestionButton from '../utils/GenerateAIQuestionButton.vue'
199
214
 
200
215
  export default {
201
216
  name: 'ScenarioChoiceSettings',
@@ -204,6 +219,7 @@ export default {
204
219
  TextEditor,
205
220
  DialogBox,
206
221
  BaseContentBlockSettings,
222
+ GenerateAIQuestionButton,
207
223
  },
208
224
  extends: BaseContentSettings,
209
225
  data() {
@@ -240,6 +256,8 @@ export default {
240
256
  computed: {
241
257
  ...mapGetters({
242
258
  contentTree: 'content/getTree',
259
+ course: 'course/get',
260
+ currentContent: 'content/get',
243
261
  }),
244
262
  },
245
263
  beforeMount() {
@@ -351,6 +369,45 @@ export default {
351
369
  !_.isEmpty(this.block.metadata.config.link_content_id) &&
352
370
  !_.isEmpty(this.block.metadata.config.link_text)
353
371
  },
372
+ onGeneratedScenarioGame(activityData) {
373
+ // Process the activity data
374
+ if (activityData && activityData.metadata &&
375
+ activityData.metadata.config &&
376
+ activityData.metadata.config.items &&
377
+ Array.isArray(activityData.metadata.config.items)) {
378
+
379
+ // Clear existing items and replace with generated ones
380
+ this.block.metadata.config.items.splice(0, this.block.metadata.config.items.length)
381
+
382
+ // Add all new choices
383
+ activityData.metadata.config.items.forEach(item => {
384
+ this.block.metadata.config.items.push({
385
+ title: item.title || '',
386
+ body: item.body || '<p></p>',
387
+ correct: item.correct || false
388
+ })
389
+ })
390
+
391
+ // Update title and instructions if provided
392
+ if (activityData.metadata.config.title) {
393
+ this.block.metadata.config.title = activityData.metadata.config.title
394
+ }
395
+
396
+ if (activityData.metadata.config.instructions) {
397
+ this.block.metadata.config.instructions = activityData.metadata.config.instructions
398
+ }
399
+
400
+ this.$toast.success(
401
+ this.$t('windward.core.components.settings.scenario_choice.generated_successfully'),
402
+ { duration: 3000 }
403
+ )
404
+ } else {
405
+ this.$toast.error(
406
+ this.$t('windward.core.components.settings.scenario_choice.invalid_response'),
407
+ { duration: 5000 }
408
+ )
409
+ }
410
+ },
354
411
  },
355
412
  }
356
413
  </script>
@@ -139,7 +139,7 @@ export default {
139
139
  name: this.$t(
140
140
  'windward.core.components.settings.user_upload.types.all_zip'
141
141
  ),
142
- value: 'application/zip,application/gzip,application/x-gzip,application/x-tar,application/rar,application/x-rar-compressed,application/x-7z-compressed,application/x-bzip2,application/x-xz',
142
+ value: 'application/zip,application/gzip,application/x-gzip,application/x-tar,application/rar,application/x-rar-compressed,application/x-7z-compressed,application/x-bzip2,application/x-xz,application/x-zip-compressed',
143
143
  },
144
144
  ]
145
145
  },
@@ -32,7 +32,7 @@
32
32
  $PermissionService.userHasAccessTo(
33
33
  'windward.global.file',
34
34
  'writable'
35
- )
35
+ ) && !isFromUrl
36
36
  "
37
37
  top
38
38
  color="primary"
@@ -63,7 +63,10 @@
63
63
  }}</span>
64
64
  </v-tooltip>
65
65
 
66
- <v-alert v-if="sourceInherit && !hasLinkedCaptions" type="warning">
66
+ <v-alert
67
+ v-if="sourceInherit && !hasLinkedCaptions && !isFromUrl"
68
+ type="warning"
69
+ >
67
70
  {{
68
71
  $t(
69
72
  'windward.core.components.settings.video.inherit_missing_captions'
@@ -180,6 +183,18 @@ export default {
180
183
  _.get(this.linkedCaptions, 'asset.public_url', '???')
181
184
  )
182
185
  },
186
+ isFromUrl() {
187
+ if (
188
+ this.source &&
189
+ _.isEmpty(this.source.id) &&
190
+ _.isEmpty(this.source.asset.id) &&
191
+ _.isEmpty(this.source.asset.name) &&
192
+ !_.isEmpty(this.source.asset.public_url)
193
+ ) {
194
+ return true
195
+ }
196
+ return false
197
+ },
183
198
  },
184
199
  watch: {},
185
200
  beforeMount() {},
@@ -197,7 +197,6 @@
197
197
  <template #activator="{ on, attrs }">
198
198
  <v-btn
199
199
  v-bind="attrs"
200
- v-on="on"
201
200
  text
202
201
  elevation="0"
203
202
  color="error"
@@ -205,6 +204,7 @@
205
204
  render ||
206
205
  block.metadata.config.playlist.length <= 1
207
206
  "
207
+ v-on="on"
208
208
  @click="onRemovePlaylistItem"
209
209
  >
210
210
  <v-icon>mdi-delete</v-icon>
@@ -334,16 +334,6 @@
334
334
  "
335
335
  :disabled="render"
336
336
  ></v-switch>
337
- <!--
338
- <v-switch
339
- v-model="block.metadata.config.attributes.playlistmenu"
340
- :label="$t('windward.core.components.settings.video.video.attributes.playlistmenu')"
341
- ></v-switch>
342
- <v-switch
343
- v-model="block.metadata.config.attributes.playlistautoadvance"
344
- :label="$t('windward.core.components.settings.video.video.attributes.playlistautoadvance')"
345
- ></v-switch>
346
- -->
347
337
  </v-col>
348
338
  <v-col cols="6">
349
339
  <v-switch
@@ -392,8 +382,8 @@
392
382
  import _ from 'lodash'
393
383
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
394
384
  import ContentBlockAsset from '~/components/Content/ContentBlockAsset.vue'
395
- import SourcePicker from './VideoSettings/SourcePicker.vue'
396
385
  import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
386
+ import SourcePicker from './VideoSettings/SourcePicker.vue'
397
387
 
398
388
  export default {
399
389
  name: 'VideoSettings',
@@ -602,7 +592,10 @@ export default {
602
592
  this.$set(
603
593
  this.block.metadata.config.playlist[this.playlistIndex].ads,
604
594
  adIndex,
605
- { sources: [], tracks: [] }
595
+ {
596
+ sources: [],
597
+ tracks: [],
598
+ }
606
599
  )
607
600
  }
608
601
 
@@ -44,7 +44,7 @@
44
44
  </div>
45
45
  </v-col>
46
46
  <v-col
47
- v-if="isFlashcardType || isMatchingGameType || isSortingGameType"
47
+ v-if="isFlashcardType || isMatchingGameType || isSortingGameType || isWordJumbleType"
48
48
  cols="auto"
49
49
  class="d-flex align-center"
50
50
  >
@@ -111,7 +111,7 @@ export default {
111
111
  'windward.core.components.content.blocks.generate_questions.blooms.none'
112
112
  ),
113
113
  },
114
- replaceExisting: this.questionType === 'bucket_game' ? true : this.replaceExistingMode,
114
+ replaceExisting: this.questionType === 'bucket_game' || this.questionType === 'word_jumble' ? true : this.replaceExistingMode,
115
115
  }
116
116
  },
117
117
  computed: {
@@ -125,11 +125,18 @@ export default {
125
125
  return this.questionType === 'bucket_game'
126
126
  },
127
127
  isMatchingGameType() {
128
+
128
129
  return this.questionType === 'matching_game'
129
130
  },
130
131
  isSortingGameType() {
131
132
  return this.questionType === 'sorting_game'
132
133
  },
134
+ isScenarioGameType() {
135
+ return this.questionType === 'scenario_game'
136
+ },
137
+ isWordJumbleType() {
138
+ return this.questionType === 'word_jumble'
139
+ },
133
140
  replaceExistingLabel() {
134
141
  if (this.isBucketGameType) {
135
142
  return this.$t(
@@ -147,6 +154,10 @@ export default {
147
154
  return this.$t(
148
155
  'windward.games.components.settings.sorting_game.form.replace_existing')
149
156
  }
157
+ if (this.isWordJumbleType) {
158
+ return this.$t(
159
+ 'windward.games.components.settings.word_jumble.form.replace_existing')
160
+ }
150
161
  return this.$t(
151
162
  'windward.games.components.settings.flashcard.form.replace_existing'
152
163
 
@@ -181,6 +192,66 @@ export default {
181
192
  return fullTree
182
193
  },
183
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
+
184
255
  // Basic Bloom's taxonomy levels available to all question types
185
256
  let basicBloomTaxonomy = [
186
257
  {
@@ -212,6 +283,7 @@ export default {
212
283
  // Only add higher-level Bloom's taxonomy for supported question types
213
284
  const supportsAdvancedTaxonomy =
214
285
  this.isSortingGameType ||
286
+ this.isScenarioGameType ||
215
287
  ([
216
288
  'multi_choice_single_answer',
217
289
  'multi_choice_multi_answer',
@@ -257,6 +329,14 @@ export default {
257
329
  return this.$t(
258
330
  'windward.core.components.content.blocks.generate_questions.button_label_sorting_game'
259
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
+ )
260
340
  } else {
261
341
  return this.$t(
262
342
  'windward.core.components.content.blocks.generate_questions.button_label'
@@ -466,6 +546,96 @@ export default {
466
546
  'Invalid response from sorting game generation'
467
547
  )
468
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
+ }
469
639
  } else {
470
640
  // ASSESSMENT QUESTION GENERATION
471
641
  const assessment = new Assessment({ id: this.block.id })
@@ -499,8 +669,29 @@ export default {
499
669
 
500
670
  let errorText = ''
501
671
 
502
- // Check for content mismatch error specifically for bucket games
503
- if (
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
504
695
  (errorMessage === 'activity.error.content_mismatch' ||
505
696
  errorType === 'content_mismatch') &&
506
697
  this.questionType === 'bucket_game'
@@ -533,11 +724,73 @@ export default {
533
724
  this.$t(
534
725
  `${basePath}.content_mismatch_sorting_game_support`
535
726
  )
536
- } else {
727
+ } else if (
728
+ (errorMessage === 'activity.error.content_mismatch' ||
729
+ errorType === 'content_mismatch') &&
730
+ this.questionType === 'scenario_game'
731
+ ) {
537
732
  errorText =
538
- this.$t(`${basePath}.${errorType}`) +
733
+ this.$t(`${basePath}.content_mismatch_scenario_game`) +
539
734
  '\n\n' +
540
- this.$t(`${basePath}.${errorType}_support`)
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
+ }
541
794
 
542
795
  if (errorType === 'technical') {
543
796
  const errorCode =
@@ -165,7 +165,7 @@ export default {
165
165
 
166
166
  computed: {
167
167
  hasActions() {
168
- if (this.allowRead && !render) {
168
+ if (this.allowRead && !this.render) {
169
169
  return true
170
170
  }
171
171
  // reach into parent text editor to see if a slot has been used.
@@ -7,6 +7,14 @@ export default {
7
7
  insufficient_content: 'More content needed to generate questions.',
8
8
  insufficient_content_support:
9
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
+ insufficient_content_blooms: 'Insufficient content to generate quality questions at the {bloomsLevel} level.',
12
+ insufficient_content_blooms_support:
13
+ 'Please add more detailed text, examples, or explanations to this section. We recommend at least {minimumRequired} words of relevant content to generate appropriate questions at this level.',
14
+
15
+ insufficient_content_dynamic: 'Insufficient content to generate quality questions.',
16
+ insufficient_content_dynamic_support:
17
+ 'Please add more detailed text, examples, or explanations to this section. We recommend at least {minimumRequired} words of relevant content to generate appropriate questions.',
10
18
 
11
19
  content_mismatch: "Content doesn't match question type.",
12
20
  content_mismatch_support:
@@ -24,6 +32,17 @@ export default {
24
32
  content_mismatch_sorting_game_support:
25
33
  "Consider adding content with sequential steps, chronological events, or items that have a clear logical order for students to arrange.",
26
34
 
35
+ content_mismatch_scenario_game: "Content not suitable for scenario games.",
36
+ content_mismatch_scenario_game_support:
37
+ "Consider adding content that describes concepts, processes, or principles that can be applied in real-world scenarios.",
38
+
39
+ content_mismatch_word_jumble: "Content not suitable for word jumble games.",
40
+ content_mismatch_word_jumble_support:
41
+ "Consider adding content with clear vocabulary terms, glossary words, or key concepts that can be unscrambled.",
42
+
43
+ character_limit: 'An error occurred while generating content. Try generating again.',
44
+ character_limit_detailed: 'The {field} exceeds the {limit} character limit (currently {actual} characters). Try generating again with shorter content.',
45
+
27
46
  llm_unavailable: 'Question generation temporarily unavailable.',
28
47
  llm_unavailable_support:
29
48
  "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.",
@@ -37,6 +56,8 @@ export default {
37
56
  button_label_bucket_game: 'Generate Buckets',
38
57
  button_label_matching_game: 'Generate Matches',
39
58
  button_label_sorting_game: 'Generate Items',
59
+ button_label_scenario_game: 'Generate Scenarios',
60
+ button_label_word_jumble: 'Generate Items',
40
61
  selected_pages: 'Selected Page',
41
62
  ai_assistance: 'AI Assistance',
42
63
  blooms: {
@@ -17,4 +17,6 @@ export default {
17
17
  number: 'Numbers',
18
18
  },
19
19
  add_choice: 'Add Choice',
20
+ generated_successfully: 'Scenarios generated successfully',
21
+ invalid_response: 'Invalid response from scenario generation',
20
22
  }
@@ -9,6 +9,14 @@ export default {
9
9
  'Se necesita más contenido para generar preguntas.',
10
10
  insufficient_content_support:
11
11
  'Agregue más texto, ejemplos o explicaciones a esta sección. Recomendamos al menos 2 o 3 párrafos de contenido.',
12
+
13
+ insufficient_content_blooms: 'Contenido insuficiente para generar preguntas de calidad en el nivel de {bloomsLevel}.',
14
+ insufficient_content_blooms_support:
15
+ 'Por favor, agregue texto más detallado, ejemplos o explicaciones a esta sección. Recomendamos al menos {minimumRequired} palabras de contenido relevante para generar preguntas apropiadas en este nivel.',
16
+
17
+ insufficient_content_dynamic: 'Contenido insuficiente para generar preguntas de calidad.',
18
+ insufficient_content_dynamic_support:
19
+ 'Por favor, agregue texto más detallado, ejemplos o explicaciones a esta sección. Recomendamos al menos {minimumRequired} palabras de contenido relevante para generar preguntas apropiadas.',
12
20
 
13
21
  content_mismatch: 'El contenido no coincide con el tipo de pregunta.',
14
22
  content_mismatch_support:
@@ -26,6 +34,17 @@ export default {
26
34
  content_mismatch_sorting_game_support:
27
35
  'Considera agregar contenido con pasos secuenciales, eventos cronológicos o elementos que tengan un orden lógico claro para que los estudiantes los organicen.',
28
36
 
37
+ content_mismatch_scenario_game: 'El contenido no es adecuado para juegos de escenarios.',
38
+ content_mismatch_scenario_game_support:
39
+ 'Considera agregar contenido que describa conceptos, procesos o principios que se puedan aplicar en escenarios del mundo real.',
40
+
41
+ content_mismatch_word_jumble: 'El contenido no es adecuado para juegos de revoltijo de palabras.',
42
+ content_mismatch_word_jumble_support:
43
+ 'Considera agregar contenido con términos de vocabulario claros, palabras de glosario o conceptos clave que puedan ser desordenados.',
44
+
45
+ character_limit: 'Se produjo un error al generar el contenido. Intenta generar de nuevo.',
46
+ character_limit_detailed: 'El campo {field} excede el límite de {limit} caracteres (actualmente {actual} caracteres). Intenta generar de nuevo con contenido más corto.',
47
+
29
48
  llm_unavailable:
30
49
  'La generación de preguntas no está disponible temporalmente.',
31
50
  llm_unavailable_support:
@@ -40,6 +59,8 @@ export default {
40
59
  button_label_bucket_game: 'Generar categorías',
41
60
  button_label_matching_game: 'Generar coincidencias',
42
61
  button_label_sorting_game: 'Generar elementos',
62
+ button_label_scenario_game: 'Generar escenarios',
63
+ button_label_word_jumble: 'Generar elementos',
43
64
  selected_pages: 'Página seleccionada',
44
65
  ai_assistance: 'Asistencia de IA',
45
66
  blooms: {
@@ -17,4 +17,6 @@ export default {
17
17
  number: 'Números',
18
18
  },
19
19
  add_choice: 'Agregar opción',
20
+ generated_successfully: 'Escenarios generados con éxito',
21
+ invalid_response: 'Respuesta inválida de la generación de escenarios',
20
22
  }
@@ -7,6 +7,14 @@ export default {
7
7
  insufficient_content: 'Mer innehåll behövs för att generera frågor.',
8
8
  insufficient_content_support:
9
9
  'Vänligen lägg till mer text, exempel eller förklaringar till det här avsnittet. Vi rekommenderar minst 2-3 stycken innehåll.',
10
+
11
+ insufficient_content_blooms: 'Otillräckligt innehåll för att generera kvalitetsfrågor på {bloomsLevel}-nivån.',
12
+ insufficient_content_blooms_support:
13
+ 'Vänligen lägg till mer detaljerad text, exempel eller förklaringar till detta avsnitt. Vi rekommenderar minst {minimumRequired} ord av relevant innehåll för att generera lämpliga frågor på denna nivå.',
14
+
15
+ insufficient_content_dynamic: 'Otillräckligt innehåll för att generera kvalitetsfrågor.',
16
+ insufficient_content_dynamic_support:
17
+ 'Vänligen lägg till mer detaljerad text, exempel eller förklaringar till detta avsnitt. Vi rekommenderar minst {minimumRequired} ord av relevant innehåll för att generera lämpliga frågor.',
10
18
 
11
19
  content_mismatch: 'Innehållet matchar inte frågetyp.',
12
20
  content_mismatch_support:
@@ -24,6 +32,17 @@ export default {
24
32
  content_mismatch_sorting_game_support:
25
33
  'Överväg att lägga till innehåll med sekventiella steg, kronologiska händelser eller objekt som har en tydlig logisk ordning för elever att arrangera.',
26
34
 
35
+ content_mismatch_scenario_game: 'Innehållet är inte lämpligt för scenariospel.',
36
+ content_mismatch_scenario_game_support:
37
+ 'Överväg att lägga till innehåll som beskriver koncept, processer eller principer som kan tillämpas i verkliga scenarier.',
38
+
39
+ content_mismatch_word_jumble: 'Innehållet är inte lämpligt för ordvirrvarr-spel.',
40
+ content_mismatch_word_jumble_support:
41
+ 'Överväg att lägga till innehåll med tydliga vokabulärtermer, ordlistor eller nyckelkoncept som kan blandas.',
42
+
43
+ character_limit: 'Ett fel uppstod när innehållet genererades. Försök generera igen.',
44
+ character_limit_detailed: 'Fältet {field} överskrider gränsen på {limit} tecken (för närvarande {actual} tecken). Försök generera igen med kortare innehåll.',
45
+
27
46
  llm_unavailable: 'Frågegenerering tillfälligt otillgänglig.',
28
47
  llm_unavailable_support:
29
48
  'Vi kan inte ansluta till vår AI-tjänst för tillfället. Försök igen om några minuter.',
@@ -37,6 +56,8 @@ export default {
37
56
  button_label_bucket_game: 'Generera kategorier',
38
57
  button_label_matching_game: 'Generera matchningar',
39
58
  button_label_sorting_game: 'Generera objekt',
59
+ button_label_scenario_game: 'Generera scenarier',
60
+ button_label_word_jumble: 'Generera objekt',
40
61
  selected_pages: 'Vald sida',
41
62
  ai_assistance: 'AI-hjälp',
42
63
  blooms: {
@@ -17,4 +17,6 @@ export default {
17
17
  number: 'Nummer',
18
18
  },
19
19
  add_choice: 'Lägg till val',
20
+ generated_successfully: 'Scenarier genererades framgångsrikt',
21
+ invalid_response: 'Ogiltigt svar från scenariogenereringen',
20
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/core",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Windward UI Core Plugins",
5
5
  "main": "plugin.js",
6
6
  "scripts": {