@windward/core 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## Release [0.23.0] - 2025-09-18
4
+
5
+ * Merged in feature/LE-2099-track-engagement-on-videos-from- (pull request #439)
6
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #438)
7
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #437)
8
+ * Merged in bugfix/LE-2088-open-response-block-remove-edito (pull request #434)
9
+ * Merged in bugfix/MIND-6075-decouple-generateaiquestionbut (pull request #432)
10
+ * Merged in bugfix/LE-2071-vimeo-videos-not-working (pull request #433)
11
+ * Merged in bugfix/LE-2052-clickable-icon-text-color-should (pull request #426)
12
+ * Merged in bugfix/LE-2088-open-response-block-remove-edito (pull request #430)
13
+ * Merged release/0.23.0 into bugfix/LE-2071-vimeo-videos-not-working
14
+ * Merged release/0.22.0 into bugfix/LE-2052-clickable-icon-text-color-should
15
+
16
+
17
+ ## Release [0.22.0] - 2025-08-28
18
+
19
+ * Merge branch 'master' into release/0.22.0
20
+ * Merged in bugfix/LE-2075-the-panel-of-the-text-block-isnt (pull request #427)
21
+ * Merged in bug-fix/LE-2060/llm-character-limit-validation (pull request #421)
22
+ * Merged in bugfix/LE-2031-open-response-download-collate-q (pull request #422)
23
+ * Merged in bugfix/LE-1960-video-using-urls-as-a-source (pull request #423)
24
+ * Merged in hotfix/0.21.1 (pull request #425)
25
+ * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
26
+ * Merged in bugfix/LE-2057-save-button-disappeared-again-on (pull request #419)
27
+ * Merged release/0.22.0 into bugfix/LE-1960-video-using-urls-as-a-source
28
+ * Merged in feature/LE-2036/word-jumble-gen (pull request #420)
29
+ * Merged in feature/LE-1997/scenario-gen (pull request #416)
30
+ * Merged in bugfix/LE-1928-user-upload-allowed-file-types (pull request #415)
31
+ * Merged in release/0.21.0 (pull request #401)
32
+ * Merged release/0.21.0 into bugfix/LE-1960-video-using-urls-as-a-source
33
+ * Merged release/0.21.0 into bugfix/LE-1928-user-upload-allowed-file-types
34
+
35
+
3
36
  ## Hotfix [0.21.1] created - 2025-08-20
4
37
 
5
38
 
@@ -54,10 +54,10 @@
54
54
  </v-avatar>
55
55
  <v-icon
56
56
  v-else-if="isIcon(item.icon)"
57
- class="clickable--icon black--text"
58
- >{{ item.icon }}</v-icon
59
- >
60
- <span v-else :class="iconClass + ' black--text'">{{
57
+ class="clickable--icon dark-text--text"
58
+ >{{ item.icon }}
59
+ </v-icon>
60
+ <span v-else :class="iconClass + ' dark-text--text'">{{
61
61
  decode(item.icon)
62
62
  }}</span>
63
63
  </button>
@@ -142,7 +142,7 @@ export default {
142
142
  ) &&
143
143
  this.itemColor(itemIndex)
144
144
  ) {
145
- classes += ' ' + this.itemColor(itemIndex) + ' black--text'
145
+ classes += ' ' + this.itemColor(itemIndex) + ' dark-text--text'
146
146
  }
147
147
  return classes
148
148
  }
@@ -276,6 +276,9 @@ button.button-icon.button-icon--outline {
276
276
  background-color: #fff !important;
277
277
  border-width: 4px;
278
278
  border-style: solid;
279
+ i.clickable--icon.dark-text--text {
280
+ color: var(--v-dark-text-base) !important;
281
+ }
279
282
  }
280
283
 
281
284
  button.button-icon.button-icon--rounded {
@@ -33,6 +33,7 @@
33
33
  v-model="response"
34
34
  :height="200"
35
35
  menubar=""
36
+ :toolbar="'styles | bold italic underline strikethrough removeformat | alignleft aligncenter alignright | table tablerowprops tablecellprops |bullist numlist outdent indent | a11yButton | undo redo'"
36
37
  ></TextEditor>
37
38
  <p class="pa-3 text-center">
38
39
  <v-btn
@@ -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)) {
@@ -428,6 +390,9 @@ export default {
428
390
  this.emitCompleted()
429
391
  }
430
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')
431
396
  },
432
397
  async onBeforeSave() {
433
398
  this.block.body = 'video'
@@ -130,6 +130,24 @@
130
130
  </v-row>
131
131
  </v-container>
132
132
 
133
+ <v-container class="pa-4 mb-6">
134
+ <v-row>
135
+ <v-col cols="12">
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>
147
+ </v-col>
148
+ </v-row>
149
+ </v-container>
150
+
133
151
  <DialogBox
134
152
  v-model="showLinkDialog"
135
153
  :trigger="false"
@@ -189,13 +207,14 @@
189
207
  </template>
190
208
  <script>
191
209
  import _ from 'lodash'
192
- import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
193
210
  import { mapGetters } from 'vuex'
211
+ import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
194
212
  import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
195
213
  import SortableExpansionPanel from '~/components/Core/SortableExpansionPanel.vue'
196
214
  import DialogBox from '~/components/Core/DialogBox.vue'
197
215
  import Uuid from '~/helpers/Uuid'
198
216
  import TextEditor from '~/components/Text/TextEditor'
217
+ import PluginRef from '~/components/Core/PluginRef.vue'
199
218
 
200
219
  export default {
201
220
  name: 'ScenarioChoiceSettings',
@@ -204,6 +223,7 @@ export default {
204
223
  TextEditor,
205
224
  DialogBox,
206
225
  BaseContentBlockSettings,
226
+ PluginRef,
207
227
  },
208
228
  extends: BaseContentSettings,
209
229
  data() {
@@ -240,6 +260,8 @@ export default {
240
260
  computed: {
241
261
  ...mapGetters({
242
262
  contentTree: 'content/getTree',
263
+ course: 'course/get',
264
+ currentContent: 'content/get',
243
265
  }),
244
266
  },
245
267
  beforeMount() {
@@ -351,6 +373,56 @@ export default {
351
373
  !_.isEmpty(this.block.metadata.config.link_content_id) &&
352
374
  !_.isEmpty(this.block.metadata.config.link_text)
353
375
  },
376
+ onPluginAppendBlock(activityData) {
377
+ // Process the activity data
378
+ if (
379
+ activityData &&
380
+ activityData.metadata &&
381
+ activityData.metadata.config &&
382
+ activityData.metadata.config.items &&
383
+ Array.isArray(activityData.metadata.config.items)
384
+ ) {
385
+ // Clear existing items and replace with generated ones
386
+ this.block.metadata.config.items.splice(
387
+ 0,
388
+ this.block.metadata.config.items.length
389
+ )
390
+
391
+ // Add all new choices
392
+ activityData.metadata.config.items.forEach((item) => {
393
+ this.block.metadata.config.items.push({
394
+ title: item.title || '',
395
+ body: item.body || '<p></p>',
396
+ correct: item.correct || false,
397
+ })
398
+ })
399
+
400
+ // Update title and instructions if provided
401
+ if (activityData.metadata.config.title) {
402
+ this.block.metadata.config.title =
403
+ activityData.metadata.config.title
404
+ }
405
+
406
+ if (activityData.metadata.config.instructions) {
407
+ this.block.metadata.config.instructions =
408
+ activityData.metadata.config.instructions
409
+ }
410
+
411
+ this.$toast.success(
412
+ this.$t(
413
+ 'windward.core.components.settings.scenario_choice.generated_successfully'
414
+ ),
415
+ { duration: 3000 }
416
+ )
417
+ } else {
418
+ this.$toast.error(
419
+ this.$t(
420
+ 'windward.core.components.settings.scenario_choice.invalid_response'
421
+ ),
422
+ { duration: 5000 }
423
+ )
424
+ }
425
+ },
354
426
  },
355
427
  }
356
428
  </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
  },
@@ -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"
@@ -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
 
@@ -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.23.0",
4
4
  "description": "Windward UI Core Plugins",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "homepage": "https://bitbucket.org/mindedge/windward-ui-plugin-core#readme",
23
23
  "dependencies": {
24
- "@mindedge/vuetify-player": "^0.4.9",
24
+ "@mindedge/vuetify-player": "^0.5.1",
25
25
  "@tinymce/tinymce-vue": "^3.2.8",
26
26
  "accessibility-scanner": "^0.0.1",
27
27
  "eslint": "^8.11.0",
package/plugin.js CHANGED
@@ -294,7 +294,7 @@ export default {
294
294
  },
295
295
  },
296
296
  ],
297
- settings: [
297
+ contentBlockSetting: [
298
298
  {
299
299
  tag: 'core-open-response-settings',
300
300
  template: OpenResponseSettings,
package/utils/index.js CHANGED
@@ -7,7 +7,6 @@ import TinyMCEWrapper from '../components/utils/TinyMCEWrapper.vue'
7
7
  import MathExpressionEditor from '../components/utils/MathExpressionEditor'
8
8
  import CourseGlossary from '../components/utils/glossary/CourseGlossary.vue'
9
9
  import CourseGlossaryForm from '../components/utils/glossary/CourseGlossaryForm.vue'
10
- import GenerateAIQuestionButton from '../components/utils/GenerateAIQuestionButton.vue'
11
10
 
12
11
  export {
13
12
  MathHelper,
@@ -17,5 +16,4 @@ export {
17
16
  TinyMCEWrapper,
18
17
  CourseGlossary,
19
18
  CourseGlossaryForm,
20
- GenerateAIQuestionButton,
21
19
  }
@@ -1,573 +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"
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' ? 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
- return this.questionType === 'matching_game'
129
- },
130
- isSortingGameType() {
131
- return this.questionType === 'sorting_game'
132
- },
133
- replaceExistingLabel() {
134
- if (this.isBucketGameType) {
135
- return this.$t(
136
- 'windward.games.components.settings.bucket_game.form.replace_existing'
137
- )
138
- return this.$t(
139
- 'windward.games.components.settings.bucket_game.form.replace_existing'
140
- )
141
- }
142
- if (this.isMatchingGameType) {
143
- return this.$t(
144
- 'windward.games.components.settings.matching_game.form.replace_existing')
145
- }
146
- if (this.isSortingGameType) {
147
- return this.$t(
148
- 'windward.games.components.settings.sorting_game.form.replace_existing')
149
- }
150
- return this.$t(
151
- 'windward.games.components.settings.flashcard.form.replace_existing'
152
-
153
- )
154
- },
155
- flattenedContent() {
156
- let cloneContentTree = _.cloneDeep(this.contentTree)
157
- const homepage = this.$ContentService.getHomepage()
158
- if (!_.isEmpty(homepage)) {
159
- cloneContentTree.unshift(homepage)
160
- }
161
- let fullTree = []
162
- // flatten content tree to get nested children pages
163
- cloneContentTree.forEach((content) => {
164
- fullTree.push(content)
165
- if (content.children.length > 0) {
166
- fullTree = fullTree.concat(_.flatten(content.children))
167
- // check if children have children
168
- content.children.forEach((child) => {
169
- fullTree = fullTree.concat(_.flatten(child.children))
170
- })
171
- }
172
- })
173
- //
174
- if (_.isEmpty(this.selectedContent)) {
175
- // returns array so hold here to pluck out below
176
- const currentPage = fullTree.filter(
177
- (contentPage) => contentPage.id === this.content.id
178
- )
179
- this.selectedContent = currentPage[0] ? currentPage[0] : ''
180
- }
181
- return fullTree
182
- },
183
- taxonomyLevels() {
184
- // Basic Bloom's taxonomy levels available to all question types
185
- let basicBloomTaxonomy = [
186
- {
187
- value: 'None',
188
- text: this.$t(
189
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
190
- ),
191
- },
192
- {
193
- value: 'Remember',
194
- text: this.$t(
195
- 'windward.core.components.content.blocks.generate_questions.blooms.remember'
196
- ),
197
- },
198
- {
199
- value: 'Understand',
200
- text: this.$t(
201
- 'windward.core.components.content.blocks.generate_questions.blooms.understand'
202
- ),
203
- },
204
- {
205
- value: 'Apply',
206
- text: this.$t(
207
- 'windward.core.components.content.blocks.generate_questions.blooms.apply'
208
- ),
209
- },
210
- ]
211
-
212
- // Only add higher-level Bloom's taxonomy for supported question types
213
- const supportsAdvancedTaxonomy =
214
- this.isSortingGameType ||
215
- ([
216
- 'multi_choice_single_answer',
217
- 'multi_choice_multi_answer',
218
- 'ordering'
219
- ].includes(this.questionType) &&
220
- !this.isFlashcardType &&
221
- !this.isBucketGameType &&
222
- !this.isMatchingGameType)
223
-
224
- if (supportsAdvancedTaxonomy) {
225
- const multiBlooms = [
226
- {
227
- value: 'Analyze',
228
- text: this.$t(
229
- 'windward.core.components.content.blocks.generate_questions.blooms.analyze'
230
- ),
231
- },
232
- {
233
- value: 'Evaluate',
234
- text: this.$t(
235
- 'windward.core.components.content.blocks.generate_questions.blooms.evaluate'
236
- ),
237
- },
238
- ]
239
- basicBloomTaxonomy = basicBloomTaxonomy.concat(multiBlooms)
240
- }
241
- return basicBloomTaxonomy
242
- },
243
- buttonLabel() {
244
- if (this.questionType === 'flashcard') {
245
- return this.$t(
246
- 'windward.core.components.content.blocks.generate_questions.button_label_flashcard'
247
- )
248
- } else if (this.questionType === 'bucket_game') {
249
- return this.$t(
250
- 'windward.core.components.content.blocks.generate_questions.button_label_bucket_game'
251
- )
252
- } else if (this.questionType === 'matching_game') {
253
- return this.$t(
254
- 'windward.core.components.content.blocks.generate_questions.button_label_matching_game'
255
- )
256
- } else if (this.questionType === 'sorting_game') {
257
- return this.$t(
258
- 'windward.core.components.content.blocks.generate_questions.button_label_sorting_game'
259
- )
260
- } else {
261
- return this.$t(
262
- 'windward.core.components.content.blocks.generate_questions.button_label'
263
- )
264
- }
265
- },
266
- },
267
- methods: {
268
- async generateAIQuestion() {
269
- this.isLoading = true
270
- try {
271
- let bloomsRequest = ''
272
- if (
273
- this.selectedDifficulty.text !==
274
- this.$t(
275
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
276
- )
277
- ) {
278
- bloomsRequest = `?blooms_level=${this.selectedDifficulty.value}`
279
- }
280
-
281
- const course = new Course(this.course)
282
- const contentData = this.selectedContent || this.content
283
- const content = new Content(contentData)
284
-
285
- let response
286
- if (this.questionType === 'flashcard') {
287
- // FLASHCARD GENERATION
288
- const activity = new Activity()
289
-
290
- const endpoint = `suggest/flashcard${bloomsRequest}`
291
-
292
- // Call the endpoint exactly like FlashCardSlidesManager does
293
- response = await Activity.custom(
294
- course,
295
- content,
296
- activity,
297
- endpoint
298
- ).get()
299
-
300
- let activityData = null
301
-
302
- if (response && response.activity) {
303
- activityData = response.activity
304
- } else if (
305
- response &&
306
- response.length > 0 &&
307
- response[0] &&
308
- response[0].activity
309
- ) {
310
- activityData = response[0].activity
311
- } else if (Array.isArray(response) && response.length > 0) {
312
- activityData = response[0]
313
- }
314
-
315
- if (
316
- activityData &&
317
- activityData.metadata &&
318
- activityData.metadata.config &&
319
- activityData.metadata.config.cards &&
320
- Array.isArray(activityData.metadata.config.cards)
321
- ) {
322
- // We pass the activity data and the replace flag to the parent component
323
- this.$emit(
324
- 'click:generate',
325
- activityData,
326
- this.replaceExisting
327
- )
328
- } else {
329
- throw new Error(
330
- 'Invalid response from flashcard generation'
331
- )
332
- }
333
- } else if (this.questionType === 'bucket_game') {
334
- // BUCKET GAME GENERATION
335
- const activity = new Activity()
336
-
337
- const endpoint = `suggest/bucket_game${bloomsRequest}`
338
-
339
- response = await Activity.custom(
340
- course,
341
- content,
342
- activity,
343
- endpoint
344
- ).get()
345
-
346
- let activityData = null
347
-
348
- if (response && response.activity) {
349
- activityData = response.activity
350
- } else if (
351
- response &&
352
- response.length > 0 &&
353
- response[0] &&
354
- response[0].activity
355
- ) {
356
- activityData = response[0].activity
357
- } else if (Array.isArray(response) && response.length > 0) {
358
- activityData = response[0]
359
- }
360
-
361
- if (
362
- activityData &&
363
- activityData.metadata &&
364
- activityData.metadata.config &&
365
- activityData.metadata.config.bucket_titles &&
366
- activityData.metadata.config.bucket_answers
367
- ) {
368
- // For bucket games, always use replace mode
369
- this.$emit(
370
- 'click:generate',
371
- activityData,
372
- true
373
- )
374
- } else {
375
- throw new Error(
376
- 'Invalid response from bucket game generation'
377
- )
378
- }
379
- } else if (this.questionType === 'matching_game') {
380
- // MATCHING GAME GENERATION
381
- const activity = new Activity()
382
-
383
- const endpoint = `suggest/matching_game${bloomsRequest}`;
384
-
385
- response = await Activity.custom(
386
- course,
387
- content,
388
- activity,
389
- endpoint
390
- ).get()
391
-
392
- let activityData = null
393
-
394
- if (response && response.activity) {
395
- activityData = response.activity
396
- } else if (
397
- response &&
398
- response.length > 0 &&
399
- response[0] &&
400
- response[0].activity
401
- ) {
402
- activityData = response[0].activity
403
- } else if (Array.isArray(response) && response.length > 0) {
404
- activityData = response[0]
405
- }
406
-
407
- if (activityData && activityData.metadata &&
408
- activityData.metadata.config &&
409
- activityData.metadata.config.answerObjects &&
410
- activityData.metadata.config.prompts
411
- ) {
412
- // We pass the activity data and the replace flag to the parent component
413
- this.$emit(
414
- 'click:generate',
415
- activityData,
416
- this.replaceExisting
417
- )
418
- } else {
419
- throw new Error(
420
- 'Invalid response from matching game generation'
421
- )
422
- }
423
- } else if (this.questionType === 'sorting_game') {
424
- // SORTING GAME GENERATION
425
- const activity = new Activity()
426
-
427
- const endpoint = `suggest/sorting_game${bloomsRequest}`;
428
-
429
- response = await Activity.custom(
430
- course,
431
- content,
432
- activity,
433
- endpoint
434
- ).get()
435
-
436
- let activityData = null
437
-
438
- if (response && response.activity) {
439
- activityData = response.activity
440
- } else if (
441
- response &&
442
- response.length > 0 &&
443
- response[0] &&
444
- response[0].activity
445
- ) {
446
- activityData = response[0].activity
447
- } else if (Array.isArray(response) && response.length > 0) {
448
- activityData = response[0]
449
- } else if (response) {
450
- activityData = response
451
- }
452
-
453
- if (activityData && activityData.metadata &&
454
- activityData.metadata.config &&
455
- activityData.metadata.config.answer &&
456
- Array.isArray(activityData.metadata.config.answer)
457
- ) {
458
- // We pass the activity data and the replace flag to the parent component
459
- this.$emit(
460
- 'click:generate',
461
- activityData,
462
- this.replaceExisting
463
- )
464
- } else {
465
- throw new Error(
466
- 'Invalid response from sorting game generation'
467
- )
468
- }
469
- } else {
470
- // ASSESSMENT QUESTION GENERATION
471
- const assessment = new Assessment({ id: this.block.id })
472
- const question = new AssessmentQuestion()
473
-
474
- response = await AssessmentQuestion.custom(
475
- course,
476
- content,
477
- assessment,
478
- question,
479
- `suggest/${this.questionType}${bloomsRequest}`
480
- ).get()
481
-
482
- if (response && response.length > 0) {
483
- const generatedQuestion = response[0]
484
- this.$emit('click:generate', generatedQuestion)
485
- } else {
486
- throw new Error(
487
- 'Invalid response from question generation'
488
- )
489
- }
490
- }
491
- } catch (error) {
492
- const errorMessage =
493
- error.response?.data?.error?.message ||
494
- error.message ||
495
- 'assessment.error.technical'
496
- const errorType = errorMessage.split('.').pop()
497
- const basePath =
498
- 'windward.core.components.content.blocks.generate_questions.error'
499
-
500
- let errorText = ''
501
-
502
- // Check for content mismatch error specifically for bucket games
503
- if (
504
- (errorMessage === 'activity.error.content_mismatch' ||
505
- errorType === 'content_mismatch') &&
506
- this.questionType === 'bucket_game'
507
- ) {
508
- errorText =
509
- this.$t(`${basePath}.content_mismatch_bucket_game`) +
510
- '\n\n' +
511
- this.$t(
512
- `${basePath}.content_mismatch_bucket_game_support`
513
- )
514
- } else if (
515
- (errorMessage === 'activity.error.content_mismatch' ||
516
- errorType === 'content_mismatch') &&
517
- this.questionType === 'matching_game'
518
- ) {
519
- errorText =
520
- this.$t(`${basePath}.content_mismatch_matching_game`) +
521
- '\n\n' +
522
- this.$t(
523
- `${basePath}.content_mismatch_matching_game_support`
524
- )
525
- } else if (
526
- (errorMessage === 'activity.error.content_mismatch' ||
527
- errorType === 'content_mismatch') &&
528
- this.questionType === 'sorting_game'
529
- ) {
530
- errorText =
531
- this.$t(`${basePath}.content_mismatch_sorting_game`) +
532
- '\n\n' +
533
- this.$t(
534
- `${basePath}.content_mismatch_sorting_game_support`
535
- )
536
- } else {
537
- errorText =
538
- this.$t(`${basePath}.${errorType}`) +
539
- '\n\n' +
540
- this.$t(`${basePath}.${errorType}_support`)
541
-
542
- if (errorType === 'technical') {
543
- const errorCode =
544
- error.response?.data?.error?.details?.error_type ||
545
- 'UNKNOWN'
546
- errorText = errorText.replace('[ERROR_CODE]', errorCode)
547
- }
548
- }
549
-
550
- this.$dialog.error(errorText, {
551
- duration: 5000,
552
- keepOnHover: true,
553
- singleton: true,
554
- type: 'error',
555
- })
556
- } finally {
557
- this.isLoading = false
558
- }
559
- },
560
- },
561
- }
562
- </script>
563
-
564
- <style scoped>
565
- .btn-selector {
566
- width: 100%;
567
- }
568
- .container-generate-ai {
569
- outline: 1px solid var(--v-secondary-base);
570
- border-radius: 15px;
571
- }
572
- </style>
573
-