From 78c37eb0ffc2326cf97d4c8495cb100f87187075 Mon Sep 17 00:00:00 2001 From: Serafin-dev Date: Wed, 9 Oct 2024 15:48:02 -0300 Subject: [PATCH] feat: add feature flag for LLM summarize --- lms/djangoapps/edxnotes/decorators.py | 7 + .../js/edxnotes/plugins/llm_summarize.js | 180 ++++++++++++++++++ lms/static/js/edxnotes/views/notes_factory.js | 14 +- 3 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 lms/static/js/edxnotes/plugins/llm_summarize.js diff --git a/lms/djangoapps/edxnotes/decorators.py b/lms/djangoapps/edxnotes/decorators.py index c522ba36bed4..7e058c3a2368 100644 --- a/lms/djangoapps/edxnotes/decorators.py +++ b/lms/djangoapps/edxnotes/decorators.py @@ -7,6 +7,7 @@ from django.conf import settings from xblock.exceptions import NoSuchServiceError +from openedx.core.djangoapps.plugins.plugins_hooks import run_extension_point from common.djangoapps.edxmako.shortcuts import render_to_string from common.djangoapps.student.auth import is_ccx_course @@ -50,6 +51,8 @@ def get_html(self, *args, **kwargs): except NoSuchServiceError: user = None + is_llm_summarize_enabled = run_extension_point('PEARSON_CORE_ENABLE_LLM_SUMMARIZE', course_id=str(course.id)) + if is_studio or not is_feature_enabled(course, user): return original_get_html(self, *args, **kwargs) else: @@ -69,6 +72,10 @@ def get_html(self, *args, **kwargs): "endpoint": get_public_endpoint(), "debug": settings.DEBUG, "eventStringLimit": settings.TRACK_MAX_EVENT / 6, + "llmSummarize": { + "isEnabled": is_llm_summarize_enabled, + "courseId": str(course.id), + }, }, }) diff --git a/lms/static/js/edxnotes/plugins/llm_summarize.js b/lms/static/js/edxnotes/plugins/llm_summarize.js new file mode 100644 index 000000000000..85644466e3f5 --- /dev/null +++ b/lms/static/js/edxnotes/plugins/llm_summarize.js @@ -0,0 +1,180 @@ +(function(define, undefined) { + 'use strict'; + define(['jquery', 'annotator_1.2.9'], function($, Annotator) { + /** + * LlmSummarize plugin adds a button to the annotatorjs adder in order to + * summarize the text selected and save as annotation. + **/ + Annotator.Plugin.LlmSummarize = function() { + Annotator.Plugin.apply(this, arguments); + }; + + $.extend(Annotator.Plugin.LlmSummarize.prototype, new Annotator.Plugin(), { + pluginInit: function() { + // Overrides of annotatorjs HTML/CSS to add summarize button. + const style = document.createElement('style'); + style.innerHTML = ` + .annotator-adder::before { + content: ''; + width: 10px; + height: 10px; + background-color: white; + display: block; + position: absolute; + top: 100%; + left: 5px; + border-bottom: 1px solid gray; + border-right: 1px solid gray; + z-index: 0; + transform: translateY(-50%) rotate(45deg); + } + + .annotator-adder button::before, + .annotator-adder button::after { + display: none !important; + } + + .annotator-adder #annotateButton, + .annotator-adder #summarizeButton { + border: none !important; + background: rgb(0, 48, 87) !important; + box-shadow: none !important; + width: initial; + transition: color .1s; + text-indent: initial; + font-size: 20px; + padding: 0; + height: fit-content; + color: white; + border-radius: 5px; + padding-left: 5px; + padding-right: 5px; + font-weight: normal; + display: inline-block; + } + + .annotator-adder #summarizeButton { + margin-left: 3px; + } + + .annotator-adder button i.fa { + font-style: normal; + } + + .annotator-adder { + width: fit-content; + height: fit-content; + padding: 5px; + background-color: white; + border: 1px solid gray; + } + + // Defining content loader since fontawesome icons don't work + // inside the annotatorjs modal. + .loader { + width: 5em !important; + border-color: red !important; + } + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + `; + let annotator = this.annotator; + document.head.appendChild(style); + this.modifyDom(this.annotator); + annotator.editor.options.llmSummarize = annotator.options.llmSummarize + const summarizeButton = document.getElementById('summarizeButton'); + + summarizeButton.addEventListener('click', function(ev) { + annotator.editor.options.isSummarizing = true; + }); + annotator.subscribe('annotationEditorShown', this.handleSummarize); + annotator.subscribe('annotationEditorHidden', this.cleanupSummarize); + }, + handleSummarize: function (editor, annotation) { + if (!editor.options?.isSummarizing) return; + + function toggleLoader() { + const saveButton = document.querySelector('.annotator-controls .annotator-save'); + const loaderWrapper = document.querySelector('.summarize-loader-wrapper'); + editor.fields[0].element.children[0].classList.toggle('d-none'); + loaderWrapper.classList.toggle('d-none'); + saveButton.disabled = !saveButton.disabled; + } + const textAreaWrapper = editor.fields[0].element; + const request = new Request('/pearson-core/llm-assistance/api/v0/summarize-text', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': $.cookie('csrftoken'), + }, + body: JSON.stringify({ + text_to_summarize: annotation.quote, + course_id: editor.options?.llmSummarize?.courseId, + }), + }); + + editor.fields[1].element.children[0].value = 'ai_summary'; + toggleLoader(editor); + fetch(request) + .then((response) => { + toggleLoader(); + if (response.ok) return response.json(); + throw new Error(gettext('There was an error while summarizing the content.')); + }) + .then((data) => { + textAreaWrapper.children[0].value = data.summary; + }) + .catch((error) => { + alert(error.message); + editor.hide(); + }); + }, + cleanupSummarize: function(editor) { + const textAreaWrapper = editor.fields[0].element; + const loaderWrapper = document.querySelector('.summarize-loader-wrapper'); + + textAreaWrapper.children[0].value = ''; + textAreaWrapper.children[1].value = ''; + editor.options.isSummarizing = false; + loaderWrapper.classList.add('d-none'); + }, + modifyDom: function(annotator) { + const textAreaWrapper = annotator.editor.fields[0].element; + + annotator.adder[0].children[0].id = 'annotateButton'; + annotator.adder[0].children[0].innerHTML = ''; + annotator.adder[0].innerHTML += ` + + `; + // Style is being defined here since classes styling not working + // for this element. + textAreaWrapper.innerHTML += ` +
+ + + + ${gettext('Summarizing...')} + +
`; + }, + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/edxnotes/views/notes_factory.js b/lms/static/js/edxnotes/views/notes_factory.js index 5c98bc7683b7..3cf07541b919 100644 --- a/lms/static/js/edxnotes/views/notes_factory.js +++ b/lms/static/js/edxnotes/views/notes_factory.js @@ -7,9 +7,10 @@ 'js/edxnotes/plugins/events', 'js/edxnotes/plugins/accessibility', 'js/edxnotes/plugins/caret_navigation', 'js/edxnotes/plugins/store_error_handler', - 'js/edxnotes/plugins/search_override' + 'js/edxnotes/plugins/search_override', + 'js/edxnotes/plugins/llm_summarize', ], function($, _, Annotator, NotesLogger, NotesCollector) { - var plugins = ['Auth', 'Store', 'Scroller', 'Events', 'Accessibility', 'CaretNavigation', 'Tags'], + var plugins = ['Auth', 'Store', 'Scroller', 'Events', 'Accessibility', 'CaretNavigation', 'Tags', 'LlmSummarize'], getOptions, setupPlugins, getAnnotator; /** @@ -50,7 +51,11 @@ destroy: '/annotations/:id/', search: '/search/' } - } + }, + llmSummarize: { + isEnabled: params?.llmSummarize?.isEnabled, + courseId: params?.llmSummarize?.courseId, + }, }; }; @@ -84,6 +89,7 @@ logger = NotesLogger.getLogger(element.id, params.debug), annotator; + if (options?.llmSummarize?.isEnabled) plugins.push('LlmSummarize') annotator = $el.annotator(options).data('annotator'); setupPlugins(annotator, plugins, options); NotesCollector.storeNotesRequestData( @@ -99,4 +105,4 @@ factory: getAnnotator }; }); -}).call(this, define || RequireJS.define); +}).call(this, define || RequireJS.define); \ No newline at end of file