@windward/core 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,20 +1,48 @@
1
1
  # Changelog
2
2
 
3
- ## Release [0.22.0] - 2025-08-25
3
+ ## Release [0.24.0] - 2025-10-06
4
4
 
5
+ * Merged in bugfix/LE-2134-email-block-show-hide-title (pull request #444)
6
+ * Merged in feature/LE-2097-tabs-and-accordions-hide-backgro (pull request #436)
7
+ * Merged release/0.24.0 into feature/LE-2097-tabs-and-accordions-hide-backgro
8
+ * Merged in bugfix/LE-1841-all-blocks-save-or-cancel-change (pull request #440)
9
+ * Merged release/0.24.0 into bugfix/LE-1841-all-blocks-save-or-cancel-change
10
+ * Merged in feature/LE-2100-edit-in-text-area-update-text-fi (pull request #435)
11
+
12
+
13
+ ## Release [0.23.0] - 2025-09-18
14
+
15
+ * Merged in feature/LE-2099-track-engagement-on-videos-from- (pull request #439)
16
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #438)
17
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #437)
18
+ * Merged in bugfix/LE-2088-open-response-block-remove-edito (pull request #434)
19
+ * Merged in bugfix/MIND-6075-decouple-generateaiquestionbut (pull request #432)
20
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #433)
21
+ * Merged in bugfix/LE-2052-clickable-icon-text-color-should (pull request #426)
22
+ * Merged in bugfix/LE-2088-open-response-block-remove-edito (pull request #430)
23
+ * Merged release/0.23.0 into bugfix/LE-2071-vimeo-videos-not-working
24
+ * Merged release/0.22.0 into bugfix/LE-2052-clickable-icon-text-color-should
25
+
26
+
27
+ ## Release [0.22.0] - 2025-08-28
28
+
29
+ * Merge branch 'master' into release/0.22.0
5
30
  * Merged in bugfix/LE-2075-the-panel-of-the-text-block-isnt (pull request #427)
6
31
  * Merged in bug-fix/LE-2060/llm-character-limit-validation (pull request #421)
7
32
  * Merged in bugfix/LE-2031-open-response-download-collate-q (pull request #422)
8
33
  * Merged in bugfix/LE-1960-video-using-urls-as-a-source (pull request #423)
34
+ * Merged in hotfix/0.21.1 (pull request #425)
9
35
  * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
10
36
  * Merged in bugfix/LE-2057-save-button-disappeared-again-on (pull request #419)
11
37
  * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
12
38
  * Merged in feature/LE-2036/word-jumble-gen (pull request #420)
13
39
  * Merged in feature/LE-1997/scenario-gen (pull request #416)
14
40
  * Merged in bugfix/LE-1928-user-upload-allowed-file-types (pull request #415)
41
+ * Merged in release/0.21.0 (pull request #401)
15
42
  * Merged release/0.21.0 into bugfix/LE-1960-video-using-urls-as-a-source
16
43
  * Merged release/0.21.0 into bugfix/LE-1928-user-upload-allowed-file-types
17
44
 
45
+
18
46
  ## Hotfix [0.21.1] created - 2025-08-20
19
47
 
20
48
 
@@ -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 {
@@ -7,7 +7,13 @@
7
7
  "
8
8
  >
9
9
  <v-col cols="12" class="pa-0">
10
- <h2 v-if="block.metadata.config.title" tabindex="0">
10
+ <h2
11
+ v-if="
12
+ block.metadata.config.title &&
13
+ block.metadata.config.display_title
14
+ "
15
+ tabindex="0"
16
+ >
11
17
  {{ block.metadata.config.title }}
12
18
  </h2>
13
19
  <p
@@ -141,7 +147,10 @@
141
147
  )
142
148
  }}: {{ item.cc }}
143
149
  </div>
144
- <div v-if="item.subject" class="div-details-subject">
150
+ <div
151
+ v-if="item.subject"
152
+ class="div-details-subject"
153
+ >
145
154
  {{
146
155
  $t(
147
156
  'windward.core.components.content.blocks.email.subject'
@@ -317,7 +326,7 @@ export default {
317
326
  methods: {
318
327
  onRemoveTags(body) {
319
328
  if (typeof body !== 'string') {
320
- return ''
329
+ return ''
321
330
  }
322
331
  let text = body.replace(/&nbsp;/g, ' ')
323
332
  text = text.replace(/\u00A0/g, ' ')
@@ -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'
@@ -96,7 +96,6 @@
96
96
  "
97
97
  :assets.sync="block.assets"
98
98
  :disabled="render"
99
- hide-background
100
99
  hide-decorative
101
100
  hide-modal
102
101
  show-spacing
@@ -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
  }
@@ -77,7 +77,6 @@
77
77
  "
78
78
  :assets.sync="block.assets"
79
79
  :disabled="render"
80
- hide-background
81
80
  hide-decorative
82
81
  hide-modal
83
82
  show-spacing
@@ -5,6 +5,7 @@
5
5
  autofill
6
6
  :disabled="render"
7
7
  allow-read
8
+ :key="updateKey"
8
9
  root-block="p"
9
10
  show-glossary
10
11
  :hide-text-editor="hideTextEditor"
@@ -52,6 +53,7 @@
52
53
  </template>
53
54
 
54
55
  <script>
56
+ import Crypto from '~/helpers/Crypto'
55
57
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
56
58
  import TextEditor from '~/components/Text/TextEditor'
57
59
  export default {
@@ -73,6 +75,7 @@ export default {
73
75
  elevation: 0,
74
76
  },
75
77
  hideTextEditor: false,
78
+ updateKey: Crypto.id(),
76
79
  }
77
80
  },
78
81
  mounted() {
@@ -82,6 +85,7 @@ export default {
82
85
  onExpand() {
83
86
  this.hideTextEditor = !this.hideTextEditor
84
87
  this.block.metadata.config.expand = this.hideTextEditor
88
+ this.updateKey = Crypto.id()
85
89
  },
86
90
  },
87
91
  }
@@ -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.24.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
@@ -12,8 +12,6 @@ import Email from './components/Content/Blocks/Email'
12
12
  import BlockQuote from './components/Content/Blocks/BlockQuote.vue'
13
13
  import HorizontalRule from './components/Content/Blocks/HorizontalRule.vue'
14
14
 
15
- import UserUploadNav from './components/Navigation/Items/UserUploadNav.vue'
16
-
17
15
  import OpenResponse from './components/Content/Blocks/OpenResponse'
18
16
  import OpenResponseCollate from './components/Content/Blocks/OpenResponseCollate'
19
17
  import Image from './components/Content/Blocks/Image'
@@ -24,7 +22,6 @@ import GlossaryPage from './pages/glossary.vue'
24
22
  import TinymcePlugin from './pages/plugins/tinymce/_plugin.vue'
25
23
 
26
24
  import CourseGlossaryToolNav from './components/Navigation/Items/CourseGlossaryToolNav.vue'
27
- import GlossaryNav from './components/Navigation/Items/GlossaryNav.vue'
28
25
  import AskTheExpert from './components/Navigation/Items/AskTheExpert.vue'
29
26
 
30
27
  // Entrypoint for npm
@@ -121,7 +118,9 @@ export default {
121
118
  menu: [
122
119
  {
123
120
  tag: 'core-user-upload-nav',
124
- template: UserUploadNav,
121
+ path: '/course/{course.id}/section/{section.id}/user-uploads',
122
+ icon: 'mdi-cloud-upload',
123
+ i18n: 'windward.core.components.navigation.user_upload.title',
125
124
  context: ['course'],
126
125
  display: ['menu'],
127
126
  permissions: {
@@ -132,7 +131,9 @@ export default {
132
131
  },
133
132
  {
134
133
  tag: 'core-user-glossary-nav',
135
- template: GlossaryNav,
134
+ path: '/course/{course.id}/section/{section.id}/glossary',
135
+ icon: 'mdi-comment-text-multiple',
136
+ i18n: 'windward.core.shared.menu.course_glossary',
136
137
  context: ['course'],
137
138
  display: ['menu'],
138
139
  permissions: {
@@ -294,7 +295,7 @@ export default {
294
295
  },
295
296
  },
296
297
  ],
297
- settings: [
298
+ contentBlockSetting: [
298
299
  {
299
300
  tag: 'core-open-response-settings',
300
301
  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,53 +0,0 @@
1
- <template>
2
- <v-tooltip right>
3
- <template #activator="{ on, attrs }">
4
- <v-list-item
5
- :class="color"
6
- :to="
7
- '/course/' +
8
- course.id +
9
- '/section/' +
10
- enrollment.course_section_id +
11
- '/glossary'
12
- "
13
- v-bind="attrs"
14
- v-on="on"
15
- >
16
- <v-list-item-action>
17
- <v-icon v-bind="attrs" v-on="on"
18
- >mdi-comment-text-multiple</v-icon
19
- >
20
- </v-list-item-action>
21
- <v-list-item-content>
22
- <v-list-item-title
23
- >{{ $t('windward.core.shared.menu.course_glossary') }}
24
- </v-list-item-title>
25
- </v-list-item-content>
26
- </v-list-item>
27
- </template>
28
- <span>{{ $t('windward.core.shared.menu.course_glossary') }}</span>
29
- </v-tooltip>
30
- </template>
31
-
32
- <script>
33
- import { mapGetters } from 'vuex'
34
-
35
- export default {
36
- name: 'GlossaryNav',
37
- middleware: ['auth'],
38
- props: {
39
- color: { type: String, required: false, default: '' },
40
- },
41
- data() {
42
- return {}
43
- },
44
- computed: {
45
- ...mapGetters({
46
- course: 'course/get',
47
- enrollment: 'enrollment/get',
48
- }),
49
- },
50
- }
51
- </script>
52
-
53
- <style scoped></style>
@@ -1,58 +0,0 @@
1
- <template>
2
- <v-tooltip right>
3
- <template #activator="{ on, attrs }">
4
- <v-list-item
5
- :class="color"
6
- :to="
7
- '/course/' +
8
- course.id +
9
- '/section/' +
10
- enrollment.course_section_id +
11
- '/user-uploads'
12
- "
13
- v-bind="attrs"
14
- v-on="on"
15
- >
16
- <v-list-item-action>
17
- <v-icon>mdi-cloud-upload</v-icon>
18
- </v-list-item-action>
19
- <v-list-item-content>
20
- <v-list-item-title
21
- >{{
22
- $t(
23
- 'windward.core.components.navigation.user_upload.title'
24
- )
25
- }}
26
- </v-list-item-title>
27
- </v-list-item-content>
28
- </v-list-item>
29
- </template>
30
- <span>{{
31
- $t('windward.core.components.navigation.user_upload.title')
32
- }}</span>
33
- </v-tooltip>
34
- </template>
35
-
36
- <script>
37
- import { mapGetters } from 'vuex'
38
-
39
- export default {
40
- components: {},
41
- middleware: ['auth'],
42
- props: {
43
- color: { type: String, required: false, default: '' },
44
- },
45
- data() {
46
- return {}
47
- },
48
- computed: {
49
- ...mapGetters({
50
- course: 'course/get',
51
- enrollment: 'enrollment/get',
52
- }),
53
- },
54
- created() {},
55
- mounted() {},
56
- methods: {},
57
- }
58
- </script>
@@ -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
-