From 2a4c5c30b22a77d003ea202783a9167bc8f0cc51 Mon Sep 17 00:00:00 2001 From: Stanislav Lashmanov Date: Tue, 16 May 2023 21:46:15 +0400 Subject: [PATCH 01/10] Add an AiAction to fill in MR template --- .../vue_shared/components/markdown/header.vue | 7 +++ .../markdown/mount_markdown_editor.js | 50 +++++++++++++++++++ .../merge_requests/creations_controller.rb | 5 +- config/routes/merge_requests.rb | 6 ++- .../ai/components/ai_actions_dropdown.vue | 18 +++++-- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index bf070943fe6aa9..3db5aab1a3ee0a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -203,6 +203,12 @@ export default { }); } }, + replaceTextarea(text) { + const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); + if (textArea) { + textArea.value = `${text}\n\n---\n\n_${__('This comment was generated using AI')}_`; + } + }, switchPreview() { if (this.previewMarkdown) { this.hideMarkdownPreview(); @@ -289,6 +295,7 @@ export default { v-if="editorAiActions.length" :actions="editorAiActions" @input="insertIntoTextarea" + @replace="replaceTextarea" /> ) or just a plain text. DO NOT fill or replace anything you're uncertain of. Please be as much as detailed as possible when it comes to the diff description. Try to understand the motivation of the change using the title and the consequences of this change. Make it easy to understand for the person reading the code. Return the text template with replaced placeholders and nothing else."; + // eslint-disable-next-line @gitlab/require-i18n-strings + content += '\n\nTITLE:\n'; + content += title; + // eslint-disable-next-line @gitlab/require-i18n-strings + content += '\n\nTEMPLATE:\n'; + content += template; + // eslint-disable-next-line @gitlab/require-i18n-strings + content += '\n\nCODE DIFFS:\n'; + content += diff; + const { data } = await Api.requestAIChat({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: + // eslint-disable-next-line @gitlab/require-i18n-strings + 'You are an AI code assistant that can understand code diffs in Git diff format, text in a Markdown format and can produce Markdown as a result.', + }, + { + role: 'user', + content, + }, + ], + }); + return data.choices[0].message.content; + }, + }, + ], + }, render(h) { return h(MarkdownEditor, { props: { diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 063813156141d5..2eba8a73d42eb6 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -57,7 +57,10 @@ def diffs @diff_notes_disabled = true - render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs) } + respond_to do |format| + format.text { render plain: @diffs.raw_diff_files.first.diff.diff } + format.json { { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs) } } + end end def diff_for_path diff --git a/config/routes/merge_requests.rb b/config/routes/merge_requests.rb index 0f9738670680de..7fecfd6301b4d2 100644 --- a/config/routes/merge_requests.rb +++ b/config/routes/merge_requests.rb @@ -78,12 +78,16 @@ scope path: 'new', as: :new_merge_request do get '', action: :new - scope constraints: ->(req) { req.format == :json }, as: :json do + scope constraints: { format: 'json' }, as: :json do get :diffs get :pipelines get :target_projects end + scope constraints: { format: 'text' }, as: :text do + get :diffs + end + scope action: :new do get :diffs, defaults: { tab: 'diffs' } get :pipelines, defaults: { tab: 'pipelines' } diff --git a/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue b/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue index 602d8501d38e50..d7368328600406 100644 --- a/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue +++ b/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue @@ -5,6 +5,10 @@ import { __ } from '~/locale'; import { createAlert } from '~/alert'; import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql'; +const METHODS = { + replace: 'replace', +}; + export default { components: { GlDisclosureDropdown, @@ -22,6 +26,7 @@ export default { return { loading: false, errorAlert: null, + method: undefined, }; }, computed: { @@ -91,13 +96,16 @@ export default { }, methods: { insertResponse(response) { - this.$emit('input', response); + const event = this.method ? METHODS[this.method] : 'input'; + this.$emit(event, response); }, - beforeAction() { + beforeAction(item) { if (this.loading) { return false; } + this.method = item.method; + this.errorAlert?.dismiss(); this.loading = true; @@ -107,17 +115,17 @@ export default { this.loading = false; }, onAbstractAction(item) { - if (!this.beforeAction()) return; + if (!this.beforeAction(item)) return; item .handler() .then((response) => { - this.insertResponse(response); + this.insertResponse(response, item); }) .catch(this.handleError) .finally(this.afterAction); }, onApolloAction(item) { - if (!this.beforeAction()) return; + if (!this.beforeAction(item)) return; this.$apollo .mutate(item.apolloMutation()) .then(({ data: { aiAction } }) => { -- GitLab From c8aa391bd857167a28a0d8eb0613bdb68dfb3107 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Tue, 13 Jun 2023 13:55:11 -0600 Subject: [PATCH 02/10] Revert Ruby changes This is per Stanislav's comment here: https://gitlab.com/gitlab-org/gitlab/-/issues/409509#note_1401530037 To be clear, however, I'm not really clear how these are related, exactly. --- .../projects/merge_requests/creations_controller.rb | 5 +---- config/routes/merge_requests.rb | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 2eba8a73d42eb6..063813156141d5 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -57,10 +57,7 @@ def diffs @diff_notes_disabled = true - respond_to do |format| - format.text { render plain: @diffs.raw_diff_files.first.diff.diff } - format.json { { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs) } } - end + render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs) } end def diff_for_path diff --git a/config/routes/merge_requests.rb b/config/routes/merge_requests.rb index 7fecfd6301b4d2..0f9738670680de 100644 --- a/config/routes/merge_requests.rb +++ b/config/routes/merge_requests.rb @@ -78,16 +78,12 @@ scope path: 'new', as: :new_merge_request do get '', action: :new - scope constraints: { format: 'json' }, as: :json do + scope constraints: ->(req) { req.format == :json }, as: :json do get :diffs get :pipelines get :target_projects end - scope constraints: { format: 'text' }, as: :text do - get :diffs - end - scope action: :new do get :diffs, defaults: { tab: 'diffs' } get :pipelines, defaults: { tab: 'pipelines' } -- GitLab From 46f00b1158c909a1f011e7b88bc2baf504c991f5 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 21 Jun 2023 16:51:23 -0600 Subject: [PATCH 03/10] Switch to using the Fill MR Description AI API endpoint --- .../markdown/mount_markdown_editor.js | 86 +++++++++++-------- .../fill_mr_description.mutation.graphql | 23 +++++ 2 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 ee/app/assets/javascripts/ai/graphql/fill_mr_description.mutation.graphql diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js index 6a80d56e623dfe..6aaa73cc386d76 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js +++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js @@ -1,6 +1,5 @@ import Vue from 'vue'; -import axios from 'axios'; -import Api from 'ee/api'; +import VueApollo from 'vue-apollo'; import { queryToObject, objectToQuery } from '~/lib/utils/url_utility'; import { parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; @@ -8,6 +7,12 @@ import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '../../constants'; import MarkdownEditor from './markdown_editor.vue'; import eventHub from './eventhub'; +import createApolloClient from '~/lib/graphql'; + +import { TYPENAME_USER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import aiFillDescriptionMutation from 'ee/ai/graphql/fill_mr_description.mutation.graphql'; + const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; const MR_TARGET_BRANCH = 'merge_request[target_branch]'; @@ -89,52 +94,57 @@ export function mountMarkdownEditor() { const setFacade = (props) => Object.assign(facade, props); const autosaveKey = `autosave/${document.location.pathname}/${searchTerm}/description`; + const apolloClient = createApolloClient(); + const apolloProvider = new VueApollo({ defaultClient: apolloClient }); + // eslint-disable-next-line no-new new Vue({ el, + apolloProvider, provide: { editorAiActions: [ { title: __('Fill in merge request template'), description: __('Replace current template with filled in placeholders'), method: 'replace', - async handler() { - const href = document.querySelector('a[data-action="diffs"]'); - const { data: diff } = await axios.get(href, { - headers: { - Accept: 'text/plain', + subscriptionVariables() { + const projectGqlId = convertToGraphQLId( + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + 'Project', + document.getElementById('merge_request_source_project_id').value, + ); + return { + userId: convertToGraphQLId(TYPENAME_USER, gon.current_user_id), + resourceId: projectGqlId, + }; + }, + apolloMutation() { + /* eslint-disable @gitlab/require-i18n-strings */ + const projectGqlId = convertToGraphQLId( + 'Project', + document.getElementById('merge_request_source_project_id').value, + ); + const targetProjectGqlId = convertToGraphQLId( + 'Project', + document.getElementById('merge_request_target_project_id').value, + ); + /* eslint-enable @gitlab/require-i18n-strings */ + const sourceBranch = document.querySelector(`[name="${MR_SOURCE_BRANCH}"]`).value; + const targetBranch = document.querySelector(`[name="${MR_TARGET_BRANCH}"]`).value; + const mrTitle = document.getElementById('merge_request_title').value; + const mrDescription = document.getElementById('merge_request_description').value; + + return { + mutation: aiFillDescriptionMutation, + variables: { + source: sourceBranch, + target: targetBranch, + templateContent: mrDescription, + mrTitle, + projectGqlId, + targetProjectGqlId, }, - }); - const title = document.querySelector('input[name="merge_request[title]"]').value; - const template = document.querySelector('textarea[name="merge_request[description]"]') - .value; - let content = - "You are provided with a title first, text template in markdown second and with code diffs in the Git format at last. Using the information from the diff please replace all the placeholder text in the text template. The placeholder might be in an HTML comment form () or just a plain text. DO NOT fill or replace anything you're uncertain of. Please be as much as detailed as possible when it comes to the diff description. Try to understand the motivation of the change using the title and the consequences of this change. Make it easy to understand for the person reading the code. Return the text template with replaced placeholders and nothing else."; - // eslint-disable-next-line @gitlab/require-i18n-strings - content += '\n\nTITLE:\n'; - content += title; - // eslint-disable-next-line @gitlab/require-i18n-strings - content += '\n\nTEMPLATE:\n'; - content += template; - // eslint-disable-next-line @gitlab/require-i18n-strings - content += '\n\nCODE DIFFS:\n'; - content += diff; - const { data } = await Api.requestAIChat({ - model: 'gpt-3.5-turbo', - messages: [ - { - role: 'system', - content: - // eslint-disable-next-line @gitlab/require-i18n-strings - 'You are an AI code assistant that can understand code diffs in Git diff format, text in a Markdown format and can produce Markdown as a result.', - }, - { - role: 'user', - content, - }, - ], - }); - return data.choices[0].message.content; + }; }, }, ], diff --git a/ee/app/assets/javascripts/ai/graphql/fill_mr_description.mutation.graphql b/ee/app/assets/javascripts/ai/graphql/fill_mr_description.mutation.graphql new file mode 100644 index 00000000000000..cd93fa925a2810 --- /dev/null +++ b/ee/app/assets/javascripts/ai/graphql/fill_mr_description.mutation.graphql @@ -0,0 +1,23 @@ +mutation fillInMergeRequestTemplate( + $sourceProjectGqlId: ID + $targetProjectGqlId: AiModelID! + $source: String! + $target: String! + $mrTitle: String! + $templateContent: String! +) { + aiAction( + input: { + fillInMergeRequestTemplate: { + resourceId: $targetProjectGqlId + sourceProjectId: $sourceProjectGqlId + sourceBranch: $source + targetBranch: $target + title: $mrTitle + content: $templateContent + } + } + ) { + errors + } +} -- GitLab From 6bff1ea4b11da415952c1ff306f1f6c70cebf365 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 21 Jun 2023 16:52:05 -0600 Subject: [PATCH 04/10] Update footer generation and trigger an input event for reactivity --- .../vue_shared/components/markdown/header.vue | 13 +++++++++++-- locale/gitlab.pot | 12 ++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 3db5aab1a3ee0a..2e37ed369f37ad 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -13,7 +13,8 @@ import { import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { getModifierKey } from '~/constants'; import { getSelectedFragment } from '~/lib/utils/common_utils'; -import { s__, __ } from '~/locale'; +import { truncateSha } from '~/lib/utils/text_utility'; +import { s__, __, sprintf } from '~/locale'; import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import { updateText } from '~/lib/utils/text_markdown'; import ToolbarButton from './toolbar_button.vue'; @@ -204,9 +205,17 @@ export default { } }, replaceTextarea(text) { + const headSha = document.getElementById('merge_request_diff_head_sha').value; const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); + const revision = s__( + 'MergeRequest|This description was generated for revision %{revision} using AI', + ); + const noSha = s__('MergeRequest|This description was generated using AI'); + const addendum = headSha ? sprintf(revision, { revision: truncateSha(headSha) }) : noSha; + if (textArea) { - textArea.value = `${text}\n\n---\n\n_${__('This comment was generated using AI')}_`; + textArea.value = `${text}\n\n---\n\n_${addendum}_`; + textArea.dispatchEvent(new Event('input')); // The text area does not react to content changes, only inputs } }, switchPreview() { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f754d32864709a..61c7262b5f0b16 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19285,6 +19285,9 @@ msgstr "" msgid "Files, directories, and submodules in the path %{path} for commit reference %{ref}" msgstr "" +msgid "Fill in merge request template" +msgstr "" + msgid "Fill in the fields below, turn on %{strong_open}Enable SAML authentication for this group%{strong_close}, and press %{strong_open}Save changes%{strong_close}" msgstr "" @@ -28804,6 +28807,12 @@ msgstr "" msgid "MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)" msgstr "" +msgid "MergeRequest|This description was generated for revision %{revision} using AI" +msgstr "" + +msgid "MergeRequest|This description was generated using AI" +msgstr "" + msgid "MergeTopics|%{sourceTopic} will be removed" msgstr "" @@ -38541,6 +38550,9 @@ msgstr "" msgid "Replace audio" msgstr "" +msgid "Replace current template with filled in placeholders" +msgstr "" + msgid "Replace file" msgstr "" -- GitLab From a30161da6600ea3fb383eec0182f14bbab8c98f0 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Wed, 21 Jun 2023 18:12:39 -0600 Subject: [PATCH 05/10] Move AI editor actions into EE-only directory/mount function --- .../merge_requests/creations/new/index.js | 4 +- .../projects/merge_requests/edit/index.js | 3 +- .../markdown/mount_markdown_editor.js | 81 +++++-------------- .../markdown/mount_markdown_editor.js | 65 +++++++++++++++ 4 files changed, 90 insertions(+), 63 deletions(-) create mode 100644 ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index 3d81e77f8798ad..f71a1041068e80 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,9 +1,11 @@ import Vue from 'vue'; + +import { mountMarkdownEditor } from 'ee_else_ce/vue_shared/components/markdown/mount_markdown_editor'; + import initPipelines from '~/commit/pipelines/pipelines_bundle'; import MergeRequest from '~/merge_request'; import CompareApp from '~/merge_requests/components/compare_app.vue'; import { __ } from '~/locale'; -import { mountMarkdownEditor } from '~/vue_shared/components/markdown/mount_markdown_editor'; import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors'; const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js index 6127adc3584284..79d771ab993f04 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -1,7 +1,8 @@ +import { mountMarkdownEditor } from 'ee_else_ce/vue_shared/components/markdown/mount_markdown_editor'; + import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; -import { mountMarkdownEditor } from '~/vue_shared/components/markdown/mount_markdown_editor'; import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown'; import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js index 6aaa73cc386d76..e12815f0094ce7 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js +++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js @@ -1,20 +1,15 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import createApolloClient from '~/lib/graphql'; import { queryToObject, objectToQuery } from '~/lib/utils/url_utility'; import { parseBoolean } from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; + import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '../../constants'; import MarkdownEditor from './markdown_editor.vue'; import eventHub from './eventhub'; -import createApolloClient from '~/lib/graphql'; - -import { TYPENAME_USER } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; -import aiFillDescriptionMutation from 'ee/ai/graphql/fill_mr_description.mutation.graphql'; - -const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; -const MR_TARGET_BRANCH = 'merge_request[target_branch]'; +export const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; +export const MR_TARGET_BRANCH = 'merge_request[target_branch]'; function organizeQuery(obj, isFallbackKey = false) { if (!obj[MR_SOURCE_BRANCH] && !obj[MR_TARGET_BRANCH]) { @@ -59,8 +54,13 @@ function mountAutosaveClearOnSubmit(autosaveKey) { } } -export function mountMarkdownEditor() { +export function mountMarkdownEditor(options = {}) { const el = document.querySelector('.js-markdown-editor'); + const componentConfiguration = { + provide: { + ...options.provide, + }, + }; if (!el) { return null; @@ -94,61 +94,19 @@ export function mountMarkdownEditor() { const setFacade = (props) => Object.assign(facade, props); const autosaveKey = `autosave/${document.location.pathname}/${searchTerm}/description`; - const apolloClient = createApolloClient(); - const apolloProvider = new VueApollo({ defaultClient: apolloClient }); + if (options.useApollo || options.apolloProvider) { + let { apolloProvider } = options; + + if (!apolloProvider) { + apolloProvider = new VueApollo({ defaultClient: createApolloClient() }); + } + + componentConfiguration.apolloProvider = apolloProvider; + } // eslint-disable-next-line no-new new Vue({ el, - apolloProvider, - provide: { - editorAiActions: [ - { - title: __('Fill in merge request template'), - description: __('Replace current template with filled in placeholders'), - method: 'replace', - subscriptionVariables() { - const projectGqlId = convertToGraphQLId( - /* eslint-disable-next-line @gitlab/require-i18n-strings */ - 'Project', - document.getElementById('merge_request_source_project_id').value, - ); - return { - userId: convertToGraphQLId(TYPENAME_USER, gon.current_user_id), - resourceId: projectGqlId, - }; - }, - apolloMutation() { - /* eslint-disable @gitlab/require-i18n-strings */ - const projectGqlId = convertToGraphQLId( - 'Project', - document.getElementById('merge_request_source_project_id').value, - ); - const targetProjectGqlId = convertToGraphQLId( - 'Project', - document.getElementById('merge_request_target_project_id').value, - ); - /* eslint-enable @gitlab/require-i18n-strings */ - const sourceBranch = document.querySelector(`[name="${MR_SOURCE_BRANCH}"]`).value; - const targetBranch = document.querySelector(`[name="${MR_TARGET_BRANCH}"]`).value; - const mrTitle = document.getElementById('merge_request_title').value; - const mrDescription = document.getElementById('merge_request_description').value; - - return { - mutation: aiFillDescriptionMutation, - variables: { - source: sourceBranch, - target: targetBranch, - templateContent: mrDescription, - mrTitle, - projectGqlId, - targetProjectGqlId, - }, - }; - }, - }, - ], - }, render(h) { return h(MarkdownEditor, { props: { @@ -174,6 +132,7 @@ export function mountMarkdownEditor() { }, }); }, + ...componentConfiguration, }); mountAutosaveClearOnSubmit(autosaveKey); diff --git a/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js new file mode 100644 index 00000000000000..81872a422e83dd --- /dev/null +++ b/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js @@ -0,0 +1,65 @@ +import { __ } from '~/locale'; +import { TYPENAME_USER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import aiFillDescriptionMutation from 'ee/ai/graphql/fill_mr_description.mutation.graphql'; +import { + mountMarkdownEditor as mountCEMarkdownEditor, + MR_SOURCE_BRANCH, + MR_TARGET_BRANCH, +} from '~/vue_shared/components/markdown/mount_markdown_editor'; + +export function mountMarkdownEditor() { + const provideEEAiActions = [ + { + title: __('Fill in merge request template'), + description: __('Replace current template with filled in placeholders'), + method: 'replace', + subscriptionVariables() { + const projectGqlId = convertToGraphQLId( + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + 'Project', + document.getElementById('merge_request_source_project_id').value, + ); + return { + userId: convertToGraphQLId(TYPENAME_USER, gon.current_user_id), + resourceId: projectGqlId, + }; + }, + apolloMutation() { + /* eslint-disable @gitlab/require-i18n-strings */ + const projectGqlId = convertToGraphQLId( + 'Project', + document.getElementById('merge_request_source_project_id').value, + ); + const targetProjectGqlId = convertToGraphQLId( + 'Project', + document.getElementById('merge_request_target_project_id').value, + ); + /* eslint-enable @gitlab/require-i18n-strings */ + const sourceBranch = document.querySelector(`[name="${MR_SOURCE_BRANCH}"]`).value; + const targetBranch = document.querySelector(`[name="${MR_TARGET_BRANCH}"]`).value; + const mrTitle = document.getElementById('merge_request_title').value; + const mrDescription = document.getElementById('merge_request_description').value; + + return { + mutation: aiFillDescriptionMutation, + variables: { + source: sourceBranch, + target: targetBranch, + templateContent: mrDescription, + mrTitle, + projectGqlId, + targetProjectGqlId, + }, + }; + }, + }, + ]; + + return mountCEMarkdownEditor({ + useApollo: true, + provide: { + editorAiActions: provideEEAiActions, + }, + }); +} -- GitLab From 681cd544127533ac87178da451a896b36bb42c0f Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 23 Jun 2023 21:58:29 -0600 Subject: [PATCH 06/10] Switch METHODS to EVENTS since that's how it's used --- .../assets/javascripts/ai/components/ai_actions_dropdown.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue b/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue index d7368328600406..3d654fe5f2ac72 100644 --- a/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue +++ b/ee/app/assets/javascripts/ai/components/ai_actions_dropdown.vue @@ -5,7 +5,7 @@ import { __ } from '~/locale'; import { createAlert } from '~/alert'; import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql'; -const METHODS = { +const EVENTS = { replace: 'replace', }; @@ -96,7 +96,7 @@ export default { }, methods: { insertResponse(response) { - const event = this.method ? METHODS[this.method] : 'input'; + const event = this.method ? EVENTS[this.method] : 'input'; this.$emit(event, response); }, beforeAction(item) { -- GitLab From 6b7db772ed18e62ae5eae1b07a93caba533b01d8 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 23 Jun 2023 21:59:50 -0600 Subject: [PATCH 07/10] Move localization text to a single location --- .../vue_shared/components/markdown/header.vue | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 2e37ed369f37ad..1bfa41e149c16a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -205,13 +205,12 @@ export default { } }, replaceTextarea(text) { + const { description, descriptionForSha } = this.$options.i18n; const headSha = document.getElementById('merge_request_diff_head_sha').value; const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); - const revision = s__( - 'MergeRequest|This description was generated for revision %{revision} using AI', - ); - const noSha = s__('MergeRequest|This description was generated using AI'); - const addendum = headSha ? sprintf(revision, { revision: truncateSha(headSha) }) : noSha; + const addendum = headSha + ? sprintf(descriptionForSha, { revision: truncateSha(headSha) }) + : description; if (textArea) { textArea.value = `${text}\n\n---\n\n_${addendum}_`; @@ -235,8 +234,13 @@ export default { outdent: keysFor(OUTDENT_LINE), }, i18n: { - preview: __('Preview'), + comment: __('This comment was generated by AI'), + description: s__('MergeRequest|This description was generated using AI'), + descriptionForSha: s__( + 'MergeRequest|This description was generated for revision %{revision} using AI', + ), hidePreview: __('Continue editing'), + preview: __('Preview'), }, }; -- GitLab From 4766670a9c57f0a744bf91e4235dbc04d77a9814 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Fri, 23 Jun 2023 22:00:25 -0600 Subject: [PATCH 08/10] Use `updateText` instead of manually setting and firing and event --- .../javascripts/vue_shared/components/markdown/header.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 1bfa41e149c16a..758254b3cc065c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -213,8 +213,12 @@ export default { : description; if (textArea) { - textArea.value = `${text}\n\n---\n\n_${addendum}_`; - textArea.dispatchEvent(new Event('input')); // The text area does not react to content changes, only inputs + updateText({ + textArea, + tag: `${text}\n\n---\n\n_${addendum}_`, + cursorOffset: 0, + wrap: false, + }); } }, switchPreview() { -- GitLab From 94394c843858b61b71c63b59c6df32e6b81c5fc0 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Mon, 26 Jun 2023 16:40:32 -0600 Subject: [PATCH 09/10] Check for the feature flag before showing the generator button --- .../components/markdown/mount_markdown_editor.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js index 81872a422e83dd..fcfbf77217cc44 100644 --- a/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js +++ b/ee/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js @@ -9,8 +9,10 @@ import { } from '~/vue_shared/components/markdown/mount_markdown_editor'; export function mountMarkdownEditor() { - const provideEEAiActions = [ - { + const provideEEAiActions = []; + + if (window.gon?.features?.fillInMrTemplate) { + provideEEAiActions.push({ title: __('Fill in merge request template'), description: __('Replace current template with filled in placeholders'), method: 'replace', @@ -53,8 +55,8 @@ export function mountMarkdownEditor() { }, }; }, - }, - ]; + }); + } return mountCEMarkdownEditor({ useApollo: true, -- GitLab From 72ba22f71a89f2996da2a22b5b6622853382abc5 Mon Sep 17 00:00:00 2001 From: Thomas Randolph Date: Mon, 26 Jun 2023 16:41:02 -0600 Subject: [PATCH 10/10] Check the LLM for the proper stage before pushing the generate flag --- .../projects/merge_requests/creations_controller.rb | 6 ++++++ app/controllers/projects/merge_requests_controller.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 063813156141d5..b9b2199b303dc5 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -11,6 +11,12 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :build_merge_request, except: [:create] + before_action only: [:new] do + if can?(current_user, :fill_in_merge_request_template, project) + push_frontend_feature_flag(:fill_in_mr_template, project) + end + end + urgency :low, [ :new, :create, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4d9cbe889fb355..568b5fe05bc496 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -53,6 +53,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:ci_job_failures_in_mr, project) end + before_action only: [:edit] do + if can?(current_user, :fill_in_merge_request_template, project) + push_frontend_feature_flag(:fill_in_mr_template, project) + end + end + around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :discussions] after_action :log_merge_request_show, only: [:show, :diffs] -- GitLab