From 386a2fdebf429b0318473e596ed9ac0966d9a8b5 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Thu, 20 Jul 2023 10:56:19 +0200 Subject: [PATCH] [ADD] web_editor: create/transform content with a LLM This commit implements a dialog for content generation using ChatGPT, which can be opened via the Powerbox or via a button in the toolbar. If text is selected, the dialog opens in "alternatives" mode. In this mode, five alternative versions of the selected text are generated. The user can choose one and it will be inserted in lieu of the selection. Five buttons can be used to guide the AI to make the content longer, shorter, more professional, more friendly, or more persuasive. Clicking any of these buttons will restart the generation process with the new parameters. If no text is selected, the dialog opens in "prompt" mode. In this mode, the dialog contains a textarea in which the user can input a prompt which is then used to generate content. The user can then exchange with the AI back and forth until the content is satisfactory. Any of the AI's responses can be inserted into the document by clicking its "Insert" button. When content is inserted into the document, a green frame is shown around it for two seconds, to make it clear to the user that something has been inserted and where. If an error occurs, it is inserted in the dialog like other responses but in red and it can't be inserted into the document. Please find the PR of the IAP part (Odoo Language Generator) here : https://github.com/odoo/iap-apps/pull/703 task-3383324 closes odoo/odoo#137064 Signed-off-by: David Monjoie (dmo) Co-authored-by: Antoine Guenet (age) Co-authored-by: Lou (loha) --- addons/web_editor/controllers/main.py | 25 +++- .../static/src/img/odoobot_transparent.png | Bin 0 -> 21919 bytes .../widgets/chatgpt_alternatives_dialog.js | 97 ++++++++++++++++ .../widgets/chatgpt_alternatives_dialog.xml | 51 ++++++++ .../src/js/wysiwyg/widgets/chatgpt_dialog.js | 109 ++++++++++++++++++ .../wysiwyg/widgets/chatgpt_prompt_dialog.js | 102 ++++++++++++++++ .../wysiwyg/widgets/chatgpt_prompt_dialog.xml | 82 +++++++++++++ .../static/src/js/wysiwyg/wysiwyg.js | 65 +++++++++++ .../web_editor/static/src/scss/wysiwyg.scss | 31 +++++ addons/web_editor/static/src/xml/editor.xml | 4 + 10 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 addons/web_editor/static/src/img/odoobot_transparent.png create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_alternatives_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_alternatives_dialog.xml create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.xml diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 1fd886ee95d..da0c8bca85f 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -17,19 +17,21 @@ from math import floor from os.path import join as opj from odoo.http import request, Response -from odoo import http, tools, _, SUPERUSER_ID +from odoo import http, tools, _, SUPERUSER_ID, release from odoo.addons.http_routing.models.ir_http import slug, unslug from odoo.addons.web_editor.tools import get_video_url_data -from odoo.exceptions import UserError, MissingError, ValidationError +from odoo.exceptions import UserError, MissingError, AccessError from odoo.tools.misc import file_open from odoo.tools.mimetypes import guess_mimetype from odoo.tools.image import image_data_uri, binary_to_image +from odoo.addons.iap.tools import iap_tools from odoo.addons.base.models.assetsbundle import AssetsBundle from ..models.ir_attachment import SUPPORTED_IMAGE_MIMETYPES logger = logging.getLogger(__name__) DEFAULT_LIBRARY_ENDPOINT = 'https://media-api.odoo.com' +DEFAULT_OLG_ENDPOINT = 'https://olg.api.odoo.com' class Web_Editor(http.Controller): @@ -762,3 +764,22 @@ class Web_Editor(http.Controller): @http.route('/web_editor/tests', type='http', auth="user") def test_suite(self, mod=None, **kwargs): return request.render('web_editor.tests') + + @http.route("/web_editor/generate_text", type="json", auth="user") + def generate_text(self, prompt, conversation_history): + try: + IrConfigParameter = request.env['ir.config_parameter'].sudo() + olg_api_endpoint = IrConfigParameter.get_param('web_editor.olg_api_endpoint', DEFAULT_OLG_ENDPOINT) + response = iap_tools.iap_jsonrpc(olg_api_endpoint + "/api/olg/1/chat", params={ + 'prompt': prompt, + 'conversation_history': conversation_history or [], + 'version': release.version, + }, timeout=300) + if response['status'] == 'success': + return response['content'] + elif response['status'] == 'error_prompt_too_long': + raise UserError(_("Sorry, your prompt is too long. Try to say it in fewer words.")) + else: + raise UserError(_("Sorry, we could not generate a response. Please try again later.")) + except AccessError: + raise AccessError(_("Oops, it looks like our AI is unreachable!")) diff --git a/addons/web_editor/static/src/img/odoobot_transparent.png b/addons/web_editor/static/src/img/odoobot_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..46e3d84f2c26ede78addb0edd83ba9ec2cb32d21 GIT binary patch literal 21919 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 { + if (isError) { + wasError = true; + } else { + this.state.conversationHistory.push({ + role: 'user', + content: query, + }, { + role: 'assistant', + content, + }); + } + this.state.messages = [...this.state.messages, { + author: 'assistant', + text: content, + isError, + }]; + }).catch(() => { + wasError = true; + this.state.messages = []; + }); + messageIndex += 1; + if (wasError) { + break; + } + } + } +} diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_alternatives_dialog.xml b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_alternatives_dialog.xml new file mode 100644 index 00000000000..c2acbafcb3b --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_alternatives_dialog.xml @@ -0,0 +1,51 @@ + + + + + +
+
+ + + +
+
+ + +
+ +
+
+ + + +
+
+ Loading... +

+ Generating alternatives... +

+
+
+
+ + + + + + +
+
+
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_dialog.js new file mode 100644 index 00000000000..14c90ee6f2a --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_dialog.js @@ -0,0 +1,109 @@ +/** @odoo-module **/ + +import { Component, markup } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { Dialog } from "@web/core/dialog/dialog"; +import { escape } from "@web/core/utils/strings"; +import { _t } from "@web/core/l10n/translation"; + +/** + * General component for common logic between different dialogs. + */ +export class ChatGPTDialog extends Component { + static components = { Dialog }; + static props = { + close: Function, + insert: Function, + }; + + setup() { + this.rpc = useService('rpc'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + selectMessage(ev) { + this.state.selectedMessage = +ev.currentTarget.getAttribute('data-message-index'); + } + insertMessage(ev) { + this.selectMessage(ev); + this._confirm(); + } + formatContent(content) { + return markup([...this._postprocessGeneratedContent(content).childNodes].map(child => { + // Escape all text. + const nodes = new Set([...child.querySelectorAll('*')].flatMap(node => node.childNodes)); + nodes.forEach(node => { + if (node.nodeType === Node.TEXT_NODE) { + node.textContent = escape(node.textContent); + } + }); + return child.outerHTML; + }).join('')); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _postprocessGeneratedContent(content) { + const lines = content.split('\n').filter(line => line.trim().length); + const fragment = document.createDocumentFragment(); + let parentUl, parentOl; + let lineIndex = 0; + for (const line of lines) { + if (line.trim().startsWith('- ')) { + // Create or continue an unordered list. + parentUl = parentUl || document.createElement('ul'); + const li = document.createElement('li'); + li.innerText = line.trim().slice(2); + parentUl.appendChild(li); + } else if ( + (parentOl && line.startsWith(`${parentOl.children.length + 1}. `)) || + (!parentOl && line.startsWith('1. ') && lines[lineIndex + 1]?.startsWith('2. ')) + ) { + // Create or continue an ordered list (only if the line starts + // with the next number in the current ordered list (or 1 if no + // ordered list was in progress and it's followed by a 2). + parentOl = parentOl || document.createElement('ol'); + const li = document.createElement('li'); + li.innerText = line.slice(line.indexOf('.') + 2); + parentOl.appendChild(li); + } else { + // Insert any list in progress, and a new block for the current + // line. + [parentUl, parentOl].forEach(list => list && fragment.appendChild(list)); + parentUl = parentOl = undefined; + const block = document.createElement(line.startsWith('Title: ') ? 'h2' : 'p'); + block.innerText = line; + fragment.appendChild(block); + } + lineIndex += 1; + } + [parentUl, parentOl].forEach(list => list && fragment.appendChild(list)); + return fragment; + } + _cancel() { + this.props.close(); + } + _confirm() { + try { + this.props.close(); + const text = this.state.messages[this.state.selectedMessage]?.text; + this.props.insert(this._postprocessGeneratedContent(text || '')); + } catch (e) { + this.props.close(); + throw e; + } + } + _generate(prompt, callback) { + return this.rpc('/web_editor/generate_text', { + prompt, + conversation_history: this.state.conversationHistory, + }, { shadow: true }) + .then(content => callback(content)) + .catch(error => callback(_t(error.data?.message || error.message), true)); + } +} diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.js b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.js new file mode 100644 index 00000000000..5c0ba12f522 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.js @@ -0,0 +1,102 @@ +/** @odoo-module **/ + +import { ChatGPTDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_dialog'; +import { useState, useEffect, useRef } from "@odoo/owl"; +import { useAutofocus } from "@web/core/utils/hooks"; +import { session } from "@web/session"; +import { browser } from "@web/core/browser/browser"; + +export class ChatGPTPromptDialog extends ChatGPTDialog { + static template = 'web_edior.ChatGPTPromptDialog'; + static props = { + ...super.props, + initialPrompt: { type: String, optional: true }, + }; + static defaultProps = { + initialPrompt: '', + }; + + setup() { + super.setup(); + this.assistantAvatarUrl = `${browser.location.origin}/web_editor/static/src/img/odoobot_transparent.png`; + this.userAvatarUrl = `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${encodeURIComponent(session.uid)}`; + this.state = useState({ + prompt: this.props.initialPrompt, + conversationHistory: [{ + role: 'system', + content: 'You are a helpful assistant, your goal is to help the user write their document.', + }, + { + role: 'assistant', + content: 'What do you need ?', + }], + messages: [], + }); + this.promptInputRef = useRef('promptInput'); + useAutofocus({ refName: 'promptInput' }); + useEffect(() => { + // Resize the textarea to fit its content. + this.promptInputRef.el.style.height = 0; + this.promptInputRef.el.style.height = this.promptInputRef.el.scrollHeight + 'px'; + }, () => [this.state.prompt]); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + onTextareaKeydown(ev) { + if (ev.key === 'Enter' && !ev.shiftKey) { + this.submitPrompt(ev); + } + } + submitPrompt(ev) { + this._freezeInput(); + ev.preventDefault(); + const prompt = this.state.prompt; + this.state.messages.push({ author: 'user', text: prompt }); + const datetime = new Date(); + const conversation = { role: 'user', content: prompt }; + this.state.conversationHistory.push(conversation); + this.state.messages.push({ author: 'assistant', datetime }); + this.state.prompt = ''; + this._generate(prompt, (content, isError) => { + if (isError) { + // There was an error, remove the prompt from the history. + this.state.conversationHistory = this.state.conversationHistory.filter(c => c !== conversation); + } else { + // There was no error, add the response to the history. + this.state.conversationHistory.push({ role: 'assistant', content }); + } + const messageIndex = this.state.messages.findIndex(m => m.datetime === datetime); + this.state.messages[messageIndex] = { author: 'assistant', text: content, isError }; + this._unfreezeInput(); + }); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _freezeInput() { + this.promptInputRef.el.setAttribute('disabled', ''); + } + _unfreezeInput() { + this.promptInputRef.el.removeAttribute('disabled'); + this.promptInputRef.el.focus(); + } + /** + * @override + */ + _cancel() { + this._freezeInput(); + super._cancel(); + } + /** + * @override + */ + _confirm() { + this._freezeInput(); + super._confirm(); + } +} diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.xml b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.xml new file mode 100644 index 00000000000..83bfb2cbec3 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.xml @@ -0,0 +1,82 @@ + + + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ + +
+ +
+ Loading... +

+ Thinking... +

+
+ +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+ + + +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 200e91bd913..86195ab4a6c 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -11,6 +11,8 @@ import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor import { Toolbar } from "@web_editor/js/editor/toolbar"; import { LinkPopoverWidget } from '@web_editor/js/wysiwyg/widgets/link_popover_widget'; import { AltDialog } from '@web_editor/js/wysiwyg/widgets/alt_dialog'; +import { ChatGPTPromptDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_prompt_dialog'; +import { ChatGPTAlternativesDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_alternatives_dialog'; import { ImageCrop } from '@web_editor/js/wysiwyg/widgets/image_crop'; import * as wysiwygUtils from "@web_editor/js/common/wysiwyg_utils"; @@ -1415,6 +1417,55 @@ export class Wysiwyg extends Component { }); } } + /** + * Open one of the ChatGPTDialogs to generate or modify content. + * + * @param {'prompt'|'alternatives'} [mode='prompt'] + */ + openChatGPTDialog(mode = 'prompt') { + const restore = preserveCursor(this.odooEditor.document); + const params = { + insert: content => { + this.odooEditor.historyPauseSteps(); + const insertedNodes = this.odooEditor.execCommand('insert', content); + this.odooEditor.historyUnpauseSteps(); + this.notification.add(_t('Your content was successfully generated.'), { + title: _t('Content generated'), + type: 'success', + }); + this.odooEditor.historyStep(); + // Add a frame around the inserted content to highlight it for 2 + // seconds. + const start = insertedNodes?.length && closestElement(insertedNodes[0]); + const end = insertedNodes?.length && closestElement(insertedNodes[insertedNodes.length - 1]); + if (start && end) { + const divContainer = this.odooEditor.editable.parentElement; + let [parent, left] = [start.offsetParent, start.offsetLeft]; + while (parent && !parent.contains(divContainer)) { + left += parent.offsetLeft; + parent = parent.offsetParent; + } + const div = document.createElement('div'); + div.classList.add('o-chatgpt-content'); + div.style.left = `${left - 10}px`; + div.style.top = `${start.offsetTop - 10}px`; + div.style.width = `${Math.max(start.clientWidth, end.clientWidth) + 20}px`; + div.style.height = `${end.offsetTop + end.clientHeight - start.offsetTop + 20}px`; + divContainer.prepend(div); + setTimeout(() => div.remove(), 2000); + } + }, + }; + if (mode === 'alternatives') { + params.originalText = this.odooEditor.document.getSelection().toString() || ''; + } + this.odooEditor.document.getSelection().collapseToEnd(); + this.env.services.dialog.add( + mode === 'prompt' ? ChatGPTPromptDialog : ChatGPTAlternativesDialog, + params, + { onClose: restore }, + ); + } /** * Removes the current Link. */ @@ -1725,6 +1776,10 @@ export class Wysiwyg extends Component { }); break; } + case 'open-chatgpt': { + this.openChatGPTDialog(this.odooEditor.document.getSelection()?.isCollapsed ? 'prompt' : 'alternatives'); + break; + } } }; if (!options.snippets) { @@ -1738,6 +1793,7 @@ export class Wysiwyg extends Component { }) $toolbar.find('#media-insert, #media-replace, #media-description').click(openTools); $toolbar.find('#create-link').click(openTools); + $toolbar.find('#open-chatgpt').click(openTools); $toolbar.find('#image-shape div, #fa-spin').click(e => { if (!this.lastMediaClicked) { return; @@ -2292,6 +2348,15 @@ export class Wysiwyg extends Component { } }, }, + { + category: _t('AI Tools'), + name: _t('ChatGPT'), + description: _t('Generate or transform content with AI.'), + fontawesome: 'fa-magic', + priority: 1, + isDisabled: () => !this.odooEditor.isSelectionInBlockRoot(), + callback: async () => this.openChatGPTDialog(), + }, ]; if (!editorOptions.inlineStyle) { commands.push( diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index 5df2a32148c..3a4d51f9076 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -714,3 +714,34 @@ img.o_we_selected_image { textarea.o_codeview { min-height: 400px; } + +.o-chatgpt-content { + position: absolute; + background-color: lightgreen; + opacity: 0.3; + animation: fade 1.5s ease-in-out; + border: dashed 5px green; + z-index: 1; +} +.o-prompt-input { + position: relative; + // see .o-mail-Composer-input + > textarea { + padding-top: 10px; // carefully crafted to have the text in the middle in chat window + padding-bottom: 10px; + min-height: 40px; + max-height: 110px; + resize: none; + } +} +button.o-message-insert { + line-height: 1; +} +.o-chatgpt-message > div > *:last-child, .o-chatgpt-alternative > *:last-child { + margin-bottom: 0; +} +.o-message-error { + color: #d44c59; + font-weight: bold; + --bg-opacity: 0.25; +} diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 0e8cf1f7fd1..659dde98eed 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -175,6 +175,10 @@ Description +
+
+
+