@windward/integrations 0.19.1 → 0.20.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 +8 -0
- package/components/Content/Blocks/ActionPanel/TransformBlock.vue +349 -0
- package/components/LLM/GenerateContent/AssessmentQuestionGenerateButton.vue +180 -9
- package/components/LLM/GenerateContent/BlockQuestionGenerateButton.vue +35 -13
- package/i18n/en-US/components/content/blocks/action_panel/index.ts +5 -0
- package/i18n/en-US/components/content/blocks/action_panel/transform_block.ts +7 -0
- package/i18n/en-US/components/content/blocks/index.ts +2 -0
- package/i18n/en-US/components/llm/generate_content/generate_questions.ts +4 -0
- package/i18n/en-US/components/llm/generate_content/index.ts +1 -0
- package/i18n/en-US/shared/settings.ts +1 -2
- package/i18n/es-ES/components/content/blocks/action_panel/index.ts +5 -0
- package/i18n/es-ES/components/content/blocks/action_panel/transform_block.ts +7 -0
- package/i18n/es-ES/components/content/blocks/index.ts +2 -0
- package/i18n/es-ES/components/llm/generate_content/generate_questions.ts +5 -0
- package/i18n/es-ES/components/llm/generate_content/index.ts +1 -0
- package/i18n/es-ES/shared/settings.ts +1 -2
- package/i18n/sv-SE/components/content/blocks/action_panel/index.ts +5 -0
- package/i18n/sv-SE/components/content/blocks/action_panel/transform_block.ts +7 -0
- package/i18n/sv-SE/components/content/blocks/index.ts +2 -0
- package/i18n/sv-SE/components/llm/generate_content/generate_questions.ts +5 -0
- package/i18n/sv-SE/components/llm/generate_content/index.ts +1 -0
- package/i18n/sv-SE/shared/settings.ts +1 -2
- package/package.json +1 -1
- package/plugin.js +16 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Release [0.20.0] - 2025-12-03
|
|
4
|
+
|
|
5
|
+
* Merged in feature/LE-2177-switch-block-type-for-text-heavy (pull request #118)
|
|
6
|
+
* Merged in feature/LE-2156-block-settings-label (pull request #115)
|
|
7
|
+
* Merged in bugfix/LE-1992-the-ui-of-the-ai-assistance-is-b (pull request #116)
|
|
8
|
+
* Merged in LE-2149/assess-inputs-rag (pull request #117)
|
|
9
|
+
|
|
10
|
+
|
|
3
11
|
## Hotfix [0.19.1] - 2025-11-07
|
|
4
12
|
|
|
5
13
|
* Merged in bugfix/LE-2197-org-admin---import-from-resource (pull request #112)
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-list-item-group v-if="canTransformBlock" :disabled="isTransforming">
|
|
3
|
+
<v-list-group v-model="showMenu" @click="onClickShowTransforms">
|
|
4
|
+
<template #activator>
|
|
5
|
+
<v-list-item-icon>
|
|
6
|
+
<v-progress-circular
|
|
7
|
+
v-if="isTransforming"
|
|
8
|
+
indeterminate
|
|
9
|
+
></v-progress-circular>
|
|
10
|
+
<v-icon v-if="!isTransforming">mdi-shape</v-icon>
|
|
11
|
+
</v-list-item-icon>
|
|
12
|
+
<v-list-item-title>
|
|
13
|
+
{{
|
|
14
|
+
$t(
|
|
15
|
+
'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type'
|
|
16
|
+
)
|
|
17
|
+
}}
|
|
18
|
+
</v-list-item-title>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<v-list-item
|
|
22
|
+
v-for="action in actions"
|
|
23
|
+
:key="action.tag"
|
|
24
|
+
:disabled="isTransforming"
|
|
25
|
+
@click="performBlockTransform($event, action.tag)"
|
|
26
|
+
>
|
|
27
|
+
<v-list-item-title>
|
|
28
|
+
<v-icon>mdi-arrow-right</v-icon> {{ action.label }}
|
|
29
|
+
</v-list-item-title>
|
|
30
|
+
</v-list-item>
|
|
31
|
+
</v-list-group>
|
|
32
|
+
</v-list-item-group>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import _ from 'lodash'
|
|
37
|
+
import { mapGetters } from 'vuex'
|
|
38
|
+
import Organization from '~/models/Organization'
|
|
39
|
+
import Course from '~/models/Course'
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
props: {
|
|
43
|
+
// The block associated with this action panel
|
|
44
|
+
value: { type: Object, required: false, default: null },
|
|
45
|
+
|
|
46
|
+
// The blocks position relative to other blocks (Potentially not the same as value.order)
|
|
47
|
+
index: { type: Number, required: false, default: 0 },
|
|
48
|
+
// The total number of blocks on the content page
|
|
49
|
+
length: { type: Number, required: false, default: 0 },
|
|
50
|
+
|
|
51
|
+
// The content block html element
|
|
52
|
+
bindElement: { type: null, required: false, default: null },
|
|
53
|
+
|
|
54
|
+
// Button enable / disable props
|
|
55
|
+
draggable: { type: Boolean, required: false, default: true },
|
|
56
|
+
savable: { type: Boolean, required: false, default: true },
|
|
57
|
+
editable: { type: Boolean, required: false, default: true },
|
|
58
|
+
deletable: { type: Boolean, required: false, default: true },
|
|
59
|
+
editMode: { type: Boolean, required: false, default: null },
|
|
60
|
+
saving: { type: Boolean, required: false, default: null },
|
|
61
|
+
btnGroupId: { type: String, required: false, default: '' },
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
data() {
|
|
65
|
+
return {
|
|
66
|
+
showMenu: false,
|
|
67
|
+
isTransforming: false,
|
|
68
|
+
actions: [
|
|
69
|
+
{
|
|
70
|
+
tag: 'plugin-core-accordion',
|
|
71
|
+
label: this.$t(
|
|
72
|
+
'windward.core.shared.content_blocks.title.accordion'
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
tag: 'plugin-core-tab',
|
|
77
|
+
label: this.$t(
|
|
78
|
+
'windward.core.shared.content_blocks.title.tab'
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
tag: 'plugin-core-clickable-icons',
|
|
83
|
+
label: this.$t(
|
|
84
|
+
'windward.core.shared.content_blocks.title.clickable_icons'
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
computed: {
|
|
91
|
+
...mapGetters({
|
|
92
|
+
organization: 'organization/get',
|
|
93
|
+
course: 'course/get',
|
|
94
|
+
}),
|
|
95
|
+
vModel: {
|
|
96
|
+
get() {
|
|
97
|
+
return this.value
|
|
98
|
+
},
|
|
99
|
+
set(value) {
|
|
100
|
+
this.$emit('input', value)
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
canTransformBlock() {
|
|
104
|
+
if (!this.value || this.value.tag !== 'content-blocks-text') {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const body = _.get(this.value, 'body', '')
|
|
109
|
+
if (!body || typeof body !== 'string') {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let paragraphCount = 0
|
|
114
|
+
try {
|
|
115
|
+
const doc = new DOMParser().parseFromString(body, 'text/html')
|
|
116
|
+
const paragraphs = Array.from(doc.querySelectorAll('p, li'))
|
|
117
|
+
paragraphCount = paragraphs.filter((el) =>
|
|
118
|
+
(el.textContent || '').trim()
|
|
119
|
+
).length
|
|
120
|
+
} catch (_e) {
|
|
121
|
+
// Fallback: simple split on double line breaks
|
|
122
|
+
const normalized = body
|
|
123
|
+
.replace(/<[^>]+>/g, '\n')
|
|
124
|
+
.split(/\n+/)
|
|
125
|
+
.map((p) => p.trim())
|
|
126
|
+
.filter((p) => p.length > 0)
|
|
127
|
+
paragraphCount = normalized.length
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return paragraphCount >= 2
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
methods: {
|
|
134
|
+
onClickShowTransforms(e) {
|
|
135
|
+
e.stopPropagation()
|
|
136
|
+
},
|
|
137
|
+
buildTransformedBlock(sourceBlock, data, targetType) {
|
|
138
|
+
const baseOrder = _.get(sourceBlock, 'order', 0)
|
|
139
|
+
const newBlock = {
|
|
140
|
+
id: _.uniqueId('create_content_block_'),
|
|
141
|
+
content_id: null,
|
|
142
|
+
tag: '',
|
|
143
|
+
body: '',
|
|
144
|
+
status: 'draft',
|
|
145
|
+
order: baseOrder + 0.5,
|
|
146
|
+
type: {
|
|
147
|
+
save_state: false,
|
|
148
|
+
trackable: false,
|
|
149
|
+
completable: false,
|
|
150
|
+
},
|
|
151
|
+
metadata: {
|
|
152
|
+
config: {},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const blockTitle = data.block_title || ''
|
|
157
|
+
const items = Array.isArray(data.items) ? data.items : []
|
|
158
|
+
|
|
159
|
+
if (targetType === 'plugin-core-accordion') {
|
|
160
|
+
newBlock.tag = 'plugin-core-accordion'
|
|
161
|
+
newBlock.body = this.$t(
|
|
162
|
+
'windward.core.shared.content_blocks.title.accordion'
|
|
163
|
+
)
|
|
164
|
+
newBlock.metadata.config = {
|
|
165
|
+
title: blockTitle,
|
|
166
|
+
display_title: true,
|
|
167
|
+
instructions: this.$t(
|
|
168
|
+
'windward.core.components.settings.accordion.instructions'
|
|
169
|
+
),
|
|
170
|
+
items: items.map((item) => ({
|
|
171
|
+
header: item.title || '',
|
|
172
|
+
expand: false,
|
|
173
|
+
content: item.body_html || '',
|
|
174
|
+
fileConfig: {
|
|
175
|
+
display: {
|
|
176
|
+
width: 100,
|
|
177
|
+
margin: '',
|
|
178
|
+
padding: '',
|
|
179
|
+
},
|
|
180
|
+
hideBackground: true,
|
|
181
|
+
},
|
|
182
|
+
})),
|
|
183
|
+
}
|
|
184
|
+
} else if (targetType === 'plugin-core-tab') {
|
|
185
|
+
newBlock.tag = 'plugin-core-tab'
|
|
186
|
+
newBlock.body = this.$t(
|
|
187
|
+
'windward.core.shared.content_blocks.title.tab'
|
|
188
|
+
)
|
|
189
|
+
newBlock.metadata.config = {
|
|
190
|
+
title: blockTitle,
|
|
191
|
+
display_title: true,
|
|
192
|
+
instructions: this.$t(
|
|
193
|
+
'windward.core.components.settings.tab.instructions'
|
|
194
|
+
),
|
|
195
|
+
currentTab: 0,
|
|
196
|
+
items: items.map((item) => ({
|
|
197
|
+
tabHeader: item.title || '',
|
|
198
|
+
expand: false,
|
|
199
|
+
content: item.body_html || '',
|
|
200
|
+
imageAsset: {
|
|
201
|
+
display: {
|
|
202
|
+
width: 100,
|
|
203
|
+
margin: '',
|
|
204
|
+
padding: '',
|
|
205
|
+
},
|
|
206
|
+
hideBackground: true,
|
|
207
|
+
},
|
|
208
|
+
})),
|
|
209
|
+
}
|
|
210
|
+
} else if (targetType === 'plugin-core-clickable-icons') {
|
|
211
|
+
newBlock.tag = 'plugin-core-clickable-icons'
|
|
212
|
+
newBlock.body = this.$t(
|
|
213
|
+
'windward.core.shared.content_blocks.title.clickable_icons'
|
|
214
|
+
)
|
|
215
|
+
newBlock.metadata.config = {
|
|
216
|
+
title: blockTitle,
|
|
217
|
+
display_title: true,
|
|
218
|
+
description: this.$t(
|
|
219
|
+
'windward.core.components.settings.clickable_icon.information'
|
|
220
|
+
),
|
|
221
|
+
display: {
|
|
222
|
+
show_title: false,
|
|
223
|
+
show_background: false,
|
|
224
|
+
round_icon: false,
|
|
225
|
+
italic_icon: false,
|
|
226
|
+
large_icon: false,
|
|
227
|
+
autocolor: true,
|
|
228
|
+
},
|
|
229
|
+
items: items.map((item) => ({
|
|
230
|
+
icon: '',
|
|
231
|
+
fileConfig: {},
|
|
232
|
+
iconImage: false,
|
|
233
|
+
title: item.title || '',
|
|
234
|
+
body: item.body_html || '',
|
|
235
|
+
color: {
|
|
236
|
+
class: '',
|
|
237
|
+
},
|
|
238
|
+
active: false,
|
|
239
|
+
})),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return newBlock
|
|
244
|
+
},
|
|
245
|
+
async performBlockTransform(e, targetType) {
|
|
246
|
+
e.stopPropagation()
|
|
247
|
+
|
|
248
|
+
const html = _.get(this.value, 'body', '')
|
|
249
|
+
if (!html || typeof html !== 'string') {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.isTransforming) {
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const organizationId = _.get(this.organization, 'id', null)
|
|
258
|
+
const courseId = _.get(this.course, 'id', null)
|
|
259
|
+
|
|
260
|
+
if (!organizationId || !courseId) {
|
|
261
|
+
if (this.$toast) {
|
|
262
|
+
this.$toast.error(
|
|
263
|
+
this.$t(
|
|
264
|
+
'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_error'
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const request = new Course()
|
|
272
|
+
request.custom(
|
|
273
|
+
new Organization({ id: organizationId }),
|
|
274
|
+
new Course({ id: courseId }),
|
|
275
|
+
'llm-block-transform'
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const resourcePath = request._customResource
|
|
279
|
+
if (!resourcePath) {
|
|
280
|
+
if (this.$toast) {
|
|
281
|
+
this.$toast.error(
|
|
282
|
+
this.$t(
|
|
283
|
+
'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_error'
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const payload = {
|
|
291
|
+
html,
|
|
292
|
+
target_tag: targetType,
|
|
293
|
+
language: this.$i18n.locale,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.isTransforming = true
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const requestConfig = request._reqConfig(
|
|
300
|
+
{
|
|
301
|
+
method: 'POST',
|
|
302
|
+
url: `${request.baseURL()}/${resourcePath}`,
|
|
303
|
+
data: payload,
|
|
304
|
+
},
|
|
305
|
+
{ forceMethod: true }
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
const response = await request.request(requestConfig)
|
|
309
|
+
const data = response?.data || response
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
!data ||
|
|
313
|
+
!Array.isArray(data.items) ||
|
|
314
|
+
data.items.length === 0
|
|
315
|
+
) {
|
|
316
|
+
throw new Error('invalid_response')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const newBlock = this.buildTransformedBlock(
|
|
320
|
+
this.value,
|
|
321
|
+
data,
|
|
322
|
+
targetType
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
// Let the host content page own block insertion via the global event
|
|
326
|
+
this.$eb.$emit('create:content-block', newBlock)
|
|
327
|
+
|
|
328
|
+
// Immediately focus the new block for the user
|
|
329
|
+
const activeBlock = _.cloneDeep(newBlock)
|
|
330
|
+
this.$ContentService.setContentBlock(activeBlock)
|
|
331
|
+
this.$eb.$emit('block:focus', activeBlock)
|
|
332
|
+
} catch (e) {
|
|
333
|
+
// Surface errors instead of failing silently
|
|
334
|
+
// eslint-disable-next-line no-console
|
|
335
|
+
console.error('Block transform failed', e)
|
|
336
|
+
if (this.$toast) {
|
|
337
|
+
this.$toast.error(
|
|
338
|
+
this.$t(
|
|
339
|
+
'windward.integrations.components.content.blocks.action_panel.transform_block.change_block_type_error'
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
this.isTransforming = false
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
</script>
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
:disabled="isLoading || disabled"
|
|
18
18
|
></BloomTaxonomySelector>
|
|
19
19
|
</v-col>
|
|
20
|
+
<v-col v-if="questionType !== 'true_false'" cols="12">
|
|
21
|
+
<v-switch
|
|
22
|
+
v-model="keepExisting"
|
|
23
|
+
:disabled="isLoading || disabled"
|
|
24
|
+
:label="$t('windward.integrations.components.llm.generate_content.keep_existing_inputs')"
|
|
25
|
+
></v-switch>
|
|
26
|
+
</v-col>
|
|
20
27
|
<v-col cols="12">
|
|
21
28
|
<v-btn
|
|
22
29
|
elevation="0"
|
|
@@ -84,6 +91,7 @@ export default {
|
|
|
84
91
|
showLoadingText: false,
|
|
85
92
|
selectedContent: null,
|
|
86
93
|
selectedDifficulty: null,
|
|
94
|
+
keepExisting: true,
|
|
87
95
|
}
|
|
88
96
|
},
|
|
89
97
|
computed: {
|
|
@@ -116,6 +124,118 @@ export default {
|
|
|
116
124
|
},
|
|
117
125
|
},
|
|
118
126
|
methods: {
|
|
127
|
+
buildClearedQuestion() {
|
|
128
|
+
const type = this.questionType
|
|
129
|
+
const cleared = {
|
|
130
|
+
question_type: type,
|
|
131
|
+
assessment_question_type_id: _.get(this.value, 'assessment_question_type_id', null),
|
|
132
|
+
body: '',
|
|
133
|
+
question_metadata: {},
|
|
134
|
+
answer_metadata: {},
|
|
135
|
+
metadata: {},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (type === 'multi_choice_single_answer' || type === 'multi_choice_multi_answer') {
|
|
139
|
+
cleared.question_metadata = { options: [] }
|
|
140
|
+
cleared.answer_metadata = { feedback: {} }
|
|
141
|
+
} else if (type === 'matching_text') {
|
|
142
|
+
cleared.question_metadata = { prompts: [], choices: [] }
|
|
143
|
+
cleared.answer_metadata = { map: [], feedback: {} }
|
|
144
|
+
} else if (type === 'ordering') {
|
|
145
|
+
cleared.question_metadata = { prompts: [] }
|
|
146
|
+
cleared.answer_metadata = { map: [], feedback: {} }
|
|
147
|
+
} else if (type === 'fill_in_the_blank') {
|
|
148
|
+
cleared.question_metadata = _.get(this.value, 'question_metadata', {}) || {}
|
|
149
|
+
cleared.answer_metadata = { correct_answer_options: [], feedback: {} }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return cleared
|
|
153
|
+
},
|
|
154
|
+
buildExistingInputs() {
|
|
155
|
+
const q = _.cloneDeep(this.value) || {}
|
|
156
|
+
const type = this.questionType
|
|
157
|
+
|
|
158
|
+
const nonEmpty = (s) => typeof s === 'string' && s.replace(/<[^>]*>/g, '').trim().length > 0
|
|
159
|
+
|
|
160
|
+
const base = { body: q.body || '' }
|
|
161
|
+
|
|
162
|
+
if (type === 'multi_choice_single_answer' || type === 'multi_choice_multi_answer') {
|
|
163
|
+
const options = _.get(q, 'question_metadata.options', [])
|
|
164
|
+
const cleanedOptions = options.map((o, i) => ({ id: o.id || `option_${i}`, body: o.body || '' }))
|
|
165
|
+
const answerMeta = _.get(q, 'answer_metadata', {})
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
question_type: type,
|
|
169
|
+
question_metadata: { options: cleanedOptions },
|
|
170
|
+
answer_metadata: {
|
|
171
|
+
correct_id: answerMeta.correct_id,
|
|
172
|
+
correct_ids: answerMeta.correct_ids,
|
|
173
|
+
feedback: answerMeta.feedback || {},
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (type === 'matching_text') {
|
|
179
|
+
const prompts = _.get(q, 'question_metadata.prompts', []).map((p) => ({ id: p.id, body: p.body || '' }))
|
|
180
|
+
const choices = _.get(q, 'question_metadata.choices', []).map((c, i) => ({ id: c.id, letter_id: c.letter_id || String.fromCharCode(65 + i), body: c.body || '' }))
|
|
181
|
+
const map = _.get(q, 'answer_metadata.map', [])
|
|
182
|
+
return {
|
|
183
|
+
...base,
|
|
184
|
+
question_type: type,
|
|
185
|
+
question_metadata: { prompts, choices },
|
|
186
|
+
answer_metadata: { map, feedback: _.get(q, 'answer_metadata.feedback', {}) },
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (type === 'ordering') {
|
|
191
|
+
const prompts = _.get(q, 'question_metadata.prompts', []).map((p) => ({ id: p.id, body: p.body || '' }))
|
|
192
|
+
const map = _.get(q, 'answer_metadata.map', [])
|
|
193
|
+
return {
|
|
194
|
+
...base,
|
|
195
|
+
question_type: type,
|
|
196
|
+
question_metadata: { prompts },
|
|
197
|
+
answer_metadata: { map, feedback: _.get(q, 'answer_metadata.feedback', {}) },
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (type === 'fill_in_the_blank') {
|
|
202
|
+
const corrects = _.get(q, 'answer_metadata.correct_answer_options', [])
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
question_type: type,
|
|
206
|
+
question_metadata: _.get(q, 'question_metadata', {}),
|
|
207
|
+
answer_metadata: {
|
|
208
|
+
correct_id: _.get(q, 'answer_metadata.correct_id'),
|
|
209
|
+
correct_answer_options: corrects.map((c) => ({ id: c.id, answer: c.answer || '' })),
|
|
210
|
+
feedback: _.get(q, 'answer_metadata.feedback', {}),
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return base
|
|
216
|
+
},
|
|
217
|
+
hasAnyExistingInput(snapshot) {
|
|
218
|
+
if (!snapshot) return false
|
|
219
|
+
if (snapshot.body && snapshot.body.replace(/<[^>]*>/g, '').trim().length > 0) return true
|
|
220
|
+
const qm = snapshot.question_metadata || {}
|
|
221
|
+
if (Array.isArray(qm.options)) {
|
|
222
|
+
return qm.options.some((o) => (o.body || '').replace(/<[^>]*>/g, '').trim().length > 0)
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(qm.prompts)) {
|
|
225
|
+
return qm.prompts.some((p) => (p.body || '').replace(/<[^>]*>/g, '').trim().length > 0)
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(qm.choices)) {
|
|
228
|
+
return qm.choices.some((c) => (c.body || '').replace(/<[^>]*>/g, '').trim().length > 0)
|
|
229
|
+
}
|
|
230
|
+
const am = snapshot.answer_metadata || {}
|
|
231
|
+
if (Array.isArray(am.correct_answer_options)) {
|
|
232
|
+
return am.correct_answer_options.some((c) => (c.answer || '').trim().length > 0)
|
|
233
|
+
}
|
|
234
|
+
if (am.feedback && typeof am.feedback.question === 'string' && am.feedback.question.trim().length > 0) {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
return false
|
|
238
|
+
},
|
|
119
239
|
async generateAIQuestion() {
|
|
120
240
|
this.isLoading = true
|
|
121
241
|
|
|
@@ -133,23 +253,64 @@ export default {
|
|
|
133
253
|
// ASSESSMENT QUESTION GENERATION
|
|
134
254
|
const assessment = new Assessment({ id: this.block.id })
|
|
135
255
|
|
|
136
|
-
const
|
|
256
|
+
const endpointBuilder = AssessmentQuestion.custom(
|
|
137
257
|
course,
|
|
138
258
|
content,
|
|
139
259
|
assessment,
|
|
140
260
|
`suggest-questions`
|
|
141
|
-
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
let response
|
|
264
|
+
if (this.keepExisting) {
|
|
265
|
+
const existingInputs = this.buildExistingInputs()
|
|
266
|
+
const hasExisting = this.hasAnyExistingInput(existingInputs)
|
|
142
267
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
268
|
+
if (hasExisting) {
|
|
269
|
+
const payload = {
|
|
270
|
+
filter: {
|
|
271
|
+
question_type: this.questionType,
|
|
272
|
+
// Do not include blooms when preserving existing inputs
|
|
273
|
+
},
|
|
274
|
+
keep_existing_inputs: true,
|
|
275
|
+
existing_inputs: existingInputs,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
response = await AssessmentQuestion.config({
|
|
279
|
+
method: 'POST',
|
|
280
|
+
data: payload,
|
|
281
|
+
})
|
|
282
|
+
.custom(course, content, assessment, `suggest-questions`)
|
|
283
|
+
.get()
|
|
284
|
+
} else {
|
|
285
|
+
// Treat as replace flow if no existing inputs
|
|
286
|
+
const builder = endpointBuilder.where(
|
|
287
|
+
'question_type',
|
|
288
|
+
this.questionType
|
|
289
|
+
)
|
|
290
|
+
if (this.selectedDifficulty !== 'None') {
|
|
291
|
+
builder.where('blooms_level', this.selectedDifficulty)
|
|
292
|
+
}
|
|
293
|
+
response = await builder.get()
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Replace flow (legacy behavior)
|
|
297
|
+
// Explicitly clear all existing inputs before generating
|
|
298
|
+
try {
|
|
299
|
+
const cleared = this.buildClearedQuestion()
|
|
300
|
+
this.$emit('input', cleared)
|
|
301
|
+
} catch (e) {
|
|
302
|
+
// no-op if emit fails
|
|
303
|
+
}
|
|
304
|
+
const builder = endpointBuilder.where(
|
|
305
|
+
'question_type',
|
|
306
|
+
this.questionType
|
|
148
307
|
)
|
|
308
|
+
if (this.selectedDifficulty !== 'None') {
|
|
309
|
+
builder.where('blooms_level', this.selectedDifficulty)
|
|
310
|
+
}
|
|
311
|
+
response = await builder.get()
|
|
149
312
|
}
|
|
150
313
|
|
|
151
|
-
const response = await responseBuilder.get()
|
|
152
|
-
|
|
153
314
|
if (response && response.length > 0) {
|
|
154
315
|
const generatedQuestion = response[0]
|
|
155
316
|
|
|
@@ -196,6 +357,16 @@ export default {
|
|
|
196
357
|
'An error occurred while generating content. Try generating again.'
|
|
197
358
|
) {
|
|
198
359
|
errorText = this.$t(`${basePath}.character_limit`)
|
|
360
|
+
} else if (
|
|
361
|
+
(errorMessage === 'assessment.error.content_mismatch' ||
|
|
362
|
+
errorType === 'content_mismatch') &&
|
|
363
|
+
errorSubtype === 'INITIAL_INPUTS_NOT_RELATED'
|
|
364
|
+
) {
|
|
365
|
+
// Inputs (question/correct answer) are unrelated to the course/page content
|
|
366
|
+
errorText =
|
|
367
|
+
this.$t(`${basePath}.inputs_unrelated`) +
|
|
368
|
+
'\\n\\n' +
|
|
369
|
+
this.$t(`${basePath}.inputs_unrelated_support`)
|
|
199
370
|
} else if (
|
|
200
371
|
errorType === 'insufficient_content' &&
|
|
201
372
|
error.response?.data?.error?.details
|
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
3
|
<v-row class="container-generate-ai mt-2">
|
|
4
|
-
<v-col>{{
|
|
4
|
+
<v-col cols="12" sm="auto" class="d-flex align-center">{{
|
|
5
5
|
$t('windward.integrations.components.llm.ai_assistance')
|
|
6
6
|
}}</v-col>
|
|
7
|
-
<v-col>
|
|
7
|
+
<v-col cols="12" sm>
|
|
8
8
|
<ContentSelector
|
|
9
9
|
v-model="selectedContent"
|
|
10
10
|
:disabled="isLoading || disabled"
|
|
11
11
|
></ContentSelector>
|
|
12
12
|
</v-col>
|
|
13
|
-
<v-col>
|
|
13
|
+
<v-col cols="12" sm class="bloom-taxonomy-col">
|
|
14
14
|
<BloomTaxonomySelector
|
|
15
15
|
v-model="selectedDifficulty"
|
|
16
16
|
:levels="taxonomyLevels"
|
|
17
17
|
:advanced-levels="isMultipleChoiceType ? false : hasAdvancedBlooms"
|
|
18
18
|
:disabled="isLoading || disabled"
|
|
19
|
-
|
|
19
|
+
hide-details
|
|
20
20
|
></BloomTaxonomySelector>
|
|
21
21
|
|
|
22
|
-
<div
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
<div class="caption-container">
|
|
23
|
+
<div
|
|
24
|
+
class="text-caption mt-1"
|
|
25
|
+
:style="{ visibility: isBucketGameType ? 'visible' : 'hidden' }"
|
|
26
|
+
>
|
|
27
|
+
{{
|
|
28
|
+
$t(
|
|
29
|
+
'windward.integrations.components.llm.generate_content.generate_questions.replaces_content'
|
|
30
|
+
)
|
|
31
|
+
}}
|
|
32
|
+
</div>
|
|
28
33
|
</div>
|
|
29
34
|
</v-col>
|
|
30
35
|
<v-col
|
|
31
36
|
v-if="showReplaceExistingToggle"
|
|
32
|
-
cols="
|
|
37
|
+
cols="12"
|
|
38
|
+
sm="auto"
|
|
33
39
|
class="d-flex align-center"
|
|
34
40
|
>
|
|
35
41
|
<v-switch
|
|
@@ -42,11 +48,11 @@
|
|
|
42
48
|
:disabled="isLoading"
|
|
43
49
|
></v-switch>
|
|
44
50
|
</v-col>
|
|
45
|
-
<v-col>
|
|
51
|
+
<v-col cols="12" class="button-col">
|
|
46
52
|
<v-btn
|
|
47
53
|
elevation="0"
|
|
48
54
|
color="secondary"
|
|
49
|
-
class="
|
|
55
|
+
class="btn-selector"
|
|
50
56
|
:loading="isLoading"
|
|
51
57
|
:disabled="isLoading"
|
|
52
58
|
@click="generateAIQuestion"
|
|
@@ -542,4 +548,20 @@ export default {
|
|
|
542
548
|
outline: 1px solid var(--v-secondary-base);
|
|
543
549
|
border-radius: $border-radius-root;
|
|
544
550
|
}
|
|
551
|
+
.bloom-taxonomy-col {
|
|
552
|
+
display: flex;
|
|
553
|
+
flex-direction: column;
|
|
554
|
+
}
|
|
555
|
+
.caption-container {
|
|
556
|
+
min-height: 28px;
|
|
557
|
+
display: flex;
|
|
558
|
+
align-items: flex-start;
|
|
559
|
+
}
|
|
560
|
+
.button-col {
|
|
561
|
+
display: flex;
|
|
562
|
+
align-items: flex-start;
|
|
563
|
+
}
|
|
564
|
+
.switch-col {
|
|
565
|
+
align-self: flex-start !important;
|
|
566
|
+
}
|
|
545
567
|
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
change_block_type: 'Change block type',
|
|
3
|
+
change_block_type_insufficient_paragraphs:
|
|
4
|
+
'Change block type is available when the text has at least two paragraphs.',
|
|
5
|
+
change_block_type_error:
|
|
6
|
+
'Unable to change block type. Try again or edit manually.',
|
|
7
|
+
}
|
|
@@ -51,6 +51,10 @@ export default {
|
|
|
51
51
|
content_mismatch_multiple_choice_support:
|
|
52
52
|
'Consider adding clear expository text with distinct concepts, definitions, or examples that support question and distractor creation.',
|
|
53
53
|
|
|
54
|
+
inputs_unrelated: 'Your question or correct answer is unrelated to this page\'s content.',
|
|
55
|
+
inputs_unrelated_support:
|
|
56
|
+
'Provide a question and/or correct answer that clearly relates to the selected page. Use terms, facts, or examples present on the page so the assistant can complete the remaining inputs.',
|
|
57
|
+
|
|
54
58
|
character_limit:
|
|
55
59
|
'An error occurred while generating content. Try generating again.',
|
|
56
60
|
character_limit_detailed:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
change_block_type: 'Cambiar tipo de bloque',
|
|
3
|
+
change_block_type_insufficient_paragraphs:
|
|
4
|
+
'Cambiar el tipo de bloque está disponible cuando el texto tiene al menos dos párrafos.',
|
|
5
|
+
change_block_type_error:
|
|
6
|
+
'No se puede cambiar el tipo de bloque. Inténtalo de nuevo o edítalo manualmente.',
|
|
7
|
+
}
|
|
@@ -54,6 +54,11 @@ export default {
|
|
|
54
54
|
content_mismatch_multiple_choice_support:
|
|
55
55
|
'Considere agregar texto expositivo claro con conceptos, definiciones o ejemplos distintos que respalden la creación de preguntas y distractores.',
|
|
56
56
|
|
|
57
|
+
inputs_unrelated:
|
|
58
|
+
'Tu pregunta o respuesta correcta no está relacionada con el contenido de esta página.',
|
|
59
|
+
inputs_unrelated_support:
|
|
60
|
+
'Proporciona una pregunta y/o respuesta correcta que se relacione claramente con la página seleccionada. Usa términos, hechos o ejemplos presentes en la página para que el asistente complete el resto de los campos.',
|
|
61
|
+
|
|
57
62
|
character_limit:
|
|
58
63
|
'Se produjo un error al generar el contenido. Intenta generar de nuevo.',
|
|
59
64
|
character_limit_detailed:
|
|
@@ -52,6 +52,11 @@ export default {
|
|
|
52
52
|
content_mismatch_multiple_choice_support:
|
|
53
53
|
'Överväg att lägga till tydlig, förklarande text med distinkta koncept, definitioner eller exempel som stödjer skapandet av frågor och distraktorer.',
|
|
54
54
|
|
|
55
|
+
inputs_unrelated:
|
|
56
|
+
'Din fråga eller korrekta svar är inte relaterade till innehållet på denna sida.',
|
|
57
|
+
inputs_unrelated_support:
|
|
58
|
+
'Ange en fråga och/eller ett korrekt svar som tydligt relaterar till den valda sidan. Använd termer, fakta eller exempel som finns på sidan så att assistenten kan fylla i återstående fält.',
|
|
59
|
+
|
|
55
60
|
character_limit:
|
|
56
61
|
'Ett fel uppstod när innehållet genererades. Försök generera igen.',
|
|
57
62
|
character_limit_detailed:
|
package/package.json
CHANGED
package/plugin.js
CHANGED
|
@@ -14,9 +14,11 @@ import ExternalIntegrationScormHelper from './helpers/ExternalIntegration/ScormH
|
|
|
14
14
|
import LtiConsumerBlock from './components/Content/Blocks/ExternalIntegration/LtiConsumer'
|
|
15
15
|
import LtiConsumerBlockSettings from './components/Settings/ExternalIntegration/LtiConsumerSettings'
|
|
16
16
|
|
|
17
|
-
import ScormConsumerBlock from './components/Content/Blocks/ExternalIntegration/ScormConsumer'
|
|
17
|
+
// import ScormConsumerBlock from './components/Content/Blocks/ExternalIntegration/ScormConsumer'
|
|
18
18
|
import ScormConsumerBlockSettings from './components/Settings/ExternalIntegration/ScormConsumerSettings'
|
|
19
19
|
|
|
20
|
+
import ActionPanelTransformBlock from './components/Content/Blocks/ActionPanel/TransformBlock.vue'
|
|
21
|
+
|
|
20
22
|
import ManageCourseIntegrationSettings from './components/Settings/ExternalIntegration/ManageCourseIntegrationSettings'
|
|
21
23
|
|
|
22
24
|
import FileImportMenu from './components/FileImport/FileImportMenu.vue'
|
|
@@ -279,7 +281,8 @@ export default {
|
|
|
279
281
|
'windward.integrations.shared.content_blocks.grouping.integrations',
|
|
280
282
|
},
|
|
281
283
|
},
|
|
282
|
-
/*
|
|
284
|
+
/*
|
|
285
|
+
{
|
|
283
286
|
tag: 'windward-integrations-scorm-consumer',
|
|
284
287
|
template: ScormConsumerBlock,
|
|
285
288
|
metadata: {
|
|
@@ -288,7 +291,8 @@ export default {
|
|
|
288
291
|
grouping:
|
|
289
292
|
'windward.integrations.shared.content_blocks.grouping.integrations',
|
|
290
293
|
},
|
|
291
|
-
}
|
|
294
|
+
},
|
|
295
|
+
*/
|
|
292
296
|
],
|
|
293
297
|
contentBlockSetting: [
|
|
294
298
|
{
|
|
@@ -297,7 +301,7 @@ export default {
|
|
|
297
301
|
context: ['block.windward-integrations-lti-consumer'],
|
|
298
302
|
metadata: {
|
|
299
303
|
icon: 'mdi-cog',
|
|
300
|
-
name: 'windward.integrations.shared.settings.title.
|
|
304
|
+
name: 'windward.integrations.shared.settings.title.block_builder',
|
|
301
305
|
},
|
|
302
306
|
},
|
|
303
307
|
{
|
|
@@ -306,10 +310,17 @@ export default {
|
|
|
306
310
|
context: ['block.windward-integrations-scorm-consumer'],
|
|
307
311
|
metadata: {
|
|
308
312
|
icon: 'mdi-cog',
|
|
309
|
-
name: 'windward.integrations.shared.settings.title.
|
|
313
|
+
name: 'windward.integrations.shared.settings.title.block_builder',
|
|
310
314
|
},
|
|
311
315
|
},
|
|
312
316
|
],
|
|
317
|
+
contentBlockActionPanel: [
|
|
318
|
+
{
|
|
319
|
+
tag: 'windward-integrations-action-panel-transform-block',
|
|
320
|
+
template: ActionPanelTransformBlock,
|
|
321
|
+
context: ['block.content-blocks-text'],
|
|
322
|
+
},
|
|
323
|
+
],
|
|
313
324
|
courseSetting: [
|
|
314
325
|
{
|
|
315
326
|
tag: 'windward-integrations-external-integration-settings',
|