@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
|
<h2
|
|
4
4
|
v-if="
|
|
5
5
|
block.metadata.config.title &&
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{{ block.metadata.config.instructions }}
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
<div v-if="hasSource" class="video-wrapper">
|
|
16
|
+
<div v-if="hasSource" class="video-wrapper" dir="ltr">
|
|
17
17
|
<VuetifyPlayer
|
|
18
18
|
class="no-border-player"
|
|
19
19
|
:language="$i18n && $i18n.locale ? $i18n.locale : 'en-US'"
|
|
@@ -146,6 +146,124 @@ export default {
|
|
|
146
146
|
}
|
|
147
147
|
},
|
|
148
148
|
computed: {
|
|
149
|
+
courseSourceLocale() {
|
|
150
|
+
// Get the source language from translation metadata
|
|
151
|
+
// This is the original language the course was translated FROM
|
|
152
|
+
// For translated courses: metadata.translation.source_language (e.g., "EN")
|
|
153
|
+
// For non-translated courses: falls back to locale code
|
|
154
|
+
|
|
155
|
+
// First check translation metadata for source_language (e.g., "EN", "ES")
|
|
156
|
+
const translationSourceLang = _.get(this.block, 'course.metadata.translation.source_language') ||
|
|
157
|
+
this.$store?.getters?.['course/get']?.metadata?.translation?.source_language
|
|
158
|
+
if (translationSourceLang) {
|
|
159
|
+
return translationSourceLang.toLowerCase()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try source_locale_id lookup
|
|
163
|
+
const translationSourceLocale = _.get(this.block, 'course.metadata.translation.source_locale_id')
|
|
164
|
+
if (translationSourceLocale) {
|
|
165
|
+
const sourceLocale = this.$store?.getters?.['locales/getById']?.(translationSourceLocale)
|
|
166
|
+
if (sourceLocale?.code) {
|
|
167
|
+
return sourceLocale.code.toLowerCase()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
_.get(this.block, 'course.locale.code')?.toLowerCase() ||
|
|
173
|
+
this.$store?.getters?.['course/sourceLocale']?.code?.toLowerCase() ||
|
|
174
|
+
'en-us'
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
courseTargetLocale() {
|
|
178
|
+
// Get the target language from translation metadata
|
|
179
|
+
// This is the language the course was translated INTO
|
|
180
|
+
// For translated courses: metadata.translation.target_language (e.g., "PT-BR", "ES")
|
|
181
|
+
|
|
182
|
+
const translationTargetLang = _.get(this.block, 'course.metadata.translation.target_language') ||
|
|
183
|
+
this.$store?.getters?.['course/get']?.metadata?.translation?.target_language
|
|
184
|
+
if (translationTargetLang) {
|
|
185
|
+
return translationTargetLang.toLowerCase()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Try target_locale_id lookup
|
|
189
|
+
const translationTargetLocale = _.get(this.block, 'course.metadata.translation.target_locale_id')
|
|
190
|
+
if (translationTargetLocale) {
|
|
191
|
+
const targetLocale = this.$store?.getters?.['locales/getById']?.(translationTargetLocale)
|
|
192
|
+
if (targetLocale?.code) {
|
|
193
|
+
return targetLocale.code.toLowerCase()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// No target language - course is not translated
|
|
198
|
+
return null
|
|
199
|
+
},
|
|
200
|
+
courseCurrentLocale() {
|
|
201
|
+
// Get the current course's locale code (the language this course is in)
|
|
202
|
+
// For a translated course, this is the target language
|
|
203
|
+
// Priority: translation target_language > block.course.locale > store getters > fallback
|
|
204
|
+
|
|
205
|
+
// For translated courses, use target_language directly
|
|
206
|
+
if (this.courseTargetLocale) {
|
|
207
|
+
return this.courseTargetLocale
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const localeCode =
|
|
211
|
+
_.get(this.block, 'course.locale.code')?.toLowerCase() ||
|
|
212
|
+
_.get(this.block, 'course.locale.name')?.toLowerCase() ||
|
|
213
|
+
this.$store?.getters?.['course/localeCode']?.toLowerCase() ||
|
|
214
|
+
this.$store?.getters?.['course/locale']?.code?.toLowerCase() ||
|
|
215
|
+
this.$store?.getters?.['course/locale']?.name?.toLowerCase() ||
|
|
216
|
+
this.$store?.state?.course?.course?.locale?.code?.toLowerCase() ||
|
|
217
|
+
this.$store?.state?.course?.course?.locale?.name?.toLowerCase()
|
|
218
|
+
|
|
219
|
+
return localeCode || this.courseSourceLocale
|
|
220
|
+
},
|
|
221
|
+
isTranslatedCourse() {
|
|
222
|
+
// Check if this course is a translated course (has translation metadata)
|
|
223
|
+
return !!(
|
|
224
|
+
_.get(this.block, 'course.metadata.translation') ||
|
|
225
|
+
this.$store?.getters?.['course/get']?.metadata?.translation
|
|
226
|
+
)
|
|
227
|
+
},
|
|
228
|
+
courseTargetLocaleId() {
|
|
229
|
+
// Get the target_locale_id from translation metadata (UUID)
|
|
230
|
+
// This is used to match VTT files which are linked by locale_id
|
|
231
|
+
return _.get(this.block, 'course.metadata.translation.target_locale_id') ||
|
|
232
|
+
this.$store?.getters?.['course/get']?.metadata?.translation?.target_locale_id ||
|
|
233
|
+
_.get(this.block, 'course.locale_id') ||
|
|
234
|
+
this.$store?.getters?.['course/get']?.locale_id ||
|
|
235
|
+
null
|
|
236
|
+
},
|
|
237
|
+
allowedCaptionLocales() {
|
|
238
|
+
// Only show captions for the source language and the current course's language
|
|
239
|
+
// This prevents showing all translated captions (FR, ES, DE, etc.) when viewing a specific course
|
|
240
|
+
const locales = new Set()
|
|
241
|
+
|
|
242
|
+
// Helper to add both full locale and base language code
|
|
243
|
+
const addLocale = (locale) => {
|
|
244
|
+
if (!locale) return
|
|
245
|
+
locales.add(locale)
|
|
246
|
+
locales.add(locale.toLowerCase())
|
|
247
|
+
// Also add base language code (e.g., "en" from "en-US", "fr" from "fr-FR")
|
|
248
|
+
const base = locale.split('-')[0]?.toLowerCase()
|
|
249
|
+
if (base) {
|
|
250
|
+
locales.add(base)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Always include the source language (original English captions)
|
|
255
|
+
addLocale(this.courseSourceLocale)
|
|
256
|
+
|
|
257
|
+
// Include the current course's language
|
|
258
|
+
addLocale(this.courseCurrentLocale)
|
|
259
|
+
|
|
260
|
+
// Also include tracks with no locale (legacy/default tracks)
|
|
261
|
+
locales.add(null)
|
|
262
|
+
locales.add(undefined)
|
|
263
|
+
locales.add('')
|
|
264
|
+
|
|
265
|
+
return locales
|
|
266
|
+
},
|
|
149
267
|
hasSource() {
|
|
150
268
|
// __isDirty is used to communicate from the settings panel that a source / caption
|
|
151
269
|
// has changed and to reload the video player
|
|
@@ -175,12 +293,38 @@ export default {
|
|
|
175
293
|
let file = this.resolveAsset(
|
|
176
294
|
playlist[index].sources[sourceIndex]
|
|
177
295
|
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
296
|
+
|
|
297
|
+
// Try to get translated notes from VTT file (for translated courses)
|
|
298
|
+
let notes = null
|
|
299
|
+
if (this.isTranslatedCourse) {
|
|
300
|
+
const linkedCaptions = this.getAllLinkedCaptions(file)
|
|
301
|
+
// Find VTT matching by locale_id (UUID) or locale.code (language code)
|
|
302
|
+
const currentVtt = _.find(linkedCaptions, vtt => {
|
|
303
|
+
// First try matching by locale_id (most reliable)
|
|
304
|
+
if (this.courseTargetLocaleId && vtt.locale_id === this.courseTargetLocaleId) {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
// Also try matching by locale.code
|
|
308
|
+
const vttLocale = _.get(vtt, 'locale.code', '').toLowerCase()
|
|
309
|
+
return vttLocale && this.isAllowedCaptionLanguage(vttLocale) &&
|
|
310
|
+
vttLocale === this.courseCurrentLocale
|
|
311
|
+
})
|
|
312
|
+
// Notes can be in different locations:
|
|
313
|
+
// - vtt.metadata.notes (FileAssetMap metadata)
|
|
314
|
+
// - vtt.notes (FileAssetMapResource exposes this)
|
|
315
|
+
// - vtt.asset.metadata.notes (FileAsset metadata - where translation stores them)
|
|
316
|
+
notes = _.get(currentVtt, 'metadata.notes') ||
|
|
317
|
+
_.get(currentVtt, 'notes') ||
|
|
318
|
+
_.get(currentVtt, 'asset.metadata.notes')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Fallback to video asset notes
|
|
322
|
+
if (!notes || !this.hasWords(notes)) {
|
|
323
|
+
notes = _.get(file, 'asset.metadata.notes')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (notes && this.hasWords(notes)) {
|
|
327
|
+
result.push(notes)
|
|
184
328
|
}
|
|
185
329
|
}
|
|
186
330
|
}
|
|
@@ -192,36 +336,123 @@ export default {
|
|
|
192
336
|
linkedPlaylist() {
|
|
193
337
|
const playlist = _.cloneDeep(this.block.metadata.config.playlist)
|
|
194
338
|
for (const index in playlist) {
|
|
339
|
+
// Build comprehensive track list from multiple sources
|
|
340
|
+
const allTracks = []
|
|
341
|
+
const addedSrcs = new Set() // Track added URLs to avoid duplicates
|
|
342
|
+
const addedLangs = new Set() // Track added languages to avoid duplicate srclang keys
|
|
343
|
+
|
|
344
|
+
// 1. FIRST: Resolve video files and get linked captions BEFORE transforming sources
|
|
345
|
+
const resolvedVideoFiles = []
|
|
195
346
|
for (const sourceIndex in playlist[index].sources) {
|
|
196
|
-
let
|
|
347
|
+
let videoFile = this.resolveAsset(
|
|
197
348
|
playlist[index].sources[sourceIndex]
|
|
198
349
|
)
|
|
350
|
+
resolvedVideoFiles.push(videoFile)
|
|
199
351
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
352
|
+
// Get ALL linked captions from this video file (adds to block.assets)
|
|
353
|
+
const linkedCaptions = this.getAllLinkedCaptions(videoFile)
|
|
354
|
+
|
|
355
|
+
for (const vtt of linkedCaptions) {
|
|
356
|
+
// Get the src - may be directly available or need to extract from asset
|
|
357
|
+
const src = _.get(vtt, 'asset.public_url', '')
|
|
358
|
+
|
|
359
|
+
// Skip if no src or already added
|
|
360
|
+
if (!src || addedSrcs.has(src)) {
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const srclang = _.get(vtt, 'locale.code') || this.courseSourceLocale
|
|
365
|
+
|
|
366
|
+
// Skip if this language was already added (avoid duplicate keys)
|
|
367
|
+
if (addedLangs.has(srclang)) {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// FILTER: Only include tracks that match the course's language(s)
|
|
372
|
+
// - Original courses: show only the course's own language
|
|
373
|
+
// - Translated courses: show source language + target language
|
|
374
|
+
if (!this.isAllowedCaptionLanguage(srclang)) {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const label = this.getLabelForLanguage(srclang) || _.get(vtt, 'locale.name') || srclang
|
|
204
379
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
380
|
+
allTracks.push({
|
|
381
|
+
src: src,
|
|
382
|
+
kind: 'captions',
|
|
383
|
+
srclang: srclang,
|
|
384
|
+
label: label,
|
|
385
|
+
default: false,
|
|
386
|
+
})
|
|
387
|
+
addedSrcs.add(src)
|
|
388
|
+
addedLangs.add(srclang)
|
|
210
389
|
}
|
|
211
390
|
}
|
|
212
391
|
|
|
392
|
+
// 2. Process manually-added tracks from playlist[x].tracks[]
|
|
213
393
|
for (const trackIndex in playlist[index].tracks) {
|
|
214
394
|
let file = this.resolveAsset(
|
|
215
395
|
playlist[index].tracks[trackIndex]
|
|
216
396
|
)
|
|
217
397
|
|
|
218
|
-
playlist[index].tracks[trackIndex]
|
|
219
|
-
|
|
398
|
+
const trackObj = playlist[index].tracks[trackIndex]
|
|
399
|
+
const src = _.get(file, 'asset.public_url', '')
|
|
400
|
+
|
|
401
|
+
// Skip if no src or already added
|
|
402
|
+
if (!src || addedSrcs.has(src)) {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const srclang = trackObj.srclang || this.courseSourceLocale
|
|
407
|
+
|
|
408
|
+
// Skip if this language was already added (avoid duplicate keys)
|
|
409
|
+
if (addedLangs.has(srclang)) {
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// FILTER: Only include tracks that match source or target language
|
|
414
|
+
// For translated courses, only show source language and target language tracks
|
|
415
|
+
// For non-translated courses, this filter is skipped and all tracks are shown
|
|
416
|
+
if (this.isTranslatedCourse && !this.isAllowedCaptionLanguage(srclang)) {
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const label = trackObj.label || this.getLabelForLanguage(srclang)
|
|
421
|
+
|
|
422
|
+
allTracks.push({
|
|
423
|
+
src: src,
|
|
220
424
|
kind: 'captions',
|
|
221
|
-
srclang:
|
|
222
|
-
|
|
425
|
+
srclang: srclang,
|
|
426
|
+
label: label,
|
|
427
|
+
default: false,
|
|
428
|
+
})
|
|
429
|
+
addedSrcs.add(src)
|
|
430
|
+
addedLangs.add(srclang)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 3. NOW transform video sources to player format
|
|
434
|
+
for (const sourceIndex in playlist[index].sources) {
|
|
435
|
+
const file = resolvedVideoFiles[sourceIndex]
|
|
436
|
+
playlist[index].sources[sourceIndex] = {
|
|
437
|
+
src: _.get(file, 'asset.public_url', ''),
|
|
438
|
+
type: _.get(file, 'asset.metadata.mime', ''),
|
|
223
439
|
}
|
|
224
440
|
}
|
|
441
|
+
|
|
442
|
+
// 4. Sort tracks: course source language first
|
|
443
|
+
allTracks.sort((a, b) => {
|
|
444
|
+
if (a.srclang === this.courseSourceLocale) return -1
|
|
445
|
+
if (b.srclang === this.courseSourceLocale) return 1
|
|
446
|
+
return 0
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
// 5. Set default on first track (should be source language)
|
|
450
|
+
if (allTracks.length > 0) {
|
|
451
|
+
allTracks[0].default = true
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 6. Replace tracks array with processed tracks
|
|
455
|
+
playlist[index].tracks = allTracks
|
|
225
456
|
for (const adIndex in playlist[index].ads) {
|
|
226
457
|
for (const adSourceIndex in playlist[index].ads[adIndex]
|
|
227
458
|
.sources) {
|
|
@@ -354,6 +585,9 @@ export default {
|
|
|
354
585
|
},
|
|
355
586
|
},
|
|
356
587
|
},
|
|
588
|
+
mounted() {
|
|
589
|
+
// Video block mounted - locale filtering applied via isTranslatedCourse check
|
|
590
|
+
},
|
|
357
591
|
beforeMount() {
|
|
358
592
|
// Apply the default config
|
|
359
593
|
if (_.isEmpty(this.block.metadata.config)) {
|
|
@@ -400,6 +634,48 @@ export default {
|
|
|
400
634
|
async onBeforeSave() {
|
|
401
635
|
this.block.body = 'video'
|
|
402
636
|
},
|
|
637
|
+
/**
|
|
638
|
+
* Get ALL linked VTT caption files (supports multiple languages)
|
|
639
|
+
* @param {Object} file - The video file object with linked_assets
|
|
640
|
+
* @returns {Array} Array of VTT files with locale information
|
|
641
|
+
*/
|
|
642
|
+
getAllLinkedCaptions(file) {
|
|
643
|
+
// Get ALL VTT files from linked_assets (not just first one)
|
|
644
|
+
const linkedCaptions = _.filter(
|
|
645
|
+
_.get(file, 'asset.linked_assets', []),
|
|
646
|
+
(f) => _.get(f, 'asset.metadata.extension', '') === 'vtt'
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
// Add locale information to each VTT AND add to block.assets
|
|
650
|
+
return linkedCaptions.map(vtt => {
|
|
651
|
+
// CRITICAL: Add to block.assets so resolveAsset can find it
|
|
652
|
+
const foundAsset = this.block.assets.find((a) => {
|
|
653
|
+
return a.file_asset_id === vtt.file_asset_id
|
|
654
|
+
})
|
|
655
|
+
if (!foundAsset) {
|
|
656
|
+
this.block.assets.push(_.cloneDeep(vtt))
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Try to get locale - priority: API response > store lookup > null
|
|
660
|
+
let locale = vtt.locale || null
|
|
661
|
+
|
|
662
|
+
// If no locale from API but we have locale_id, try store lookup
|
|
663
|
+
if (!locale && vtt.locale_id && this.$store?.getters?.['locales/getById']) {
|
|
664
|
+
locale = this.$store.getters['locales/getById'](vtt.locale_id)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
...vtt,
|
|
669
|
+
locale: locale
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get single linked caption (backward compatibility)
|
|
676
|
+
* @param {Object} file - The video file object
|
|
677
|
+
* @returns {Object|null} First VTT file found
|
|
678
|
+
*/
|
|
403
679
|
getLinkedCaptions(file) {
|
|
404
680
|
// Check to see if the video source has a linked asset and it's a vtt file
|
|
405
681
|
// Prefer the most recently linked VTT (last match), not the oldest.
|
|
@@ -431,6 +707,69 @@ export default {
|
|
|
431
707
|
return linkedCaption || null
|
|
432
708
|
},
|
|
433
709
|
|
|
710
|
+
/**
|
|
711
|
+
* TODO:find a better place for this to live
|
|
712
|
+
* Get human-readable language label for a language code
|
|
713
|
+
* @param {string} langCode - ISO language code (e.g., 'en-US')
|
|
714
|
+
* @returns {string} Language name
|
|
715
|
+
*/
|
|
716
|
+
getLabelForLanguage(langCode) {
|
|
717
|
+
// Map common language codes to display names
|
|
718
|
+
const labels = {
|
|
719
|
+
'en': 'English',
|
|
720
|
+
'en-US': 'English',
|
|
721
|
+
'en-GB': 'English (UK)',
|
|
722
|
+
'es': 'Spanish',
|
|
723
|
+
'es-ES': 'Spanish',
|
|
724
|
+
'es-MX': 'Spanish (Mexico)',
|
|
725
|
+
'fr': 'French',
|
|
726
|
+
'fr-FR': 'French',
|
|
727
|
+
'fr-CA': 'French (Canada)',
|
|
728
|
+
'de': 'German',
|
|
729
|
+
'de-DE': 'German',
|
|
730
|
+
'it': 'Italian',
|
|
731
|
+
'it-IT': 'Italian',
|
|
732
|
+
'pt': 'Portuguese',
|
|
733
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
734
|
+
'pt-PT': 'Portuguese (Portugal)',
|
|
735
|
+
'zh': 'Chinese',
|
|
736
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
737
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
738
|
+
'ja': 'Japanese',
|
|
739
|
+
'ja-JP': 'Japanese',
|
|
740
|
+
'ko': 'Korean',
|
|
741
|
+
'ko-KR': 'Korean',
|
|
742
|
+
'ru': 'Russian',
|
|
743
|
+
'ru-RU': 'Russian',
|
|
744
|
+
'ar': 'Arabic',
|
|
745
|
+
'ar-SA': 'Arabic',
|
|
746
|
+
'hi': 'Hindi',
|
|
747
|
+
'hi-IN': 'Hindi',
|
|
748
|
+
}
|
|
749
|
+
return labels[langCode] || langCode
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Check if a caption language is allowed based on course source/target languages
|
|
754
|
+
* Since allowedCaptionLocales already contains both full codes (en-us) and base codes (en),
|
|
755
|
+
* we just need to check if the language or its base is in the Set.
|
|
756
|
+
* @param {string} lang - The language code to check (e.g., 'en-US', 'PT-BR')
|
|
757
|
+
* @returns {boolean} - True if the language is allowed
|
|
758
|
+
*/
|
|
759
|
+
isAllowedCaptionLanguage(lang) {
|
|
760
|
+
if (!lang) {
|
|
761
|
+
// Allow tracks with no language set (legacy/default tracks)
|
|
762
|
+
return true
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const langLower = lang.toLowerCase()
|
|
766
|
+
const langBase = langLower.split('-')[0]
|
|
767
|
+
|
|
768
|
+
// Check if the full language code or its base is in allowedCaptionLocales
|
|
769
|
+
return this.allowedCaptionLocales.has(langLower) ||
|
|
770
|
+
this.allowedCaptionLocales.has(langBase)
|
|
771
|
+
},
|
|
772
|
+
|
|
434
773
|
/**
|
|
435
774
|
* Check if the given text has words, omitting HTML tags and HTML entities
|
|
436
775
|
* @param {string} text - The text to check
|
|
@@ -310,8 +310,8 @@ export default {
|
|
|
310
310
|
}
|
|
311
311
|
if (_.isEmpty(this.block.metadata.config.display)) {
|
|
312
312
|
this.block.metadata.config.display = {
|
|
313
|
-
show_title:
|
|
314
|
-
show_background:
|
|
313
|
+
show_title: true,
|
|
314
|
+
show_background: true,
|
|
315
315
|
round_icon: false,
|
|
316
316
|
italic_icon: false,
|
|
317
317
|
large_icon: false,
|
|
@@ -326,7 +326,7 @@ export default {
|
|
|
326
326
|
methods: {
|
|
327
327
|
onAddElement() {
|
|
328
328
|
const defaultObject = {
|
|
329
|
-
icon: '',
|
|
329
|
+
icon: 'mdi-star',
|
|
330
330
|
fileConfig: {},
|
|
331
331
|
iconImage: false,
|
|
332
332
|
title: '',
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-container>
|
|
3
|
+
<BaseContentBlockSettings
|
|
4
|
+
v-model="block.metadata.config"
|
|
5
|
+
:disabled="render"
|
|
6
|
+
></BaseContentBlockSettings>
|
|
7
|
+
<v-divider class="my-4 primary"></v-divider>
|
|
3
8
|
<ImageAssetSettings
|
|
4
9
|
v-model="block.metadata.config"
|
|
5
10
|
:assets.sync="block.assets"
|
|
@@ -13,11 +18,14 @@
|
|
|
13
18
|
import _ from 'lodash'
|
|
14
19
|
import BaseContentSettings from '~/components/Content/Settings/BaseContentSettings.js'
|
|
15
20
|
import ImageAssetSettings from '~/components/Content/Settings/ImageAssetSettings.vue'
|
|
21
|
+
import BaseContentBlockSettings from '~/components/Content/Settings/BaseContentBlockSettings.vue'
|
|
22
|
+
import Uuid from '~/helpers/Uuid'
|
|
16
23
|
|
|
17
24
|
export default {
|
|
18
25
|
name: 'ImageSettings',
|
|
19
26
|
components: {
|
|
20
27
|
ImageAssetSettings,
|
|
28
|
+
BaseContentBlockSettings,
|
|
21
29
|
},
|
|
22
30
|
extends: BaseContentSettings,
|
|
23
31
|
props: {
|
|
@@ -40,6 +48,24 @@ export default {
|
|
|
40
48
|
if (_.isEmpty(this.block.metadata.config)) {
|
|
41
49
|
this.block.metadata.config = {}
|
|
42
50
|
}
|
|
51
|
+
// If the block is brand new and the title is missing then pre-set the word 'Image' to it
|
|
52
|
+
if (
|
|
53
|
+
!Uuid.test(this.block.id) &&
|
|
54
|
+
_.isEmpty(this.block.metadata.config.title)
|
|
55
|
+
) {
|
|
56
|
+
this.block.metadata.config.title = this.$t(
|
|
57
|
+
'windward.core.shared.content_blocks.title.image'
|
|
58
|
+
)
|
|
59
|
+
} else if (_.isEmpty(this.block.metadata.config.title)) {
|
|
60
|
+
// Otherwise make sure the title key at least exists
|
|
61
|
+
this.block.metadata.config.title = ''
|
|
62
|
+
}
|
|
63
|
+
if (!_.isBoolean(this.block.metadata.config.display_title)) {
|
|
64
|
+
this.$set(this.block.metadata.config, 'display_title', true)
|
|
65
|
+
}
|
|
66
|
+
if (_.isEmpty(this.block.metadata.config.instructions)) {
|
|
67
|
+
this.block.metadata.config.instructions = ''
|
|
68
|
+
}
|
|
43
69
|
if (_.isEmpty(this.block.metadata.config.asset)) {
|
|
44
70
|
this.block.metadata.config.asset = null
|
|
45
71
|
}
|
|
@@ -24,10 +24,25 @@
|
|
|
24
24
|
</p>
|
|
25
25
|
<TextEditor
|
|
26
26
|
id="block-settings-sample-response"
|
|
27
|
+
ref="sampleResponseEditor"
|
|
27
28
|
v-model="block.metadata.config.sample_response"
|
|
28
29
|
:height="200"
|
|
30
|
+
:rules="sampleResponseRules"
|
|
29
31
|
:disabled="render"
|
|
30
32
|
></TextEditor>
|
|
33
|
+
<v-row class="pt-4">
|
|
34
|
+
<v-col cols="12">
|
|
35
|
+
<v-switch
|
|
36
|
+
v-model="block.metadata.config.ai_mode_for_student"
|
|
37
|
+
:label="
|
|
38
|
+
$t(
|
|
39
|
+
'windward.core.components.settings.open_response.ai_mode_for_student'
|
|
40
|
+
)
|
|
41
|
+
"
|
|
42
|
+
:disabled="render"
|
|
43
|
+
></v-switch>
|
|
44
|
+
</v-col>
|
|
45
|
+
</v-row>
|
|
31
46
|
<p class="pt-4">
|
|
32
47
|
{{
|
|
33
48
|
$t(
|
|
@@ -84,6 +99,34 @@ export default {
|
|
|
84
99
|
course: 'course/get',
|
|
85
100
|
currentContent: 'content/get',
|
|
86
101
|
}),
|
|
102
|
+
sampleResponseRules() {
|
|
103
|
+
return [
|
|
104
|
+
(value) => {
|
|
105
|
+
if (!this.block?.metadata?.config?.ai_mode_for_student) {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const text = this.stripHtmlTags(value)
|
|
110
|
+
return (
|
|
111
|
+
!_.isEmpty(text) ||
|
|
112
|
+
this.$t(
|
|
113
|
+
'windward.core.components.settings.open_response.validation.sample_response_required_ai_mode'
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
watch: {
|
|
121
|
+
'block.metadata.config.ai_mode_for_student'(newValue, oldValue) {
|
|
122
|
+
if (newValue === oldValue) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.$nextTick(() => {
|
|
127
|
+
this.$refs.sampleResponseEditor?.validate?.()
|
|
128
|
+
})
|
|
129
|
+
},
|
|
87
130
|
},
|
|
88
131
|
beforeMount() {
|
|
89
132
|
if (_.isEmpty(this.block)) {
|
|
@@ -117,9 +160,21 @@ export default {
|
|
|
117
160
|
if (_.isEmpty(this.block.metadata.config.starting_text)) {
|
|
118
161
|
this.block.metadata.config.starting_text = ''
|
|
119
162
|
}
|
|
163
|
+
if (!_.isBoolean(this.block.metadata.config.ai_mode_for_student)) {
|
|
164
|
+
this.$set(this.block.metadata.config, 'ai_mode_for_student', false)
|
|
165
|
+
}
|
|
120
166
|
},
|
|
121
167
|
mounted() {},
|
|
122
168
|
methods: {
|
|
169
|
+
stripHtmlTags(body) {
|
|
170
|
+
if (typeof body !== 'string') {
|
|
171
|
+
return ''
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let text = body.replace(/ /gi, ' ')
|
|
175
|
+
text = text.replace(/\u00A0/g, ' ')
|
|
176
|
+
return text.replace(/(<([^>]+)>)/gi, '').trim()
|
|
177
|
+
},
|
|
123
178
|
onApplyGeneratedBlock(payload = {}) {
|
|
124
179
|
if (_.isEmpty(payload)) {
|
|
125
180
|
return
|
|
@@ -136,6 +191,10 @@ export default {
|
|
|
136
191
|
_.get(config, 'sample_response', '') ?? ''
|
|
137
192
|
this.block.metadata.config.starting_text =
|
|
138
193
|
_.get(config, 'starting_text', '') ?? ''
|
|
194
|
+
if (_.has(config, 'ai_mode_for_student')) {
|
|
195
|
+
this.block.metadata.config.ai_mode_for_student =
|
|
196
|
+
config.ai_mode_for_student
|
|
197
|
+
}
|
|
139
198
|
|
|
140
199
|
if (_.has(config, 'display_title')) {
|
|
141
200
|
this.block.metadata.config.display_title = config.display_title
|