@windward/games 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ### Release [0.17.0] created - 2025-05-13
4
+
5
+
6
+ ### Release [0.16.0] created - 2025-04-29
7
+
8
+
3
9
  ### Release [0.15.0] created - 2025-04-09
4
10
 
5
11
 
@@ -33,7 +33,15 @@
33
33
  )
34
34
  }}
35
35
  </p>
36
- <p>{{ error }}</p>
36
+ <p>
37
+ {{
38
+ render
39
+ ? $t(
40
+ 'windward.games.components.content.blocks.crossword.error.unable_to_load_student'
41
+ )
42
+ : error
43
+ }}
44
+ </p>
37
45
  </v-alert>
38
46
  <div
39
47
  v-if="!error"
@@ -284,7 +292,6 @@ export default {
284
292
  )
285
293
  }
286
294
  }
287
-
288
295
  // this is true if render is toggled back to true
289
296
  if (generate && this.error === '') {
290
297
  this.generateGrid(this.wordMap)
@@ -443,7 +450,7 @@ export default {
443
450
 
444
451
  if (grid.length > MATRIX_LENGTH) {
445
452
  this.error = this.$t(
446
- 'windward.games.components.content.blocks.crossword.error.could_not_generate'
453
+ 'windward.games.components.content.blocks.crossword.error.unable_to_load_editor'
447
454
  )
448
455
  return false
449
456
  }
@@ -8,7 +8,8 @@
8
8
  </v-row>
9
9
  <v-row
10
10
  v-if="
11
- settings.fileConfig.asset && settings.fileConfig.asset.file_asset_id
11
+ settings.fileConfig.asset &&
12
+ settings.fileConfig.asset.file_asset_id
12
13
  "
13
14
  >
14
15
  <v-col>
@@ -1,15 +1,17 @@
1
1
  <template>
2
2
  <v-container
3
+ :key="updateCardKey"
3
4
  style="height: 100%"
4
5
  class="div-container"
5
- :key="updateCardKey"
6
6
  >
7
7
  <v-card
8
- outlined
9
- @click="toggleCard"
8
+ ref="cardFront"
10
9
  v-show="side"
10
+ outlined
11
11
  :class="cardClass"
12
12
  style="height: 90%"
13
+ @keyup.enter="toggleCard"
14
+ @click="toggleCard"
13
15
  >
14
16
  <v-card-text class="fill-height">
15
17
  <CardFace
@@ -20,11 +22,13 @@
20
22
  </v-card-text>
21
23
  </v-card>
22
24
  <v-card
23
- outlined
24
- @click="toggleCard"
25
+ ref="cardBack"
25
26
  v-show="!side"
27
+ outlined
26
28
  :class="cardClass"
27
29
  style="height: 90%"
30
+ @keyup.enter="toggleCard"
31
+ @click="toggleCard"
28
32
  >
29
33
  <v-card-title class="card-title">
30
34
  <v-row align="center" justify="center">{{
@@ -118,6 +122,12 @@ export default {
118
122
  this.side = !this.side
119
123
  // update container with correct side
120
124
  this.updateCardKey = Crypto.id()
125
+ this.$nextTick(() => {
126
+ const target = this.side
127
+ ? this.$refs.cardFront
128
+ : this.$refs.cardBack
129
+ target.$el.focus()
130
+ })
121
131
  },
122
132
  },
123
133
  }
@@ -68,9 +68,13 @@
68
68
  outlined
69
69
  tile
70
70
  @click="onChooseAnswer(answer, question)"
71
+ @keyup.enter="onChooseAnswer(answer, question)"
71
72
  >
72
73
  <div class="d-flex justify-space-between">
73
- <p class="mb-0">
74
+ <p
75
+ class="mb-0"
76
+ :aria-labelledby="answer.value"
77
+ >
74
78
  {{
75
79
  getCharacter(question, answer) +
76
80
  '. ' +
@@ -79,6 +83,24 @@
79
83
  </p>
80
84
  <div>
81
85
  <v-icon
86
+ v-if="
87
+ handleInformationOutline(
88
+ question,
89
+ answer
90
+ ) !== undefined
91
+ "
92
+ :ref="
93
+ 'icon-information-' +
94
+ question.id +
95
+ '-' +
96
+ answer.id
97
+ "
98
+ tabindex="0"
99
+ :aria-label="
100
+ $t(
101
+ 'windward.games.components.content.blocks.multiple_choice.information'
102
+ )
103
+ "
82
104
  :class="
83
105
  handleInformationClass(
84
106
  question,
@@ -86,7 +108,10 @@
86
108
  )
87
109
  "
88
110
  @click="
89
- onAnswerDescription(question)
111
+ onAnswerDescription(
112
+ question,
113
+ answer
114
+ )
90
115
  "
91
116
  >{{
92
117
  handleInformationOutline(
@@ -105,6 +130,7 @@
105
130
  <v-col cols="12" md="12">
106
131
  <v-row class="d-flex justify-center">
107
132
  <v-btn
133
+ :ref="'btn-hint-' + question.id"
108
134
  color="primary"
109
135
  text
110
136
  elevation="0"
@@ -172,7 +198,6 @@
172
198
  :trigger="false"
173
199
  @click:outside="close"
174
200
  @click:close="close"
175
- @keydown.esc="close"
176
201
  >
177
202
  <template #title>
178
203
  <div v-if="hint">
@@ -240,14 +265,14 @@ export default {
240
265
  data() {
241
266
  return {
242
267
  updateKey: 0,
243
- completedItems: [],
244
268
  dialog: false,
245
269
  hint: false,
246
270
  hintText: '',
247
271
  answerDescriptionModal: false,
248
272
  answerDescription: '',
249
273
  studentResponses: [],
250
- mountCourseCounter: false,
274
+ activeQuestion: '',
275
+ activeAnswer: '',
251
276
  }
252
277
  },
253
278
  computed: {
@@ -257,8 +282,8 @@ export default {
257
282
  enrollment: 'enrollment/get',
258
283
  }),
259
284
  completedAmount() {
260
- if (this.completedItems) {
261
- return _.flatten(this.completedItems).length
285
+ if (this.studentResponses) {
286
+ return _.flatten(this.studentResponses).length
262
287
  }
263
288
  },
264
289
  totalAmountQuestions() {
@@ -269,7 +294,7 @@ export default {
269
294
  completedPercent() {
270
295
  if (
271
296
  this.block.metadata.config.questions.length > 0 &&
272
- this.completedItems.length > 0
297
+ this.studentResponses.length > 0
273
298
  ) {
274
299
  return (this.completedAmount / this.totalAmountQuestions) * 100
275
300
  }
@@ -277,41 +302,42 @@ export default {
277
302
  },
278
303
  },
279
304
  methods: {
280
- onAnswerDescription(question) {
305
+ onAnswerDescription(question, answer) {
306
+ this.activeQuestion = question
307
+ this.activeAnswer = answer
281
308
  //launches modal and displays anwer description
282
309
  this.dialog = true
283
310
  this.answerDescriptionModal = true
284
311
  this.answerDescription = question.answer_description
285
312
  },
286
313
  onChooseAnswer(answer, question) {
287
- let studRep
288
- this.studentResponses.find((obj) => {
289
- if (obj.id === question.id) {
290
- studRep = obj
291
- }
292
- })
293
- if (_.isEmpty(this.studentResponses) || _.isEmpty(studRep)) {
294
- // lets html side know whish answer was chosen, this is important in determine css
295
- answer.chosen = true
296
- // clones question block into the correct index in the studentResponses araay
314
+ // check to see if student already answered this question
315
+ let studentResponse = this.studentResponses.find(
316
+ (obj) => obj.id === question.id
317
+ )
318
+ // if there is no student response for this question or there no student responses to any of the questions
319
+ if (
320
+ _.isEmpty(this.studentResponses) ||
321
+ _.isEmpty(studentResponse)
322
+ ) {
323
+ // get question and answer index to update the answer directly
324
+ // on block.metadata.config.questions to prevent buggy behavior
325
+ const questionIndex =
326
+ this.block.metadata.config.questions.findIndex(
327
+ (q) => q.id === question.id
328
+ )
329
+ const answerIndex = this.block.metadata.config.questions[
330
+ questionIndex
331
+ ].answer_options.findIndex((a) => a.id === answer.id)
332
+ // lets html side know which answer was chosen, this is important in determine css
333
+ this.block.metadata.config.questions[
334
+ questionIndex
335
+ ].answer_options[answerIndex].chosen = true
336
+ // clones question block to break reference
297
337
  const clonedQuestion = _.cloneDeep(question)
298
338
  clonedQuestion.student_response = answer
299
339
 
300
- if (answer.correctAnswer === true) {
301
- clonedQuestion.isStudentCorrect = true
302
- } else {
303
- clonedQuestion.isStudentCorrect = false
304
- }
305
-
306
340
  this.studentResponses.push(clonedQuestion)
307
- // ensure the object is not already in the completed items array if not push into the array
308
- const questionDone = this.completedItems.find(
309
- (item) => item === question
310
- )
311
-
312
- if (!questionDone) {
313
- this.completedItems.push(question)
314
- }
315
341
 
316
342
  this.updateKey = Crypto.id()
317
343
  }
@@ -365,6 +391,7 @@ export default {
365
391
  }
366
392
  }
367
393
  )
394
+ // check if student already responded
368
395
  if (studentsQuestionResponse) {
369
396
  // requirements for correct and chosen
370
397
  if (
@@ -380,7 +407,12 @@ export default {
380
407
  return 'mdi-check-circle-outline'
381
408
  }
382
409
  // requirements for wrong and chosen
383
- if (answer.correctAnswer !== true && answer.chosen === true) {
410
+ if (
411
+ answer.id ===
412
+ studentsQuestionResponse.student_response.id &&
413
+ answer.correctAnswer !== true &&
414
+ answer.chosen === true
415
+ ) {
384
416
  return 'mdi-close-circle'
385
417
  }
386
418
  }
@@ -461,12 +493,12 @@ export default {
461
493
  return letters[answerIndex]
462
494
  },
463
495
  onHint(question) {
496
+ this.activeQuestion = question
464
497
  // launches dialog modal
465
498
  this.dialog = true
466
499
  this.hint = true
467
- // sets text for modal, cannot just reference question.hint in html because
468
- // the carousel loads all elements regardless of if they are on a different slide or not
469
- // when mountCourseCounter so it always shows the last question if referenced question.hint
500
+ // sets text for modal, cannot just reference question.hint in html because the carousel loads
501
+ // all elements regardless of if they are on a different slide so it always shows the last question
470
502
  this.hintText = question.hint
471
503
  },
472
504
  onFifty(question) {
@@ -505,11 +537,28 @@ export default {
505
537
  return array
506
538
  },
507
539
  close() {
540
+ if (this.answerDescriptionModal) {
541
+ this.answerDescriptionModal = false
542
+ this.answerDescription = ''
543
+ const iconButton =
544
+ this.$refs[
545
+ 'icon-information-' +
546
+ this.activeQuestion.id +
547
+ '-' +
548
+ this.activeAnswer.id
549
+ ][0].$el
550
+ iconButton.focus()
551
+ this.activeAnswer = ''
552
+ this.activeQuestion = ''
553
+ } else if (this.hint) {
554
+ this.hint = false
555
+ this.hintText = ''
556
+ const hintButton =
557
+ this.$refs['btn-hint-' + this.activeQuestion.id][0].$el
558
+ hintButton.focus()
559
+ this.activeQuestion = ''
560
+ }
508
561
  this.dialog = false
509
- this.hint = false
510
- this.answerDescriptionModal = false
511
- this.hintText = ''
512
- this.answerDescription = ''
513
562
  },
514
563
  },
515
564
  }
@@ -277,15 +277,31 @@
277
277
  indeterminate
278
278
  ></v-progress-circular>
279
279
  </div>
280
+ <v-container class="pa-4 mb-6">
281
+ <v-row>
282
+ <v-col cols="12">
283
+ <GenerateAIQuestionButton
284
+ :course="course"
285
+ :content="content"
286
+ :block="block"
287
+ question-type="flashcard"
288
+ :replace-existing-mode="replaceExisting"
289
+ @click:generate="onGeneratedFlashcards"
290
+ ></GenerateAIQuestionButton>
291
+ </v-col>
292
+ </v-row>
293
+ </v-container>
280
294
  </div>
281
295
  </template>
282
296
 
283
297
  <script>
284
298
  import _ from 'lodash'
299
+ import { mapGetters } from 'vuex'
285
300
  import {
286
301
  MathExpressionEditor,
287
302
  MathLiveWrapper,
288
303
  ContentViewer,
304
+ GenerateAIQuestionButton
289
305
  } from '@windward/core/utils'
290
306
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
291
307
  import ContentBlockAsset from '~/components/Content/ContentBlockAsset.vue'
@@ -305,11 +321,13 @@ export default {
305
321
  TextEditor,
306
322
  SortableExpansionPanel,
307
323
  ImageAssetSettings,
324
+ GenerateAIQuestionButton,
308
325
  },
309
326
  data() {
310
327
  return {
311
328
  loading: false,
312
329
  valid: true,
330
+ replaceExisting: false,
313
331
  }
314
332
  },
315
333
  beforeMount() {
@@ -342,6 +360,10 @@ export default {
342
360
  },
343
361
 
344
362
  computed: {
363
+ ...mapGetters({
364
+ course: 'course/get',
365
+ content: 'content/get',
366
+ }),
345
367
  defaultCard() {
346
368
  return {
347
369
  front: {
@@ -440,6 +462,72 @@ export default {
440
462
  }
441
463
  return htmlString.replace(/(<([^>]+)>)/gi, '')
442
464
  },
465
+ // Handler for receiving flashcards from GenerateAIQuestionButton
466
+ onGeneratedFlashcards(activityData, replaceCards) {
467
+ this.loading = true
468
+ try {
469
+ // Now process the activity data
470
+ if (activityData && activityData.metadata &&
471
+ activityData.metadata.config &&
472
+ activityData.metadata.config.cards &&
473
+ Array.isArray(activityData.metadata.config.cards)) {
474
+
475
+ // Save new cards
476
+ const newCards = activityData.metadata.config.cards.map(card => ({
477
+ front: { ...card.front },
478
+ back: { ...card.back },
479
+ side: card.side !== undefined ? card.side : true,
480
+ expand: false
481
+ }))
482
+
483
+ if (replaceCards) {
484
+ // Replace mode: Clear existing cards
485
+ this.block.metadata.config.cards.splice(0, this.block.metadata.config.cards.length)
486
+
487
+ // Add all new cards
488
+ newCards.forEach(card => {
489
+ this.block.metadata.config.cards.push(card)
490
+ })
491
+ } else {
492
+ // Merge mode: Add new cards to existing ones
493
+ newCards.forEach(card => {
494
+ this.block.metadata.config.cards.push(card)
495
+ })
496
+ }
497
+
498
+ // Update title and instructions if provided and we're in replace mode
499
+ if (replaceCards) {
500
+ if (activityData.metadata.config.title) {
501
+ this.block.metadata.config.title = activityData.metadata.config.title
502
+ }
503
+
504
+ if (activityData.metadata.config.instructions) {
505
+ this.block.metadata.config.instructions = activityData.metadata.config.instructions
506
+ }
507
+ }
508
+
509
+ this.$toast.success(
510
+ replaceCards
511
+ ? this.$t('windward.games.components.settings.flashcard.form.replaced_successfully')
512
+ : this.$t('windward.games.components.settings.flashcard.form.added_successfully'),
513
+ { duration: 3000 }
514
+ )
515
+ } else {
516
+ this.$toast.error(this.$t('windward.games.components.settings.flashcard.form.invalid_response'), {
517
+ duration: 5000
518
+ })
519
+ }
520
+ } catch (error) {
521
+ // Extract error message from the response
522
+ const errorMessage = error.message || 'Unknown error occurred'
523
+ this.$toast.error(`${this.$t('windward.games.components.settings.flashcard.form.failed_to_process')}: ${errorMessage}`, {
524
+ duration: 5000
525
+ })
526
+ } finally {
527
+ this.loading = false
528
+ }
529
+ }
443
530
  },
444
531
  }
445
532
  </script>
533
+
@@ -14,6 +14,10 @@ export default {
14
14
  could_not_generate:
15
15
  'Could not generate a {0} by {0} crossword with the supplied words',
16
16
  unknown: 'An unknown error occurred during the crossword creation',
17
+ unable_to_load_editor:
18
+ 'Unable to generate crossword puzzle. Your current word list may have too many words, words that are too short/long, or insufficient shared letters for crossings. You can edit your word list and try again.',
19
+ unable_to_load_student:
20
+ 'Unable to generate crossword puzzle with these words. Please try refreshing the page until it loads successfully or contact support if the issue persists.',
17
21
  },
18
22
 
19
23
  play_again: 'Randomize Crossword and Play Again',
@@ -5,4 +5,5 @@ export default {
5
5
  hint_title: 'Hint',
6
6
  answer_feedback: 'Answer Feedback',
7
7
  total_points: 'Total Points for Course',
8
+ information: 'Answer Information',
8
9
  }
@@ -26,5 +26,10 @@ export default {
26
26
  'The character count for this field must be less than',
27
27
  },
28
28
  instructions:"Click on each card to reveal the term's definition. Use the arrows to move through the deck.",
29
+ replace_existing: 'Replace existing flashcards',
30
+ replaced_successfully: 'Flashcards replaced successfully',
31
+ added_successfully: 'New flashcards added successfully',
32
+ invalid_response: 'Invalid response from flashcard generation',
33
+ failed_to_process: 'Failed to process generated flashcards'
29
34
  },
30
35
  }
@@ -17,6 +17,10 @@ export default {
17
17
  'No se pudo generar un crucigrama {0} por {0} con las palabras proporcionadas',
18
18
  unknown:
19
19
  'Se produjo un error desconocido durante la creación del crucigrama',
20
+ unable_to_load_editor:
21
+ 'No se puede generar el crucigrama. Tu lista de palabras actual puede tener demasiadas palabras, palabras demasiado cortas o largas, o suficientes letras compartidas para los cruces. Puedes editar tu lista de palabras e intentarlo de nuevo.',
22
+ unable_to_load_student:
23
+ 'No se puede generar un crucigrama con estas palabras. Actualice la página hasta que se cargue correctamente o contacte con el soporte si el problema persiste.',
20
24
  },
21
25
 
22
26
  play_again: 'Aleatorizar crucigramas y jugar de nuevo',
@@ -5,4 +5,5 @@ export default {
5
5
  flip_card: 'Haga clic para ver atras',
6
6
  click_to_show_front: 'Haga clic para mostrar el frente',
7
7
  click_to_show_back: 'Haga clic para mostrar Atrás',
8
+ replace_existing: 'Reemplazar tarjetas existentes'
8
9
  }
@@ -5,4 +5,5 @@ export default {
5
5
  hint_title: 'Pista',
6
6
  answer_feedback: 'Comentarios de respuesta',
7
7
  total_points: 'Puntos totales por Curso',
8
+ information: 'Información de respuesta',
8
9
  }
@@ -26,5 +26,10 @@ export default {
26
26
  'El número de caracteres para este campo debe ser menor que',
27
27
  },
28
28
  instructions:"Haga clic en cada tarjeta para revelar la definición del término. Usa las flechas para moverte por el mazo.",
29
+ replace_existing: 'Reemplazar tarjetas existentes',
30
+ replaced_successfully: 'Tarjetas reemplazadas con éxito',
31
+ added_successfully: 'Nuevas tarjetas agregadas con éxito',
32
+ invalid_response: 'Respuesta inválida de la generación de tarjetas',
33
+ failed_to_process: 'Error al procesar las tarjetas generadas'
29
34
  },
30
35
  }
@@ -12,6 +12,10 @@ export default {
12
12
  could_not_generate:
13
13
  'Det gick inte att skapa ett {0} gånger {0} korsord med de angivna orden',
14
14
  unknown: 'Ett okänt fel inträffade under korsordsskapandet',
15
+ unable_to_load_editor:
16
+ 'Det går inte att skapa korsord. Din nuvarande ordlista kan ha för många ord, ord som är för korta/långa eller otillräckliga delade bokstäver för korsningar. Du kan redigera din ordlista och försöka igen.',
17
+ unable_to_load_student:
18
+ 'Det går inte att skapa korsord med dessa ord. Försök att uppdatera sidan tills den läses in eller kontakta supporten om problemet kvarstår.',
15
19
  },
16
20
 
17
21
  play_again: 'Randomisera korsord och spela igen',
@@ -5,4 +5,5 @@ export default {
5
5
  flip_card: 'Klicka för att vända kortet',
6
6
  click_to_show_front: 'Klicka för att visa fronten',
7
7
  click_to_show_back: 'Klicka för att visa tillbaka',
8
+ replace_existing: 'Ersätt befintliga flashcards'
8
9
  }
@@ -5,4 +5,5 @@ export default {
5
5
  hint_title: 'Tips',
6
6
  answer_feedback: 'Svara feedback',
7
7
  total_points: 'Totala poäng för Kurs',
8
+ information: 'Svarsinformation',
8
9
  }
@@ -26,5 +26,10 @@ export default {
26
26
  'Teckenantalet för detta fält måste vara mindre än',
27
27
  },
28
28
  instructions:"Klicka på varje kort för att avslöja termens definition. Använd pilarna för att flytta genom däcket.",
29
+ replace_existing: 'Ersätt befintliga flashcards',
30
+ replaced_successfully: 'Flashkort har ersatts framgångsrikt',
31
+ added_successfully: 'Nya flashkort har lagts till',
32
+ invalid_response: 'Ogiltigt svar från genereringen av flashkort',
33
+ failed_to_process: 'Kunde inte bearbeta genererade flashkort'
29
34
  },
30
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/games",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Windward UI Plugin Games",
5
5
  "main": "plugin.js",
6
6
  "scripts": {