@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 +33 -0
- package/components/Content/Blocks/ClickableIcons.vue +8 -5
- package/components/Content/Blocks/OpenResponse.vue +1 -0
- package/components/Content/Blocks/OpenResponseCollate.vue +27 -4
- package/components/Content/Blocks/Video.vue +58 -93
- package/components/Settings/ScenarioChoiceSettings.vue +73 -1
- package/components/Settings/UserUploadSettings.vue +1 -1
- package/components/Settings/VideoSettings/SourcePicker.vue +18 -3
- package/components/Settings/VideoSettings.vue +6 -13
- package/components/utils/TinyMCEWrapper.vue +1 -1
- package/i18n/en-US/components/content/blocks/generate_questions.ts +21 -0
- package/i18n/en-US/components/settings/scenario_choice.ts +2 -0
- package/i18n/es-ES/components/content/blocks/generate_questions.ts +21 -0
- package/i18n/es-ES/components/settings/scenario_choice.ts +2 -0
- package/i18n/sv-SE/components/content/blocks/generate_questions.ts +21 -0
- package/i18n/sv-SE/components/settings/scenario_choice.ts +2 -0
- package/package.json +2 -2
- package/plugin.js +1 -1
- package/utils/index.js +0 -2
- package/components/utils/GenerateAIQuestionButton.vue +0 -573
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
|
|
58
|
-
>{{ item.icon }}
|
|
59
|
-
>
|
|
60
|
-
<span v-else :class="iconClass + '
|
|
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) + '
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
70
|
+
'windward.core.components.content.blocks.video.edit_prompt'
|
|
65
71
|
)
|
|
66
|
-
}}
|
|
67
|
-
</v-card
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
{
|
|
595
|
+
{
|
|
596
|
+
sources: [],
|
|
597
|
+
tracks: [],
|
|
598
|
+
}
|
|
606
599
|
)
|
|
607
600
|
}
|
|
608
601
|
|
|
@@ -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: {
|
|
@@ -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: {
|
|
@@ -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: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windward/core",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
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
|
-
|