+ % if download_video_link or public_sharing_enabled:
+
+
${_('Video')}
+ % if download_video_link:
+
+
+ ${_('Download video file')}
+
+ % endif
+ % if download_video_link and public_sharing_enabled:
+
+ % endif
+ % if sharing_sites_info:
+
+
+
-
-
${_("Share on {site}").format(site=sharing_site_info['name'])}
-
- % endfor
-
-
-
+ % endif
+
+ % endif
+ % if track:
+
+
${_('Transcripts')}
+ % if transcript_download_format:
+
+ % for item in transcript_download_formats_list:
+ -
+
+ <% dname = _("Download {file}").format(file=item['display_name']) %>
+ ${dname}
+
+ % endfor
+
+ % else:
+
${_('Download transcript')}
% endif
- % endif
- % if track:
-
-
${_('Transcripts')}
- % if transcript_download_format:
-
- % for item in transcript_download_formats_list:
- -
-
- <% dname = _("Download {file}").format(file=item['display_name']) %>
- ${dname}
-
- % endfor
-
- % else:
-
${_('Download transcript')}
+ % endif
+ % if handout:
+
+ % endif
+ % if branding_info:
+
+
${branding_info['logo_tag']}
+
+
% endif
% endif
- % if handout:
-
- % endif
- % if branding_info:
-
-
${branding_info['logo_tag']}
-
+ % if transcript_feedback_enabled:
+
+
${_('How is the transcript quality ?')}
+
% endif
- % endif
% if cdn_eval:
diff --git a/openedx/core/djangoapps/video_config/toggles.py b/openedx/core/djangoapps/video_config/toggles.py
index 6552f7366417..0af57582c5c3 100644
--- a/openedx/core/djangoapps/video_config/toggles.py
+++ b/openedx/core/djangoapps/video_config/toggles.py
@@ -15,3 +15,14 @@
PUBLIC_VIDEO_SHARE = CourseWaffleFlag(
f'{WAFFLE_FLAG_NAMESPACE}.public_video_share', __name__
)
+
+# .. toggle_name: video_config.transcript_feedback
+# .. toggle_implementation: CourseWaffleFlag
+# .. toggle_default: False
+# .. toggle_description: Gates access to the transcript feedback widget feature.
+# .. toggle_use_cases: temporary, opt_in
+# .. toggle_creation_date: 2023-05-10
+# .. toggle_target_removal_date: None
+TRANSCRIPT_FEEDBACK = CourseWaffleFlag(
+ f'{WAFFLE_FLAG_NAMESPACE}.transcript_feedback', __name__
+)
diff --git a/xmodule/assets/video/_display.scss b/xmodule/assets/video/_display.scss
index 467c5960751e..2aea2bb24612 100644
--- a/xmodule/assets/video/_display.scss
+++ b/xmodule/assets/video/_display.scss
@@ -89,19 +89,14 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
margin: 1em 0 0;
}
- .wrapper-downloads {
- @include media-breakpoint-up(md) {
- display: flex;
- }
-
- .hd {
- margin: 0;
- }
+ .wrapper-video-bottom-section {
+ display: flex;
.wrapper-download-video,
.wrapper-download-transcripts,
.wrapper-handouts,
- .branding {
+ .branding,
+ .wrapper-transcript-feedback {
flex: 1;
margin-top: $baseline;
@@ -109,6 +104,16 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
vertical-align: top;
}
+ }
+
+ .wrapper-downloads {
+ @include media-breakpoint-up(md) {
+ display: flex;
+ }
+
+ .hd {
+ margin: 0;
+ }
.wrapper-download-video {
.video-sources {
@@ -152,6 +157,22 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark
vertical-align: middle;
}
}
+
+ }
+
+ .wrapper-transcript-feedback {
+ .transcript-feedback-buttons {
+ display: flex;
+ }
+ .transcript-feedback-btn-wrapper {
+ margin-right: 10px;
+ }
+ .thumbs-up-btn,
+ .thumbs-down-btn {
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ }
}
.video-wrapper {
diff --git a/xmodule/js/fixtures/video_all.html b/xmodule/js/fixtures/video_all.html
index bd166720589a..280fc4db35f4 100644
--- a/xmodule/js/fixtures/video_all.html
+++ b/xmodule/js/fixtures/video_all.html
@@ -33,48 +33,49 @@
-
Downloads and transcripts
-
diff --git a/xmodule/js/fixtures/video_transcript_feedback.html b/xmodule/js/fixtures/video_transcript_feedback.html
new file mode 100644
index 000000000000..3ba5f0dae785
--- /dev/null
+++ b/xmodule/js/fixtures/video_transcript_feedback.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
How is the transcript quality ?
+
+
+
+
+
+
+
+
diff --git a/xmodule/js/karma_runner_webpack.js b/xmodule/js/karma_runner_webpack.js
index bb29b1c6f2e8..6dfee7db9e7d 100644
--- a/xmodule/js/karma_runner_webpack.js
+++ b/xmodule/js/karma_runner_webpack.js
@@ -46,6 +46,7 @@ import './spec/video/initialize_spec.js';
import './spec/video/iterator_spec.js';
import './spec/video/resizer_spec.js';
import './spec/video/sjson_spec.js';
+import './spec/video/social_share_spec.js';
import './spec/video/video_autoadvance_spec.js';
import './spec/video/video_bumper_spec.js';
import './spec/video/video_caption_spec.js';
@@ -66,9 +67,9 @@ import './spec/video/video_save_state_plugin_spec.js';
import './spec/video/video_skip_control_spec.js';
import './spec/video/video_speed_control_spec.js';
import './spec/video/video_storage_spec.js';
+import './spec/video/video_transcript_feedback_spec.js';
import './spec/video/video_volume_control_spec.js';
import './spec/time_spec.js';
-import './spec/video/social_share_spec.js';
// overwrite the loaded method and manually start the karma after a delay
// Somehow the code initialized in jQuery's onready doesn't get called before karma auto starts
diff --git a/xmodule/js/spec/helper.js b/xmodule/js/spec/helper.js
index 978de6785ed9..d7a019e07caf 100644
--- a/xmodule/js/spec/helper.js
+++ b/xmodule/js/spec/helper.js
@@ -180,6 +180,38 @@
return {};
} else if (settings.url === '/save_user_state') {
return {success: true};
+ } else if (settings.url.match(/.+video-transcript.+$/)) {
+ if (settings.url.match(/.+&video_uuid=notAIGenerated/)) {
+ return settings.success(null);
+ }
+ if (settings.url.match(/.+&video_uuid=inProgress/)) {
+ return settings.success({
+ status: 'In Progress'
+ });
+ }
+ if (settings.url.match(/.+&video_uuid=error/)) {
+ return settings.error();
+ }
+ return settings.success({
+ status: 'Completed'
+ });
+ } else if (settings.url.match(/.+transcript-feedback.+$/) && settings.type === 'GET') {
+ if (settings.url.match(/.+&video_uuid=error.+$/)) {
+ return settings.error();
+ }
+ if (settings.url.match(/.+&video_uuid=negative.+$/)) {
+ return settings.success({
+ value: false
+ });
+ }
+ if (settings.url.match(/.+&video_uuid=none.+$/)) {
+ return settings.success(null);
+ }
+ return settings.success({
+ value: true
+ });
+ } else if (settings.url.match(/.+transcript-feedback.+$/) && settings.type === 'POST') {
+ return settings.success(settings.data.value !== null ? { value: settings.data.value } : null);
} else if (settings.url.match(new RegExp(jasmine.getFixtures().fixturesPath + '.+', 'g'))) {
return origAjax(settings);
} else {
diff --git a/xmodule/js/spec/video/video_caption_spec.js b/xmodule/js/spec/video/video_caption_spec.js
index 6bd9a307dc88..58fcc1cce99d 100644
--- a/xmodule/js/spec/video/video_caption_spec.js
+++ b/xmodule/js/spec/video/video_caption_spec.js
@@ -833,7 +833,7 @@
});
});
- msg = 'on succes: language menu is rendered if translations available';
+ msg = 'on success: language menu is rendered if translations available';
it(msg, function() {
state.config.transcriptLanguages = {
en: 'English',
@@ -853,7 +853,7 @@
});
});
- msg = 'on succes: language menu isn\'t rendered if translations unavailable';
+ msg = 'on success: language menu isn\'t rendered if translations unavailable';
it(msg, function() {
state.config.transcriptLanguages = {
en: 'English',
diff --git a/xmodule/js/spec/video/video_transcript_feedback_spec.js b/xmodule/js/spec/video/video_transcript_feedback_spec.js
new file mode 100644
index 000000000000..234f92138487
--- /dev/null
+++ b/xmodule/js/spec/video/video_transcript_feedback_spec.js
@@ -0,0 +1,271 @@
+(function() {
+ // eslint-disable-next-line lines-around-directive
+ 'use strict';
+
+ describe('VideoTranscriptFeedback', function() {
+ var state;
+ var videoId = "365b710a-6dd6-11ee-b962-0242ac120002";
+ var userId = 1;
+ var currentLanguage = "en";
+ var getAITranscriptUrl = '/video-transcript' + '?transcript_language=' + currentLanguage + '&video_uuid=' + videoId;
+ var getTranscriptFeedbackUrl = '/transcript-feedback' + '?transcript_language=' + currentLanguage + '&video_uuid=' + videoId + '&user_id=' + userId;
+ var sendTranscriptFeedbackUrl = '/transcript-feedback/';
+
+ beforeEach(function() {
+ state = jasmine.initializePlayer('video_transcript_feedback.html');
+ });
+
+ afterEach(function() {
+ $('source').remove();
+ state.storage.clear();
+ if (state.videoPlayer) {
+ state.videoPlayer.destroy();
+ }
+ });
+
+ describe('initialize', function() {
+ it('instantiates widget and handlers along with necessary data', function() {
+ spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callFake(function() {
+ return true;
+ });
+ spyOn(state.videoTranscriptFeedback, 'bindHandlers').and.callFake(function() {
+ return true;
+ });
+ state.videoTranscriptFeedback.initialize();
+
+ expect(state.videoTranscriptFeedback.videoId).toEqual(videoId);
+ expect(state.videoTranscriptFeedback.userId).toEqual(userId);
+ expect(state.videoTranscriptFeedback.currentTranscriptLanguage).toEqual(currentLanguage);
+ expect(state.videoTranscriptFeedback.loadAndSetVisibility).toHaveBeenCalled();
+ expect(state.videoTranscriptFeedback.bindHandlers).toHaveBeenCalled();
+ });
+ });
+
+ describe('should show widget', function() {
+ it('checks if transcript was AI generated', function() {
+ spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callThrough();
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+
+ var getAITranscriptCall = $.ajax.calls.all().find(function(call) {
+ return call.args[0].url.match(/.+video-transcript.+$/);
+ });
+
+ expect(state.videoTranscriptFeedback.loadAndSetVisibility).toHaveBeenCalled();
+ expect(getAITranscriptCall.args[0].url).toEqual(state.videoTranscriptFeedback.aiTranslationsUrl + getAITranscriptUrl);
+ expect(getAITranscriptCall.args[0].type).toEqual('GET');
+ expect(getAITranscriptCall.args[0].async).toEqual(false);
+ expect(getAITranscriptCall.args[0].success).toEqual(jasmine.any(Function));
+ expect(getAITranscriptCall.args[0].error).toEqual(jasmine.any(Function));
+ });
+ it('shows widget if transcript is AI generated', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ expect($('.wrapper-transcript-feedback')[0]).toExist();
+ });
+ it('hides widget if transcript is not AI generated', function() {
+ state.videoTranscriptFeedback.videoId = 'notAIGenerated';
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ expect($('.wrapper-transcript-feedback')[0]).toExist();
+ expect($('.wrapper-transcript-feedback')[0].style.display).toEqual('none');
+ });
+ it('hides widget if transcript is AI generated but is still in progress', function() {
+ state.videoTranscriptFeedback.videoId = 'inProgress';
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ expect($('.wrapper-transcript-feedback')[0]).toExist();
+ expect($('.wrapper-transcript-feedback')[0].style.display).toEqual('none');
+ });
+ it('hides widget if query for transcript AI generated fails', function() {
+ state.videoTranscriptFeedback.videoId = 'error';
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ expect($('.wrapper-transcript-feedback')[0]).toExist();
+ expect($('.wrapper-transcript-feedback')[0].style.display).toEqual('none');
+ });
+ it('checks if feedback exists for AI generated transcript', function() {
+ spyOn(state.videoTranscriptFeedback, 'getFeedbackForCurrentTranscript').and.callThrough();
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+
+ var getTranscriptFeedbackCall = $.ajax.calls.all().find(function(call) {
+ return call.args[0].url.match(/.+transcript-feedback.+$/);
+ });
+
+ expect(state.videoTranscriptFeedback.getFeedbackForCurrentTranscript).toHaveBeenCalled();
+ expect(getTranscriptFeedbackCall.args[0].url).toEqual(state.videoTranscriptFeedback.aiTranslationsUrl + getTranscriptFeedbackUrl);
+ expect(getTranscriptFeedbackCall.args[0].type).toEqual('GET');
+ expect(getTranscriptFeedbackCall.args[0].success).toEqual(jasmine.any(Function));
+ expect(getTranscriptFeedbackCall.args[0].error).toEqual(jasmine.any(Function));
+ });
+ });
+
+ describe('get feedback for current transcript', function() {
+ it('marks thumbs up button if feedback exists and it is positive', function() {
+ state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(true);
+ });
+ it('marks thumbs down button if feedback exists and it is negative', function() {
+ state.videoTranscriptFeedback.videoId = 'negative';
+ state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
+
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(false);
+ });
+ it('marks thumbs up buttons as empty if feedback does not exist', function() {
+ state.videoTranscriptFeedback.videoId = 'none';
+ state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
+
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(null);
+ });
+ it('marks thumbs up buttons as empty if query fails', function() {
+ state.videoTranscriptFeedback.videoId = 'error';
+ state.videoTranscriptFeedback.getFeedbackForCurrentTranscript();
+
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(null);
+ });
+ });
+
+ describe('onHideLanguageMenu', function() {
+ it('calls loadAndSetVisibility if language changed', function() {
+ state.videoTranscriptFeedback.currentTranscriptLanguage = 'es';
+ spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callThrough();
+ state.el.trigger('language_menu:hide', {
+ id: 'id',
+ code: 'code',
+ language: 'en',
+ duration: 10
+ });
+ expect(state.videoTranscriptFeedback.loadAndSetVisibility).toHaveBeenCalled();
+ });
+ it('does not call loadAndSetVisibility if language did not change', function() {
+ state.videoTranscriptFeedback.currentTranscriptLanguage = 'en';
+ spyOn(state.videoTranscriptFeedback, 'loadAndSetVisibility').and.callThrough();
+ state.el.trigger('language_menu:hide', {
+ id: 'id',
+ code: 'code',
+ language: 'en',
+ duration: 10
+ });
+ expect(state.videoTranscriptFeedback.loadAndSetVisibility).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('clicking on thumbs up button', function() {
+ it('sends positive feedback if there is no current feedback', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = undefined;
+ spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
+ return true;
+ });
+ var thumbsUpButton = $('.thumbs-up-btn');
+ thumbsUpButton.trigger('click');
+ expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(true);
+ });
+ it('sends empty feedback if there is a current positive feedback', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = true;
+ spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
+ return true;
+ });
+ var thumbsUpButton = $('.thumbs-up-btn');
+ thumbsUpButton.trigger('click');
+ expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('clicking on thumbs down button', function() {
+ it('sends negative feedback if there is no current feedback', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = undefined;
+ spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
+ return true;
+ });
+ var thumbsDownButton = $('.thumbs-down-btn');
+ thumbsDownButton.trigger('click');
+ expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(false);
+ });
+ it('sends empty feedback if there is a current negative feedback', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = false;
+ spyOn(state.videoTranscriptFeedback, 'sendFeedbackForCurrentTranscript').and.callFake(function() {
+ return true;
+ });
+ var thumbsDownButton = $('.thumbs-down-btn');
+ thumbsDownButton.trigger('click');
+ expect(state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('calling send transcript feedback', function() {
+ it('sends proper request to ai translation service', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = undefined;
+ state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(true);
+ var sendTranscriptFeedbackCall = $.ajax.calls.all().find(function(call) {
+ return call.args[0].url.match(/.+transcript-feedback.+$/) && call.args[0].type === 'POST';
+ });
+
+ expect(sendTranscriptFeedbackCall.args[0].url).toEqual(state.videoTranscriptFeedback.aiTranslationsUrl + sendTranscriptFeedbackUrl);
+ expect(sendTranscriptFeedbackCall.args[0].type).toEqual('POST');
+ expect(sendTranscriptFeedbackCall.args[0].dataType).toEqual('json');
+ expect(sendTranscriptFeedbackCall.args[0].data).toEqual({
+ transcript_language: currentLanguage,
+ video_uuid: videoId,
+ user_id: userId,
+ value: true,
+ });
+ expect(sendTranscriptFeedbackCall.args[0].success).toEqual(jasmine.any(Function));
+ expect(sendTranscriptFeedbackCall.args[0].error).toEqual(jasmine.any(Function));
+ });
+ it('marks thumbs up button as selected if response is positive', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = undefined;
+ state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(true);
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(true);
+ });
+ it('marks thumbs down button as selected if response is negative', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = undefined;
+ state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(false);
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(false);
+ });
+ it('unselects thumbs buttons if response is empty', function() {
+ state.videoTranscriptFeedback.loadAndSetVisibility();
+ state.videoTranscriptFeedback.currentFeedback = true;
+ state.videoTranscriptFeedback.sendFeedbackForCurrentTranscript(null);
+ var thumbsUpIcon = $('.thumbs-up-icon')[0];
+ var thumbsDownIcon = $('.thumbs-down-icon')[0];
+
+ expect(thumbsUpIcon.classList).toContain('fa-thumbs-o-up');
+ expect(thumbsDownIcon.classList).toContain('fa-thumbs-o-down');
+ expect(state.videoTranscriptFeedback.currentFeedback).toEqual(null);
+ });
+ });
+ });
+}).call(this);
diff --git a/xmodule/js/src/video/037_video_transcript_feedback.js b/xmodule/js/src/video/037_video_transcript_feedback.js
new file mode 100644
index 000000000000..e369c1c11153
--- /dev/null
+++ b/xmodule/js/src/video/037_video_transcript_feedback.js
@@ -0,0 +1,247 @@
+(function(define) {
+ // VideoTranscriptFeedbackHandler module.
+
+ 'use strict';
+
+ define('video/037_video_caption.js', ['underscore'],
+ function(_) {
+ /**
+ * @desc VideoTranscriptFeedbackHandler module exports a function.
+ *
+ * @type {function}
+ * @access public
+ *
+ * @param {object} state - The object containing the state of the video
+ * player. All other modules, their parameters, public variables, etc.
+ * are available via this object.
+ *
+ * @this {object} The global window object.
+ *
+ */
+
+ var VideoTranscriptFeedbackHandler = function(state) {
+ if (!(this instanceof VideoTranscriptFeedbackHandler)) {
+ return new VideoTranscriptFeedbackHandler(state);
+ }
+
+ _.bindAll(this, 'destroy', 'getFeedbackForCurrentTranscript', 'markAsPositiveFeedback', 'markAsNegativeFeedback', 'markAsEmptyFeedback',
+ 'selectThumbsUp', 'selectThumbsDown', 'unselectThumbsUp', 'unselectThumbsDown', 'thumbsUpClickHandler', 'thumbsDownClickHandler',
+ 'sendFeedbackForCurrentTranscript', 'onHideLanguageMenu', 'getCurrentLanguage', 'loadAndSetVisibility', 'showWidget', 'hideWidget'
+ );
+
+ this.state = state;
+ this.state.videoTranscriptFeedback = this;
+ this.currentTranscriptLanguage = this.state.lang;
+ this.transcriptLanguages = this.state.config.transcriptLanguages;
+
+ if (this.state.el.find('.wrapper-transcript-feedback').length) {
+ this.initialize();
+ }
+
+ return false;
+ };
+
+ VideoTranscriptFeedbackHandler.prototype = {
+
+ destroy: function() {
+ this.state.el.off(this.events);
+ },
+
+ // Initializes the module.
+ initialize: function() {
+ this.el = this.state.el.find('.wrapper-transcript-feedback');
+
+ this.videoId = this.el.data('video-id');
+ this.userId = this.el.data('user-id');
+ this.aiTranslationsUrl = this.state.config.aiTranslationsUrl;
+
+ this.thumbsUpButton = this.el.find('.thumbs-up-btn');
+ this.thumbsDownButton = this.el.find('.thumbs-down-btn');
+ this.thumbsUpButton.on('click', this.thumbsUpClickHandler);
+ this.thumbsDownButton.on('click', this.thumbsDownClickHandler);
+
+ this.events = {
+ 'language_menu:hide': this.onHideLanguageMenu,
+ destroy: this.destroy
+ };
+ this.loadAndSetVisibility();
+ this.bindHandlers();
+ },
+
+ bindHandlers: function() {
+ this.state.el.on(this.events);
+ },
+
+ getFeedbackForCurrentTranscript: function() {
+ var self = this;
+ var url = self.aiTranslationsUrl + '/transcript-feedback' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_uuid=' + self.videoId + '&user_id=' + self.userId;
+
+ $.ajax({
+ url: url,
+ type: 'GET',
+ success: function(data) {
+ if (data && data.value === true) {
+ self.markAsPositiveFeedback();
+ self.currentFeedback = true;
+ } else {
+ if (data && data.value === false) {
+ self.markAsNegativeFeedback();
+ self.currentFeedback = false;
+ } else {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ }
+ },
+ error: function(error) {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ });
+ },
+
+ markAsPositiveFeedback: function() {
+ this.selectThumbsUp();
+ this.unselectThumbsDown();
+ },
+
+ markAsNegativeFeedback: function() {
+ this.selectThumbsDown();
+ this.unselectThumbsUp();
+ },
+
+ markAsEmptyFeedback: function() {
+ this.unselectThumbsUp();
+ this.unselectThumbsDown();
+ },
+
+ selectThumbsUp: function() {
+ var thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
+ if (thumbsUpIcon[0].classList.contains('fa-thumbs-o-up')) {
+ thumbsUpIcon[0].classList.remove("fa-thumbs-o-up");
+ thumbsUpIcon[0].classList.add("fa-thumbs-up");
+ }
+ },
+
+ selectThumbsDown: function() {
+ var thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
+ if (thumbsDownIcon[0].classList.contains('fa-thumbs-o-down')) {
+ thumbsDownIcon[0].classList.remove("fa-thumbs-o-down");
+ thumbsDownIcon[0].classList.add("fa-thumbs-down");
+ }
+ },
+
+ unselectThumbsUp: function() {
+ var thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
+ if (thumbsUpIcon[0].classList.contains('fa-thumbs-up')) {
+ thumbsUpIcon[0].classList.remove("fa-thumbs-up");
+ thumbsUpIcon[0].classList.add("fa-thumbs-o-up");
+ }
+ },
+
+ unselectThumbsDown: function() {
+ var thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
+ if (thumbsDownIcon[0].classList.contains('fa-thumbs-down')) {
+ thumbsDownIcon[0].classList.remove("fa-thumbs-down");
+ thumbsDownIcon[0].classList.add("fa-thumbs-o-down");
+ }
+ },
+
+ thumbsUpClickHandler: function() {
+ if (this.currentFeedback) {
+ this.sendFeedbackForCurrentTranscript(null);
+ } else {
+ this.sendFeedbackForCurrentTranscript(true);
+ }
+ },
+
+ thumbsDownClickHandler: function() {
+ if (this.currentFeedback === false) {
+ this.sendFeedbackForCurrentTranscript(null);
+ } else {
+ this.sendFeedbackForCurrentTranscript(false);
+ }
+ },
+
+ sendFeedbackForCurrentTranscript: function(feedbackValue) {
+ var self = this;
+ var url = self.aiTranslationsUrl + '/transcript-feedback/';
+ $.ajax({
+ url: url,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ transcript_language: self.currentTranscriptLanguage,
+ video_uuid: self.videoId,
+ user_id: self.userId,
+ value: feedbackValue,
+ },
+ success: function(data) {
+ if (data && data.value === true) {
+ self.markAsPositiveFeedback();
+ self.currentFeedback = true;
+ } else {
+ if (data && data.value === false) {
+ self.markAsNegativeFeedback();
+ self.currentFeedback = false;
+ } else {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ }
+ },
+ error: function() {
+ self.markAsEmptyFeedback();
+ self.currentFeedback = null;
+ }
+ });
+ },
+
+ onHideLanguageMenu: function() {
+ var newLanguageSelected = this.getCurrentLanguage();
+ if (this.currentTranscriptLanguage !== newLanguageSelected) {
+ this.currentTranscriptLanguage = this.getCurrentLanguage();
+ this.loadAndSetVisibility();
+ }
+ },
+
+ getCurrentLanguage: function() {
+ var language = this.state.lang;
+ return language;
+ },
+
+ loadAndSetVisibility: function() {
+ var self = this;
+ var url = self.aiTranslationsUrl + '/video-transcript' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_uuid=' + self.videoId;
+
+ $.ajax({
+ url: url,
+ type: 'GET',
+ async: false,
+ success: function(data) {
+ if (data && data.status === 'Completed') {
+ self.showWidget();
+ self.getFeedbackForCurrentTranscript();
+ } else {
+ self.hideWidget();
+ }
+ },
+ error: function(error) {
+ self.hideWidget();
+ }
+ });
+ },
+
+ showWidget: function() {
+ this.el.show();
+ },
+
+ hideWidget: function() {
+ this.el.hide();
+ },
+
+ };
+
+ return VideoTranscriptFeedbackHandler;
+ });
+}(RequireJS.define));
diff --git a/xmodule/js/src/video/10_main.js b/xmodule/js/src/video/10_main.js
index e1c8753ed255..35e9680b4ec1 100644
--- a/xmodule/js/src/video/10_main.js
+++ b/xmodule/js/src/video/10_main.js
@@ -65,14 +65,15 @@
'video/09_completion.js',
'video/10_commands.js',
'video/095_video_context_menu.js',
- 'video/036_video_social_sharing.js'
+ 'video/036_video_social_sharing.js',
+ 'video/037_video_transcript_feedback.js'
],
function(
VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen,
VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoAutoAdvanceControl,
VideoCaption, VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl,
VideoBumper, VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster,
- VideoCompletionHandler, VideoCommands, VideoContextMenu, VideoSocialSharing
+ VideoCompletionHandler, VideoCommands, VideoContextMenu, VideoSocialSharing, VideoTranscriptFeedback
) {
/* RequireJS */
var youtubeXhr = null,
@@ -92,10 +93,10 @@
FocusGrabber, VideoControl, VideoPlayPlaceholder,
VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl,
VideoVolumeControl, VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands,
- VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler
+ VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler, VideoTranscriptFeedback
].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []),
bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl,
- VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin,
+ VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoTranscriptFeedback,
VideoEventsBumperPlugin, VideoCompletionHandler],
state = {
el: el,
diff --git a/xmodule/video_block/video_block.py b/xmodule/video_block/video_block.py
index fdc9a0270b06..e28b47f56fc6 100644
--- a/xmodule/video_block/video_block.py
+++ b/xmodule/video_block/video_block.py
@@ -30,9 +30,9 @@
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData
-from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE
+from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE, ATTR_KEY_USER_ID
from openedx.core.djangoapps.video_config.models import HLSPlaybackEnabledFlag, CourseYoutubeBlockedFlag
-from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
+from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE, TRANSCRIPT_FEEDBACK
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE
from openedx.core.lib.cache_utils import request_cached
from openedx.core.lib.courses import get_course_by_id
@@ -448,6 +448,7 @@ def get_html(self, view=STUDENT_VIEW, context=None): # lint-amnesty, pylint: di
'transcriptAvailableTranslationsUrl': self.runtime.handler_url(
self, 'transcript', 'available_translations'
).rstrip('/?'),
+ 'aiTranslationsUrl': settings.AI_TRANSLATIONS_API_URL,
'transcriptLanguage': transcript_language,
'transcriptLanguages': sorted_languages,
'transcriptTranslationUrl': self.runtime.handler_url(
@@ -479,6 +480,8 @@ def get_html(self, view=STUDENT_VIEW, context=None): # lint-amnesty, pylint: di
'id': self.location.html_id(),
'block_id': str(self.location),
'course_id': str(self.location.course_key),
+ 'video_id': str(self.edx_video_id),
+ 'user_id': self.get_user_id(),
'is_embed': is_embed,
'license': getattr(self, "license", None),
'metadata': json.dumps(OrderedDict(metadata)),
@@ -486,6 +489,7 @@ def get_html(self, view=STUDENT_VIEW, context=None): # lint-amnesty, pylint: di
'track': track_url,
'transcript_download_format': transcript_download_format,
'transcript_download_formats_list': self.fields['transcript_download_format'].values, # lint-amnesty, pylint: disable=unsubscriptable-object
+ 'transcript_feedback_enabled': self.is_transcript_feedback_enabled(),
}
if self.is_public_sharing_enabled():
public_video_url = self.get_public_video_url()
@@ -541,6 +545,21 @@ def is_public_sharing_enabled(self):
else:
return self.public_access
+ def is_transcript_feedback_enabled(self):
+ """
+ Is transcript feedback enabled for this video?
+ """
+ try:
+ # Video transcript feedback must be enabled in order to show the widget
+ feature_enabled = TRANSCRIPT_FEEDBACK.is_enabled(self.location.course_key)
+ except Exception as err: # pylint: disable=broad-except
+ log.exception(f"Error retrieving course for course ID: {self.location.course_key}")
+ return False
+ return feature_enabled
+
+ def get_user_id(self):
+ return self.runtime.service(self, 'user').get_current_user().opt_attrs.get(ATTR_KEY_USER_ID)
+
def get_public_video_url(self):
"""
Returns the public video url