@windward/games 0.22.0 → 0.24.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## Release [0.24.0] - 2025-10-23
4
+
5
+ * Merged in feature/LE-2034/generate-crossword (pull request #262)
6
+ * Merged in bugfix/LE-2020-the-quiz-show-manager-panel-does (pull request #263)
7
+
8
+
9
+ ## Release [0.23.0] - 2025-09-16
10
+
11
+ * Merged in feature/LE-2035/seven-strikes (pull request #259)
12
+ * Merged in feature/LE-1998/MC-games (pull request #258)
13
+ * Merged in feature/LE-1853-multiple-choice-block-optional-h (pull request #257)
14
+ * Merged in bugfix/MIND-6075-decouple-generateaiquestionbut (pull request #254)
15
+ * Merged in feature/LE-2091-multiple-choice-block-add-space- (pull request #253)
16
+ * Merged release/0.22.0 into feature/LE-2091-multiple-choice-block-add-space-
17
+
18
+
3
19
  ## Release [0.22.0] - 2025-08-28
4
20
 
5
21
  * Merged in feature/LE-2036/update-core-package-v (pull request #252)
@@ -81,7 +81,7 @@
81
81
  <div class="d-flex justify-space-between">
82
82
  <div class="d-flex">
83
83
  <p
84
- class="mb-0"
84
+ class="mb-0 mr-1"
85
85
  :aria-labelledby="answer.value"
86
86
  >
87
87
  {{
@@ -147,6 +147,7 @@
147
147
  text
148
148
  elevation="0"
149
149
  class="mr-4 btn-hint"
150
+ :disabled="hasHint(question)"
150
151
  @click="onHint(question)"
151
152
  >{{
152
153
  $t(
@@ -318,6 +319,12 @@ export default {
318
319
  },
319
320
  },
320
321
  methods: {
322
+ hasHint(question) {
323
+ if (_.isEmpty(question.hint)) {
324
+ return true
325
+ }
326
+ return false
327
+ },
321
328
  onAnswerDescription(question, answer) {
322
329
  this.activeQuestion = question
323
330
  this.activeAnswer = answer
@@ -18,15 +18,6 @@
18
18
  @input="onCheckValidation"
19
19
  ></TextEditor>
20
20
  </v-col>
21
- <v-col xl="3" lg="3" md="4" sm="12" cols="12" v-if="false">
22
- <GenerateAIQuestionButton
23
- :course="course"
24
- :content="content"
25
- :block="block"
26
- :question-type="questionType"
27
- @click:generate="onQuestionGenerated"
28
- ></GenerateAIQuestionButton>
29
- </v-col>
30
21
  <v-col cols="12">
31
22
  <ImageAssetSettings
32
23
  v-model="question.question_metadata.body_asset"
@@ -31,10 +31,10 @@ export default {
31
31
  icons: {
32
32
  type: Object,
33
33
  required: false,
34
- default: {
34
+ default: () => ({
35
35
  success: 'mdi-check-circle-outline',
36
36
  error: 'mdi-close-circle-outline',
37
- },
37
+ }),
38
38
  },
39
39
  },
40
40
  }
@@ -196,14 +196,18 @@
196
196
  <v-container class="pa-4 mb-6">
197
197
  <v-row>
198
198
  <v-col cols="12">
199
- <GenerateAIQuestionButton
200
- :course="course"
201
- :content="currentContent"
202
- :block="block"
203
- question-type="bucket_game"
204
- :replace-existing-mode="replaceExisting"
205
- @click:generate="onGeneratedBucketGame"
206
- ></GenerateAIQuestionButton>
199
+ <PluginRef
200
+ target="contentBlockSettingTool"
201
+ :attrs="{
202
+ value: block,
203
+ course: course,
204
+ content: currentContent,
205
+ }"
206
+ :on="{
207
+ input: onPluginSetBlock,
208
+ append: onPluginAppendBlock,
209
+ }"
210
+ ></PluginRef>
207
211
  </v-col>
208
212
  </v-row>
209
213
  </v-container>
@@ -217,7 +221,6 @@ import {
217
221
  MathExpressionEditor,
218
222
  MathLiveWrapper,
219
223
  ContentViewer,
220
- GenerateAIQuestionButton
221
224
  } from '@windward/core/utils'
222
225
  import colors from 'vuetify/lib/util/colors'
223
226
  import TextEditor from '~/components/Text/TextEditor'
@@ -225,6 +228,7 @@ import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentB
225
228
  import Crypto from '~/helpers/Crypto'
226
229
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
227
230
  import SortableExpansionPanel from '~/components/Core/SortableExpansionPanel.vue'
231
+ import PluginRef from '~/components/Core/PluginRef.vue'
228
232
  import Uuid from '~/helpers/Uuid'
229
233
 
230
234
  export default {
@@ -237,9 +241,9 @@ export default {
237
241
  SortableExpansionPanel,
238
242
  BaseContentBlockSettings,
239
243
  TextEditor,
240
- GenerateAIQuestionButton,
244
+ PluginRef,
241
245
  },
242
- beforeMount() {
246
+ beforeMount() {
243
247
  if (_.isEmpty(this.block)) {
244
248
  this.block = {}
245
249
  }
@@ -424,125 +428,161 @@ export default {
424
428
  return htmlString.replace(/(<([^>]+)>)/gi, '')
425
429
  }
426
430
  },
427
- // Handler for receiving bucket game data from GenerateAIQuestionButton
428
- onGeneratedBucketGame(activityData, replaceMode) {
431
+ // Handler for receiving bucket game data from Plugins
432
+ onPluginSetBlock(activityData) {
429
433
  this.loading = true
430
434
  try {
431
435
  // Process the activity data
432
- if (activityData && activityData.metadata &&
436
+ if (
437
+ activityData &&
438
+ activityData.metadata &&
433
439
  activityData.metadata.config &&
434
440
  activityData.metadata.config.bucket_titles &&
435
441
  activityData.metadata.config.bucket_answers &&
436
442
  Array.isArray(activityData.metadata.config.bucket_titles) &&
437
- Array.isArray(activityData.metadata.config.bucket_answers)) {
438
-
443
+ Array.isArray(activityData.metadata.config.bucket_answers)
444
+ ) {
439
445
  // Save new buckets and answers
440
- const newBuckets = activityData.metadata.config.bucket_titles
441
- const newAnswers = activityData.metadata.config.bucket_answers
446
+ const newBuckets =
447
+ activityData.metadata.config.bucket_titles
448
+ const newAnswers =
449
+ activityData.metadata.config.bucket_answers
442
450
 
443
- if (replaceMode) {
444
- // Replace mode: Clear existing buckets and answers
445
- this.block.metadata.config.bucket_titles.splice(0, this.block.metadata.config.bucket_titles.length)
446
- this.block.metadata.config.bucket_answers.splice(0, this.block.metadata.config.bucket_answers.length)
451
+ // Replace mode: Clear existing buckets and answers
452
+ this.block.metadata.config.bucket_titles.splice(
453
+ 0,
454
+ this.block.metadata.config.bucket_titles.length
455
+ )
456
+ this.block.metadata.config.bucket_answers.splice(
457
+ 0,
458
+ this.block.metadata.config.bucket_answers.length
459
+ )
447
460
 
448
- // Add all new buckets
449
- newBuckets.forEach(bucket => {
450
- this.block.metadata.config.bucket_titles.push({
451
- title: bucket.title || '',
452
- color: bucket.color || colors.blueGrey.lighten5,
453
- expand: false
454
- })
461
+ // Add all new buckets
462
+ newBuckets.forEach((bucket) => {
463
+ this.block.metadata.config.bucket_titles.push({
464
+ title: bucket.title || '',
465
+ color: bucket.color || colors.blueGrey.lighten5,
466
+ expand: false,
455
467
  })
468
+ })
456
469
 
457
- // Add all new answers
458
- newAnswers.forEach((bucketAnswers, index) => {
459
- this.block.metadata.config.bucket_answers[index] = []
460
- if (Array.isArray(bucketAnswers)) {
461
- bucketAnswers.forEach(answer => {
462
- this.block.metadata.config.bucket_answers[index].push({
463
- bucket_index: index,
464
- display: answer.display || '',
465
- feedback: answer.feedback || '',
466
- expand: false,
467
- id: Crypto.id()
468
- })
470
+ // Add all new answers
471
+ newAnswers.forEach((bucketAnswers, index) => {
472
+ this.block.metadata.config.bucket_answers[index] = []
473
+ if (Array.isArray(bucketAnswers)) {
474
+ bucketAnswers.forEach((answer) => {
475
+ this.block.metadata.config.bucket_answers[
476
+ index
477
+ ].push({
478
+ bucket_index: index,
479
+ display: answer.display || '',
480
+ feedback: answer.feedback || '',
481
+ expand: false,
482
+ id: Crypto.id(),
469
483
  })
470
- }
471
- })
472
- } else {
473
- // Merge mode: Add new buckets and answers to existing ones
474
- const existingBucketCount = this.block.metadata.config.bucket_titles.length
475
-
476
- // Add new buckets
477
- newBuckets.forEach(bucket => {
478
- this.block.metadata.config.bucket_titles.push({
479
- title: bucket.title || '',
480
- color: bucket.color || colors.blueGrey.lighten5,
481
- expand: false
482
484
  })
483
- })
485
+ }
486
+ })
484
487
 
485
- // Add new answers with adjusted bucket indices
486
- newAnswers.forEach((bucketAnswers, index) => {
487
- const adjustedIndex = existingBucketCount + index
488
- if (!this.block.metadata.config.bucket_answers[adjustedIndex]) {
489
- this.block.metadata.config.bucket_answers[adjustedIndex] = []
490
- }
491
- if (Array.isArray(bucketAnswers)) {
492
- bucketAnswers.forEach(answer => {
493
- this.block.metadata.config.bucket_answers[adjustedIndex].push({
494
- bucket_index: adjustedIndex,
495
- display: answer.display || '',
496
- feedback: answer.feedback || '',
497
- expand: false,
498
- id: Crypto.id()
499
- })
500
- })
501
- }
502
- })
488
+ // Update title and instructions if provided and we're in replace mode
489
+ if (activityData.metadata.config.title) {
490
+ this.block.metadata.config.title =
491
+ activityData.metadata.config.title
503
492
  }
504
493
 
505
- // Update title and instructions if provided and we're in replace mode
506
- if (replaceMode) {
507
- if (activityData.metadata.config.title) {
508
- this.block.metadata.config.title = activityData.metadata.config.title
509
- }
494
+ if (activityData.metadata.config.instructions) {
495
+ this.block.metadata.config.instructions =
496
+ activityData.metadata.config.instructions
497
+ }
510
498
 
511
- if (activityData.metadata.config.instructions) {
512
- this.block.metadata.config.instructions = activityData.metadata.config.instructions
513
- }
499
+ // Update feedback messages if provided
500
+ if (activityData.metadata.config.feedback_correct) {
501
+ this.block.metadata.config.feedback_correct =
502
+ activityData.metadata.config.feedback_correct
503
+ }
514
504
 
515
- // Update feedback messages if provided
516
- if (activityData.metadata.config.feedback_correct) {
517
- this.block.metadata.config.feedback_correct = activityData.metadata.config.feedback_correct
518
- }
505
+ if (activityData.metadata.config.feedback_incorrect) {
506
+ this.block.metadata.config.feedback_incorrect =
507
+ activityData.metadata.config.feedback_incorrect
508
+ }
519
509
 
520
- if (activityData.metadata.config.feedback_incorrect) {
521
- this.block.metadata.config.feedback_incorrect = activityData.metadata.config.feedback_incorrect
510
+ this.$toast.success(
511
+ this.$t(
512
+ 'windward.games.components.settings.bucket_game.form.replaced_successfully'
513
+ ),
514
+ { duration: 3000 }
515
+ )
516
+ } else {
517
+ this.$toast.error(
518
+ this.$t(
519
+ 'windward.games.components.settings.bucket_game.form.invalid_response'
520
+ ),
521
+ {
522
+ duration: 5000,
522
523
  }
524
+ )
525
+ }
526
+ } catch (error) {
527
+ // Extract error message from the response
528
+ const errorMessage = error.message || 'Unknown error occurred'
529
+ this.$toast.error(
530
+ `${this.$t(
531
+ 'windward.games.components.settings.bucket_game.form.failed_to_process'
532
+ )}: ${errorMessage}`,
533
+ {
534
+ duration: 5000,
523
535
  }
536
+ )
537
+ } finally {
538
+ this.loading = false
539
+ }
540
+ },
524
541
 
542
+ onPluginAppendBlock(activityData) {
543
+ this.loading = true
544
+ try {
545
+ // Process the activity data
546
+ if (
547
+ activityData &&
548
+ activityData.metadata &&
549
+ activityData.metadata.config &&
550
+ activityData.metadata.config.bucket_titles &&
551
+ activityData.metadata.config.bucket_answers &&
552
+ Array.isArray(activityData.metadata.config.bucket_titles) &&
553
+ Array.isArray(activityData.metadata.config.bucket_answers)
554
+ ) {
525
555
  this.$toast.success(
526
- replaceMode
527
- ? this.$t('windward.games.components.settings.bucket_game.form.replaced_successfully')
528
- : this.$t('windward.games.components.settings.bucket_game.form.added_successfully'),
556
+ this.$t(
557
+ 'windward.games.components.settings.bucket_game.form.added_successfully'
558
+ ),
529
559
  { duration: 3000 }
530
560
  )
531
561
  } else {
532
- this.$toast.error(this.$t('windward.games.components.settings.bucket_game.form.invalid_response'), {
533
- duration: 5000
534
- })
562
+ this.$toast.error(
563
+ this.$t(
564
+ 'windward.games.components.settings.bucket_game.form.invalid_response'
565
+ ),
566
+ {
567
+ duration: 5000,
568
+ }
569
+ )
535
570
  }
536
571
  } catch (error) {
537
572
  // Extract error message from the response
538
573
  const errorMessage = error.message || 'Unknown error occurred'
539
- this.$toast.error(`${this.$t('windward.games.components.settings.bucket_game.form.failed_to_process')}: ${errorMessage}`, {
540
- duration: 5000
541
- })
574
+ this.$toast.error(
575
+ `${this.$t(
576
+ 'windward.games.components.settings.bucket_game.form.failed_to_process'
577
+ )}: ${errorMessage}`,
578
+ {
579
+ duration: 5000,
580
+ }
581
+ )
542
582
  } finally {
543
583
  this.loading = false
544
584
  }
545
- }
585
+ },
546
586
  },
547
587
  }
548
588
  </script>
@@ -90,6 +90,24 @@
90
90
  </v-row>
91
91
  </v-container>
92
92
  </v-container>
93
+ <v-container class="pa-4 mb-6">
94
+ <v-row>
95
+ <v-col cols="12">
96
+ <PluginRef
97
+ target="contentBlockSettingTool"
98
+ :attrs="{
99
+ value: block,
100
+ course: course,
101
+ content: currentContent,
102
+ }"
103
+ :on="{
104
+ input: onPluginSetBlock,
105
+ append: onPluginAppendBlock,
106
+ }"
107
+ ></PluginRef>
108
+ </v-col>
109
+ </v-row>
110
+ </v-container>
93
111
  <div v-if="loading" class="text-center">
94
112
  <v-progress-circular
95
113
  :size="70"
@@ -103,15 +121,17 @@
103
121
 
104
122
  <script>
105
123
  import _ from 'lodash'
124
+ import { mapGetters } from 'vuex'
106
125
  import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
107
126
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
108
127
  import SortableExpansionPanel from '~/components/Core/SortableExpansionPanel.vue'
128
+ import PluginRef from '~/components/Core/PluginRef.vue'
109
129
  import Uuid from '~/helpers/Uuid'
110
130
 
111
131
  export default {
112
132
  name: 'CrosswordPuzzleSettingsManager',
113
133
  extends: BaseContentSettings,
114
- components: { SortableExpansionPanel, BaseContentBlockSettings },
134
+ components: { SortableExpansionPanel, BaseContentBlockSettings, PluginRef },
115
135
  beforeMount() {
116
136
  if (_.isEmpty(this.block)) {
117
137
  this.block = {}
@@ -186,6 +206,12 @@ export default {
186
206
  },
187
207
  }
188
208
  },
209
+ computed: {
210
+ ...mapGetters({
211
+ course: 'course/get',
212
+ currentContent: 'content/get',
213
+ }),
214
+ },
189
215
  methods: {
190
216
  onAddElement() {
191
217
  // pushes in new crossword object
@@ -207,6 +233,227 @@ export default {
207
233
  this.onAddElement()
208
234
  }
209
235
  },
236
+ onPluginSetBlock(activityData) {
237
+ this.loading = true
238
+ try {
239
+ const result = this.processGeneratedCrossword(activityData)
240
+ if (!result) {
241
+ this.showInvalidResponseToast()
242
+ return
243
+ }
244
+
245
+ this.$set(
246
+ this.block.metadata,
247
+ 'config',
248
+ this.block.metadata.config || {}
249
+ )
250
+
251
+ this.$set(
252
+ this.block.metadata.config,
253
+ 'words',
254
+ result.words.map((item, index) => ({
255
+ id: index,
256
+ word: item.word,
257
+ clue: item.clue,
258
+ expand: false,
259
+ }))
260
+ )
261
+
262
+ if (result.title) {
263
+ this.block.metadata.config.title = result.title
264
+ }
265
+
266
+ this.$toast.success(
267
+ this.$t(
268
+ 'windward.games.components.settings.crossword.replaced_successfully'
269
+ ),
270
+ { duration: 3000 }
271
+ )
272
+ } catch (error) {
273
+ console.error('Failed to process generated crossword items', error)
274
+ this.$toast.error(
275
+ this.$t(
276
+ 'windward.games.components.settings.crossword.failed_to_process'
277
+ ),
278
+ { duration: 5000 }
279
+ )
280
+ } finally {
281
+ this.loading = false
282
+ }
283
+ },
284
+ onPluginAppendBlock(activityData) {
285
+ this.loading = true
286
+ try {
287
+ const result = this.processGeneratedCrossword(activityData)
288
+ if (!result) {
289
+ this.showInvalidResponseToast()
290
+ return
291
+ }
292
+
293
+ if (!Array.isArray(this.block.metadata.config.words)) {
294
+ this.$set(this.block.metadata.config, 'words', [])
295
+ }
296
+
297
+ const startingIndex = this.block.metadata.config.words.length
298
+
299
+ result.words.forEach((item, index) => {
300
+ this.block.metadata.config.words.push({
301
+ id: startingIndex + index,
302
+ word: item.word,
303
+ clue: item.clue,
304
+ expand: false,
305
+ })
306
+ })
307
+
308
+ this.renumberWordIds()
309
+
310
+ if (result.title) {
311
+ const defaultTitle = this.$t(
312
+ 'windward.games.components.content.blocks.crossword.crossword'
313
+ )
314
+ if (
315
+ !this.block.metadata.config.title ||
316
+ this.block.metadata.config.title === defaultTitle
317
+ ) {
318
+ this.block.metadata.config.title = result.title
319
+ }
320
+ }
321
+
322
+ this.$toast.success(
323
+ this.$t(
324
+ 'windward.games.components.settings.crossword.added_successfully'
325
+ ),
326
+ { duration: 3000 }
327
+ )
328
+ } catch (error) {
329
+ console.error('Failed to append generated crossword items', error)
330
+ this.$toast.error(
331
+ this.$t(
332
+ 'windward.games.components.settings.crossword.failed_to_process'
333
+ ),
334
+ { duration: 5000 }
335
+ )
336
+ } finally {
337
+ this.loading = false
338
+ }
339
+ },
340
+ processGeneratedCrossword(activityData) {
341
+ const config = _.get(activityData, 'metadata.config', null)
342
+ const words = _.get(config, 'words', [])
343
+
344
+ if (!Array.isArray(words) || words.length === 0) {
345
+ return null
346
+ }
347
+
348
+ const sanitizedWords = words
349
+ .map((item) => {
350
+ const sanitizedWord = this.normalizeWord(item?.word)
351
+ const sanitizedClue = this.normalizeClue(item?.clue)
352
+
353
+ if (!sanitizedWord || !sanitizedClue) {
354
+ return null
355
+ }
356
+
357
+ return {
358
+ word: sanitizedWord,
359
+ clue: sanitizedClue,
360
+ expand: false,
361
+ }
362
+ })
363
+ .filter((item) => item !== null)
364
+
365
+ if (sanitizedWords.length < 2) {
366
+ return null
367
+ }
368
+
369
+ const limitedWords = sanitizedWords.slice(0, 10)
370
+
371
+ const normalizedTitle = this.normalizeTitle(_.get(config, 'title', ''))
372
+
373
+ return {
374
+ title: normalizedTitle,
375
+ words: limitedWords,
376
+ }
377
+ },
378
+ normalizeWord(word) {
379
+ if (!word) {
380
+ return ''
381
+ }
382
+
383
+ const formatted = word
384
+ .toString()
385
+ .normalize('NFKD')
386
+ .replace(/[^A-Za-z]/g, '')
387
+ .toUpperCase()
388
+ .slice(0, 20)
389
+
390
+ return formatted
391
+ },
392
+ normalizeClue(clue) {
393
+ if (!clue) {
394
+ return ''
395
+ }
396
+
397
+ let normalized = clue
398
+ .toString()
399
+ .replace(/\s+/g, ' ')
400
+ .trim()
401
+
402
+ if (!normalized) {
403
+ return ''
404
+ }
405
+
406
+ if (normalized.endsWith('?')) {
407
+ normalized = normalized.slice(0, -1).trim()
408
+ }
409
+
410
+ const maxLength = 155
411
+ if (normalized.length > maxLength) {
412
+ normalized = normalized.slice(0, maxLength).trim()
413
+ }
414
+
415
+ if (normalized && !/[.!]$/.test(normalized)) {
416
+ if (normalized.length >= maxLength) {
417
+ normalized = normalized.slice(0, maxLength - 1).trim()
418
+ }
419
+ normalized += '.'
420
+ }
421
+
422
+ return normalized
423
+ },
424
+ normalizeTitle(title) {
425
+ if (!title) {
426
+ return ''
427
+ }
428
+
429
+ let normalized = title
430
+ .toString()
431
+ .replace(/\s+/g, ' ')
432
+ .trim()
433
+
434
+ if (!normalized) {
435
+ return ''
436
+ }
437
+
438
+ if (normalized.length > 100) {
439
+ normalized = normalized.slice(0, 100).trim()
440
+ }
441
+
442
+ return normalized
443
+ },
444
+ renumberWordIds() {
445
+ this.block.metadata.config.words.forEach((item, index) => {
446
+ item.id = index
447
+ })
448
+ },
449
+ showInvalidResponseToast() {
450
+ this.$toast.error(
451
+ this.$t(
452
+ 'windward.games.components.settings.crossword.invalid_response'
453
+ ),
454
+ { duration: 5000 }
455
+ )
456
+ },
210
457
  },
211
458
  }
212
459
  </script>