@windward/core 0.27.0 → 0.28.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 +37 -0
- package/components/Content/Blocks/Accordion.vue +1 -1
- package/components/Content/Blocks/BlockQuote.vue +1 -1
- package/components/Content/Blocks/ClickableIcons.vue +4 -3
- package/components/Content/Blocks/Email.vue +1 -1
- package/components/Content/Blocks/FileDownload.vue +2 -2
- package/components/Content/Blocks/Image.vue +140 -0
- package/components/Content/Blocks/OpenResponse.vue +419 -5
- package/components/Content/Blocks/ScenarioChoice.vue +1 -1
- package/components/Content/Blocks/Tab.vue +1 -1
- package/components/Content/Blocks/UserUpload.vue +1 -1
- package/components/Content/Blocks/Video.vue +361 -22
- package/components/Settings/ClickableIconsSettings.vue +3 -3
- package/components/Settings/ImageSettings.vue +26 -0
- package/components/Settings/OpenResponseSettings.vue +59 -0
- package/components/Settings/VideoSettings/SourcePicker.vue +40 -32
- package/components/utils/ContentViewer.vue +180 -1
- package/i18n/en-US/components/content/blocks/open_response.ts +18 -0
- package/i18n/en-US/components/settings/open_response.ts +5 -0
- package/i18n/es-ES/components/content/blocks/open_response.ts +18 -0
- package/i18n/es-ES/components/settings/open_response.ts +5 -0
- package/i18n/sv-SE/components/content/blocks/open_response.ts +18 -0
- package/i18n/sv-SE/components/settings/open_response.ts +5 -0
- package/package.json +2 -2
- package/test/Components/Settings/ClickableIconsSettings.spec.js +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div :class="blockDirectionClasses" :dir="blockTextDirection">
|
|
3
3
|
<v-container
|
|
4
4
|
v-if="
|
|
5
5
|
block.metadata.config.title ||
|
|
@@ -64,7 +64,11 @@
|
|
|
64
64
|
class="alert-text"
|
|
65
65
|
></TextViewer>
|
|
66
66
|
</v-alert>
|
|
67
|
-
<div
|
|
67
|
+
<div
|
|
68
|
+
v-if="
|
|
69
|
+
block.metadata.config.sample_response && !aiModeEnabled
|
|
70
|
+
"
|
|
71
|
+
>
|
|
68
72
|
<p>
|
|
69
73
|
{{
|
|
70
74
|
$t(
|
|
@@ -80,11 +84,145 @@
|
|
|
80
84
|
></TextViewer>
|
|
81
85
|
</v-alert>
|
|
82
86
|
</div>
|
|
87
|
+
<div v-if="aiModeEnabled" class="pt-6">
|
|
88
|
+
<p>
|
|
89
|
+
{{
|
|
90
|
+
$t(
|
|
91
|
+
'windward.core.components.content.blocks.open_response.ai_feedback'
|
|
92
|
+
)
|
|
93
|
+
}}
|
|
94
|
+
</p>
|
|
95
|
+
<v-alert v-if="aiFeedbackError" type="error">
|
|
96
|
+
{{ aiFeedbackError }}
|
|
97
|
+
<div class="pt-2">
|
|
98
|
+
<v-btn
|
|
99
|
+
small
|
|
100
|
+
color="primary"
|
|
101
|
+
elevation="0"
|
|
102
|
+
:disabled="isGeneratingFeedback"
|
|
103
|
+
@click="onGenerateFeedback"
|
|
104
|
+
>
|
|
105
|
+
{{
|
|
106
|
+
$t(
|
|
107
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_retry'
|
|
108
|
+
)
|
|
109
|
+
}}
|
|
110
|
+
</v-btn>
|
|
111
|
+
</div>
|
|
112
|
+
</v-alert>
|
|
113
|
+
<v-alert
|
|
114
|
+
v-else-if="aiFeedbackResult && aiFeedbackResult.feedback_html"
|
|
115
|
+
color="light-green lighten-5"
|
|
116
|
+
>
|
|
117
|
+
<div
|
|
118
|
+
v-if="
|
|
119
|
+
aiFeedbackResult &&
|
|
120
|
+
aiFeedbackResult.understanding_level
|
|
121
|
+
"
|
|
122
|
+
class="alert-text mb-2"
|
|
123
|
+
>
|
|
124
|
+
<strong>
|
|
125
|
+
{{
|
|
126
|
+
$t(
|
|
127
|
+
'windward.core.components.content.blocks.open_response.understanding_level'
|
|
128
|
+
)
|
|
129
|
+
}}:
|
|
130
|
+
</strong>
|
|
131
|
+
{{
|
|
132
|
+
getUnderstandingLevelLabel(
|
|
133
|
+
aiFeedbackResult.understanding_level
|
|
134
|
+
)
|
|
135
|
+
}}
|
|
136
|
+
</div>
|
|
137
|
+
<TextViewer
|
|
138
|
+
v-model="aiFeedbackResult.feedback_html"
|
|
139
|
+
:height="200"
|
|
140
|
+
class="alert-text"
|
|
141
|
+
></TextViewer>
|
|
142
|
+
</v-alert>
|
|
143
|
+
<v-alert
|
|
144
|
+
v-else-if="isGeneratingFeedback"
|
|
145
|
+
color="grey lighten-4"
|
|
146
|
+
>
|
|
147
|
+
<div class="d-flex align-center">
|
|
148
|
+
<v-progress-circular
|
|
149
|
+
indeterminate
|
|
150
|
+
:size="18"
|
|
151
|
+
:width="2"
|
|
152
|
+
color="primary"
|
|
153
|
+
class="mr-2"
|
|
154
|
+
></v-progress-circular>
|
|
155
|
+
<span>
|
|
156
|
+
{{
|
|
157
|
+
$t(
|
|
158
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_generating'
|
|
159
|
+
)
|
|
160
|
+
}}
|
|
161
|
+
</span>
|
|
162
|
+
<span v-if="aiFeedbackJob && aiFeedbackJob.progress">
|
|
163
|
+
({{ aiFeedbackJob.progress }}%)
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
</v-alert>
|
|
167
|
+
<v-alert v-else color="grey lighten-4">
|
|
168
|
+
{{
|
|
169
|
+
$t(
|
|
170
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_not_available'
|
|
171
|
+
)
|
|
172
|
+
}}
|
|
173
|
+
</v-alert>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
v-if="
|
|
177
|
+
aiFeedbackResult &&
|
|
178
|
+
aiFeedbackResult.citations &&
|
|
179
|
+
aiFeedbackResult.citations.length
|
|
180
|
+
"
|
|
181
|
+
class="pt-4"
|
|
182
|
+
>
|
|
183
|
+
<p>
|
|
184
|
+
{{
|
|
185
|
+
$t(
|
|
186
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_sources'
|
|
187
|
+
)
|
|
188
|
+
}}
|
|
189
|
+
</p>
|
|
190
|
+
<v-list dense class="pt-0 pb-0">
|
|
191
|
+
<v-list-item
|
|
192
|
+
v-for="citation in aiFeedbackResult.citations"
|
|
193
|
+
:key="citation.id"
|
|
194
|
+
class="pl-0 pr-0"
|
|
195
|
+
>
|
|
196
|
+
<v-list-item-content class="pl-0 pr-0">
|
|
197
|
+
<nuxt-link
|
|
198
|
+
v-if="getCitationLink(citation.id)"
|
|
199
|
+
:to="getCitationLink(citation.id)"
|
|
200
|
+
>
|
|
201
|
+
{{
|
|
202
|
+
citation.title ||
|
|
203
|
+
$t(
|
|
204
|
+
'windward.core.components.content.blocks.open_response.untitled_page'
|
|
205
|
+
)
|
|
206
|
+
}}
|
|
207
|
+
</nuxt-link>
|
|
208
|
+
<span v-else>
|
|
209
|
+
{{
|
|
210
|
+
citation.title ||
|
|
211
|
+
$t(
|
|
212
|
+
'windward.core.components.content.blocks.open_response.untitled_page'
|
|
213
|
+
)
|
|
214
|
+
}}
|
|
215
|
+
</span>
|
|
216
|
+
</v-list-item-content>
|
|
217
|
+
</v-list-item>
|
|
218
|
+
</v-list>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
83
221
|
<p class="pa-3 text-center">
|
|
84
222
|
<v-btn
|
|
85
223
|
elevation="0"
|
|
86
224
|
color="primary"
|
|
87
|
-
@click="
|
|
225
|
+
@click="onClickEdit"
|
|
88
226
|
>{{ $t('shared.forms.edit') }}</v-btn
|
|
89
227
|
>
|
|
90
228
|
</p>
|
|
@@ -115,6 +253,8 @@ import _ from 'lodash'
|
|
|
115
253
|
import { mapGetters } from 'vuex'
|
|
116
254
|
import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
|
|
117
255
|
import UserContentBlockState from '~/models/UserContentBlockState'
|
|
256
|
+
import Enrollment from '~/models/Enrollment'
|
|
257
|
+
import ContentBlock from '~/models/ContentBlock'
|
|
118
258
|
import Crypto from '~/helpers/Crypto'
|
|
119
259
|
import TextViewer from '~/components/Text/TextViewer'
|
|
120
260
|
import TextEditor from '~/components/Text/TextEditor'
|
|
@@ -128,17 +268,32 @@ export default {
|
|
|
128
268
|
extends: BaseContentBlock,
|
|
129
269
|
data() {
|
|
130
270
|
return {
|
|
271
|
+
isComponentActive: true,
|
|
131
272
|
stateLoaded: false,
|
|
132
273
|
response: '',
|
|
133
274
|
submitted: false,
|
|
134
275
|
updateKey: Crypto.id(),
|
|
276
|
+
aiFeedbackJobId: null,
|
|
277
|
+
aiFeedbackJob: null,
|
|
278
|
+
aiFeedbackResult: null,
|
|
279
|
+
aiFeedbackError: null,
|
|
280
|
+
isGeneratingFeedback: false,
|
|
281
|
+
feedbackPollingCancelled: false,
|
|
135
282
|
}
|
|
136
283
|
},
|
|
137
284
|
computed: {
|
|
138
285
|
...mapGetters({
|
|
139
286
|
enrollment: 'enrollment/get',
|
|
287
|
+
organization: 'organization/get',
|
|
140
288
|
}),
|
|
289
|
+
aiModeEnabled() {
|
|
290
|
+
return !!_.get(this.block, 'metadata.config.ai_mode_for_student', false)
|
|
291
|
+
},
|
|
141
292
|
canSubmit() {
|
|
293
|
+
if (this.aiModeEnabled) {
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
|
|
142
297
|
// Make sure the response is not empty and not equal to the starting text
|
|
143
298
|
return (
|
|
144
299
|
this.response &&
|
|
@@ -175,10 +330,24 @@ export default {
|
|
|
175
330
|
if (_.isEmpty(this.block.metadata.config.starting_text)) {
|
|
176
331
|
this.block.metadata.config.starting_text = ''
|
|
177
332
|
}
|
|
333
|
+
if (!_.isBoolean(this.block.metadata.config.ai_mode_for_student)) {
|
|
334
|
+
this.$set(this.block.metadata.config, 'ai_mode_for_student', false)
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
beforeDestroy() {
|
|
338
|
+
this.isComponentActive = false
|
|
339
|
+
this.feedbackPollingCancelled = true
|
|
178
340
|
},
|
|
179
341
|
mounted() {},
|
|
180
342
|
methods: {
|
|
181
343
|
async onAfterSetContentBlockState() {
|
|
344
|
+
// This component's state is persisted via UserContentBlockState, so ensure
|
|
345
|
+
// transient lifecycle flags are reset when restoring state.
|
|
346
|
+
this.isComponentActive = true
|
|
347
|
+
this.feedbackPollingCancelled = true
|
|
348
|
+
this.isGeneratingFeedback = false
|
|
349
|
+
this.aiFeedbackError = null
|
|
350
|
+
|
|
182
351
|
// Check to see if we have a state already for this block with the same block_id
|
|
183
352
|
// States are loaded via the ContentBlock.id but in this particular case we want to
|
|
184
353
|
// maintain the state ACROSS different ContentBlock.ids but with the same linked Block.id
|
|
@@ -198,6 +367,22 @@ export default {
|
|
|
198
367
|
'metadata.submitted',
|
|
199
368
|
false
|
|
200
369
|
)
|
|
370
|
+
|
|
371
|
+
this.aiFeedbackJobId = _.get(
|
|
372
|
+
userState,
|
|
373
|
+
'metadata.aiFeedbackJobId',
|
|
374
|
+
null
|
|
375
|
+
)
|
|
376
|
+
this.aiFeedbackJob = _.get(
|
|
377
|
+
userState,
|
|
378
|
+
'metadata.aiFeedbackJob',
|
|
379
|
+
null
|
|
380
|
+
)
|
|
381
|
+
this.aiFeedbackResult = _.get(
|
|
382
|
+
userState,
|
|
383
|
+
'metadata.aiFeedbackResult',
|
|
384
|
+
null
|
|
385
|
+
)
|
|
201
386
|
}
|
|
202
387
|
|
|
203
388
|
// If after the state is applied the response is still empty then apply the default response
|
|
@@ -208,14 +393,243 @@ export default {
|
|
|
208
393
|
// If we don't have a block_id then we are in the initial setup state
|
|
209
394
|
this.response = this.block.metadata.config.starting_text
|
|
210
395
|
}
|
|
396
|
+
|
|
397
|
+
if (!this.aiModeEnabled) {
|
|
398
|
+
this.aiFeedbackJobId = null
|
|
399
|
+
this.aiFeedbackJob = null
|
|
400
|
+
this.aiFeedbackResult = null
|
|
401
|
+
}
|
|
402
|
+
|
|
211
403
|
this.updateKey = Crypto.id()
|
|
212
404
|
this.stateLoaded = true
|
|
405
|
+
|
|
406
|
+
if (
|
|
407
|
+
this.aiModeEnabled &&
|
|
408
|
+
this.submitted &&
|
|
409
|
+
this.aiFeedbackJobId &&
|
|
410
|
+
!this.isGeneratingFeedback
|
|
411
|
+
) {
|
|
412
|
+
await this.resumeFeedbackJob()
|
|
413
|
+
}
|
|
213
414
|
},
|
|
214
|
-
onSubmit() {
|
|
415
|
+
async onSubmit() {
|
|
215
416
|
this.submitted = true
|
|
417
|
+
this.aiFeedbackError = null
|
|
418
|
+
|
|
419
|
+
if (this.aiModeEnabled) {
|
|
420
|
+
this.aiFeedbackJobId = null
|
|
421
|
+
this.aiFeedbackJob = null
|
|
422
|
+
this.aiFeedbackResult = null
|
|
423
|
+
}
|
|
216
424
|
|
|
217
425
|
// Force the state to save on submit and not wait
|
|
218
|
-
this.$Tracking.flushState()
|
|
426
|
+
await this.$Tracking.flushState()
|
|
427
|
+
|
|
428
|
+
if (this.aiModeEnabled) {
|
|
429
|
+
await this.onGenerateFeedback()
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
onClickEdit() {
|
|
433
|
+
this.feedbackPollingCancelled = true
|
|
434
|
+
this.isGeneratingFeedback = false
|
|
435
|
+
this.submitted = false
|
|
436
|
+
},
|
|
437
|
+
async fetchAiFeedbackStatus(jobId) {
|
|
438
|
+
const enrollment = new Enrollment({ id: this.enrollment.id })
|
|
439
|
+
const contentBlock = new ContentBlock({ id: this.block.id })
|
|
440
|
+
|
|
441
|
+
const statusResponse = await Enrollment.params({ job_id: jobId })
|
|
442
|
+
.custom(
|
|
443
|
+
enrollment,
|
|
444
|
+
contentBlock,
|
|
445
|
+
'open-response/ai-feedback/status'
|
|
446
|
+
)
|
|
447
|
+
.first()
|
|
448
|
+
|
|
449
|
+
this.aiFeedbackJob = _.get(statusResponse, 'job', null)
|
|
450
|
+
this.aiFeedbackResult = _.get(statusResponse, 'result', null)
|
|
451
|
+
|
|
452
|
+
return _.get(this.aiFeedbackJob, 'status', null)
|
|
453
|
+
},
|
|
454
|
+
async onGenerateFeedback() {
|
|
455
|
+
if (this.isGeneratingFeedback) {
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!this.aiModeEnabled) {
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!this.organization?.id) {
|
|
464
|
+
this.aiFeedbackError = this.$t(
|
|
465
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_error'
|
|
466
|
+
)
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.aiFeedbackError = null
|
|
471
|
+
this.feedbackPollingCancelled = false
|
|
472
|
+
this.isGeneratingFeedback = true
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const enrollment = new Enrollment({ id: this.enrollment.id })
|
|
476
|
+
const contentBlock = new ContentBlock({ id: this.block.id })
|
|
477
|
+
|
|
478
|
+
const startResponse = await Enrollment.config({
|
|
479
|
+
method: 'POST',
|
|
480
|
+
data: { response: this.response || '' },
|
|
481
|
+
})
|
|
482
|
+
.custom(enrollment, contentBlock, 'open-response/ai-feedback')
|
|
483
|
+
.first()
|
|
484
|
+
|
|
485
|
+
const job = _.get(startResponse, 'job', null)
|
|
486
|
+
const jobId = _.get(job, 'id', null)
|
|
487
|
+
if (!jobId) {
|
|
488
|
+
throw new Error('AI_FEEDBACK_MISSING_JOB_ID')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.aiFeedbackJobId = jobId
|
|
492
|
+
this.aiFeedbackJob = job
|
|
493
|
+
this.aiFeedbackResult = _.get(startResponse, 'result', null)
|
|
494
|
+
|
|
495
|
+
// Persist the job id + any immediate result
|
|
496
|
+
await this.$Tracking.flushState()
|
|
497
|
+
|
|
498
|
+
if (this.aiFeedbackResult) {
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
await this.pollFeedbackJob(jobId)
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (!this.isComponentActive) {
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
if (error?.cancelled) {
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this.aiFeedbackError = this.$t(
|
|
512
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_error'
|
|
513
|
+
)
|
|
514
|
+
} finally {
|
|
515
|
+
if (this.isComponentActive) {
|
|
516
|
+
this.isGeneratingFeedback = false
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
async resumeFeedbackJob() {
|
|
521
|
+
if (this.isGeneratingFeedback) {
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const jobId = this.aiFeedbackJobId
|
|
526
|
+
if (!jobId || !this.organization?.id) {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.feedbackPollingCancelled = false
|
|
531
|
+
this.aiFeedbackError = null
|
|
532
|
+
this.isGeneratingFeedback = true
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const status = await this.fetchAiFeedbackStatus(jobId)
|
|
536
|
+
if (status === 'queued' || status === 'running') {
|
|
537
|
+
await this.pollFeedbackJob(jobId)
|
|
538
|
+
}
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!this.isComponentActive) {
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
if (error?.cancelled) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.aiFeedbackError = this.$t(
|
|
548
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_error'
|
|
549
|
+
)
|
|
550
|
+
} finally {
|
|
551
|
+
if (this.isComponentActive) {
|
|
552
|
+
this.isGeneratingFeedback = false
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
async pollFeedbackJob(jobId) {
|
|
557
|
+
// 30 minutes at 2s intervals
|
|
558
|
+
const maxAttempts = 900
|
|
559
|
+
const delayMs = 2000
|
|
560
|
+
|
|
561
|
+
let attempt = 0
|
|
562
|
+
|
|
563
|
+
while (attempt < maxAttempts) {
|
|
564
|
+
if (
|
|
565
|
+
this.feedbackPollingCancelled ||
|
|
566
|
+
!this.isComponentActive
|
|
567
|
+
) {
|
|
568
|
+
const cancelled = new Error('AI_FEEDBACK_POLLING_CANCELLED')
|
|
569
|
+
cancelled.cancelled = true
|
|
570
|
+
throw cancelled
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// eslint-disable-next-line no-await-in-loop
|
|
574
|
+
const status = await this.fetchAiFeedbackStatus(jobId)
|
|
575
|
+
if (status === 'succeeded' && this.aiFeedbackResult) {
|
|
576
|
+
// Persist the final result so it loads instantly later
|
|
577
|
+
this.isGeneratingFeedback = false
|
|
578
|
+
// eslint-disable-next-line no-await-in-loop
|
|
579
|
+
await this.$Tracking.flushState()
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (status === 'failed') {
|
|
584
|
+
this.aiFeedbackError = this.$t(
|
|
585
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_error'
|
|
586
|
+
)
|
|
587
|
+
this.isGeneratingFeedback = false
|
|
588
|
+
// eslint-disable-next-line no-await-in-loop
|
|
589
|
+
await this.$Tracking.flushState()
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
attempt += 1
|
|
594
|
+
// eslint-disable-next-line no-await-in-loop
|
|
595
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
this.aiFeedbackError = this.$t(
|
|
599
|
+
'windward.core.components.content.blocks.open_response.ai_feedback_timeout'
|
|
600
|
+
)
|
|
601
|
+
this.isGeneratingFeedback = false
|
|
602
|
+
await this.$Tracking.flushState()
|
|
603
|
+
},
|
|
604
|
+
getCitationLink(pageId) {
|
|
605
|
+
const courseId =
|
|
606
|
+
_.get(this.enrollment, 'course.id', null) ||
|
|
607
|
+
_.get(this.enrollment, 'course_id', null)
|
|
608
|
+
const sectionId =
|
|
609
|
+
_.get(this.enrollment, 'course_section_id', null) ||
|
|
610
|
+
_.get(this.enrollment, 'courseSection.id', null)
|
|
611
|
+
|
|
612
|
+
if (!courseId || !sectionId || !pageId) {
|
|
613
|
+
return null
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return `/course/${courseId}/section/${sectionId}/content/${pageId}`
|
|
617
|
+
},
|
|
618
|
+
getUnderstandingLevelLabel(level) {
|
|
619
|
+
const normalized = String(level || '')
|
|
620
|
+
.trim()
|
|
621
|
+
.toLowerCase()
|
|
622
|
+
if (!normalized) {
|
|
623
|
+
return ''
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const key = `windward.core.components.content.blocks.open_response.understanding_levels.${normalized}`
|
|
627
|
+
const translated = this.$t(key)
|
|
628
|
+
if (translated === key) {
|
|
629
|
+
return _.startCase(normalized)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return translated
|
|
219
633
|
},
|
|
220
634
|
},
|
|
221
635
|
}
|