Skip to content
代码片段 群组 项目
提交 63611589 编辑于 作者: David Pisek's avatar David Pisek
浏览文件

Merge branch '413693-add-user-feedback-form' into 'master'

Add comment to AI feedback

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124803



Merged-by: default avatarDavid Pisek <dpisek@gitlab.com>
Approved-by: default avatarDavid Pisek <dpisek@gitlab.com>
Reviewed-by: default avatarDavid Pisek <dpisek@gitlab.com>
Co-authored-by: default avatarSamantha Ming <sming@gitlab.com>
No related branches found
No related tags found
无相关合并请求
......@@ -21,8 +21,8 @@ import aiActionMutation from 'ee/graphql_shared/mutations/ai_action.mutation.gra
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER, TYPENAME_VULNERABILITY } from '~/graphql_shared/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import UserFeedback from 'ee/ai/components/user_feedback.vue';
import ExplainVulnerabilityPrompt from './explain_vulnerability_prompt.vue';
import UserFeedback from './explain_this_vulnerability_user_feedback.vue';
// Returns the file extension of a file path, including all segments after the first period
// ex. 'index.html.erb' => 'html.erb'
......@@ -226,12 +226,7 @@ export default {
</div>
<template #footer>
<user-feedback
v-if="markdown"
event-name="explain_vulnerability"
:event-extra-data="eventExtraData"
class="gl-pb-0!"
/>
<user-feedback v-if="markdown" :event-extra-data="eventExtraData" />
</template>
</gl-drawer>
</gl-card>
......
<script>
import { GlButton, GlFormTextarea } from '@gitlab/ui';
import Tracking from '~/tracking';
import { FEEDBACK_OPTIONS } from 'ee/ai/constants';
export default {
components: {
GlButton,
GlFormTextarea,
},
mixins: [Tracking.mixin()],
props: {
eventExtraData: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
selectedOption: null,
comment: '',
isSubmitted: false,
};
},
computed: {
feedbackOptions() {
return this.isSubmitted ? [this.selectedOption] : FEEDBACK_OPTIONS;
},
},
methods: {
selectFeedback(option) {
this.selectedOption = option;
},
submitFeedback() {
this.isSubmitted = true;
this.track('explain_vulnerability', {
action: 'click_button',
label: 'response_feedback',
property: this.selectedOption.value,
extra: {
comment: this.comment,
...this.eventExtraData,
},
});
},
},
};
</script>
<template>
<div>
<gl-button
v-for="option in feedbackOptions"
:key="option.value"
class="gl-mr-2"
:variant="option === selectedOption ? 'confirm' : 'default'"
:category="option === selectedOption ? 'secondary' : 'primary'"
:icon="option.icon"
:disabled="isSubmitted && option === selectedOption"
data-testid="feedback-button"
@click="selectFeedback(option)"
>
{{ option.title }}
</gl-button>
<template v-if="!isSubmitted">
<gl-form-textarea
v-model="comment"
:placeholder="s__('AI|Explain your rating (optional)')"
class="gl-my-4"
rows="2"
max-rows="5"
/>
<gl-button
type="submit"
:disabled="!selectedOption"
variant="confirm"
@click="submitFeedback"
>
{{ __('Submit') }}
</gl-button>
</template>
</div>
</template>
......@@ -6,7 +6,7 @@ import ExplainThisVulnerability, {
getFileExtension,
} from 'ee/vulnerabilities/components/explain_this_vulnerability.vue';
import ExplainVulnerabilityPrompt from 'ee/vulnerabilities/components/explain_vulnerability_prompt.vue';
import UserFeedback from 'ee/ai/components/user_feedback.vue';
import UserFeedback from 'ee/vulnerabilities/components/explain_this_vulnerability_user_feedback.vue';
import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql';
import aiActionMutation from 'ee/graphql_shared/mutations/ai_action.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -77,14 +77,6 @@ describe('Explain This Vulnerability component', () => {
});
describe('User feedback', () => {
it('shows the user feedback with the expected event name', async () => {
createWrapper();
findTryButton().vm.$emit('click');
await waitForPromises();
expect(findUserFeedback().props('eventName')).toBe('explain_vulnerability');
});
it('shows the user feedback with the expected event extra information', async () => {
createWrapper();
findTryButton().vm.$emit('click');
......
import { GlFormTextarea } from '@gitlab/ui';
import { nextTick } from 'vue';
import Tracking from '~/tracking';
import { FEEDBACK_OPTIONS } from 'ee/ai/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ExplainThisVulnerabilityUserFeedback from 'ee/vulnerabilities/components/explain_this_vulnerability_user_feedback.vue';
const EVENT_NAME = 'explain_vulnerability';
const MOCK_TRACKING_DATA = {
action: 'click_button',
label: 'response_feedback',
property: FEEDBACK_OPTIONS[0].value,
extra: { comment: '' },
};
describe('ExplainThisVulnerabilityUserFeedback', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(ExplainThisVulnerabilityUserFeedback, {
propsData: {
promptLocation: MOCK_TRACKING_DATA.extra.prompt_location,
...props,
},
});
};
const findFeedbackButtons = () => wrapper.findAllByTestId('feedback-button');
const firstFeedbackButton = () => findFeedbackButtons().at(0);
const findSubmitButton = () => wrapper.find('[type="submit"]');
const findTextArea = () => wrapper.findComponent(GlFormTextarea);
beforeEach(() => {
jest.spyOn(Tracking, 'event');
});
describe('basic structure', () => {
beforeEach(() => {
createComponent();
});
it('renders buttons based on provided options', () => {
expect(findFeedbackButtons()).toHaveLength(FEEDBACK_OPTIONS.length);
});
it('should render a textarea for the user comment', () => {
expect(findTextArea().exists()).toBe(true);
});
});
describe('button', () => {
beforeEach(() => {
createComponent();
});
it('has correct text', () => {
expect(firstFeedbackButton().text()).toBe(FEEDBACK_OPTIONS[0].title);
});
it('receives correct icon prop', () => {
expect(firstFeedbackButton().props('icon')).toBe(FEEDBACK_OPTIONS[0].icon);
});
});
describe('tracking', () => {
beforeEach(() => {
createComponent();
});
it('fires tracking event with extra data passed from prop', () => {
const eventExtraData = { foo: 'bar' };
createComponent({ eventExtraData });
firstFeedbackButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(
undefined,
EVENT_NAME,
expect.objectContaining({
extra: expect.objectContaining(eventExtraData),
}),
);
});
it('fires tracking event when component is destroyed if button was clicked', () => {
firstFeedbackButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_NAME, MOCK_TRACKING_DATA);
});
it('fires tracking event when the window is closed', () => {
firstFeedbackButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_NAME, MOCK_TRACKING_DATA);
});
it('shows only selected option with disabled state once feedback is provided', async () => {
const selectedButtonIndex = 2;
expect(findFeedbackButtons()).toHaveLength(3);
findFeedbackButtons().at(selectedButtonIndex).vm.$emit('click');
findSubmitButton().vm.$emit('click');
await nextTick();
const savedFeedbackButtons = findFeedbackButtons();
const savedFeedbackButton = savedFeedbackButtons.at(0);
expect(savedFeedbackButtons).toHaveLength(1);
expect(savedFeedbackButtons.exists()).toBe(true);
expect(savedFeedbackButton.attributes('disabled')).toBeDefined();
expect(savedFeedbackButton.text()).toBe(FEEDBACK_OPTIONS[selectedButtonIndex].title);
});
});
describe('submit', () => {
beforeEach(() => {
createComponent();
});
it('should have the submit button disabled initially', () => {
expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('enables the submit button once a feedback option is clicked', async () => {
await firstFeedbackButton().vm.$emit('click');
expect(findSubmitButton().attributes('disabled')).toBeUndefined();
});
it('does not display textarea and submit button after the form is submitted', async () => {
firstFeedbackButton().vm.$emit('click');
findSubmitButton().vm.$emit('click');
await nextTick();
expect(findTextArea().exists()).toBe(false);
expect(findSubmitButton().exists()).toBe(false);
});
});
});
......@@ -1946,6 +1946,9 @@ msgstr ""
msgid "AI|Explain the code from %{filePath} in human understandable language presented in Markdown format. In the response add neither original code snippet nor any title. `%{text}`. If it is not programming code, say `The selected text is not code. I am afraid this feature is for explaining code only. Would you like to ask a different question about the selected text?` and wait for another question."
msgstr ""
 
msgid "AI|Explain your rating (optional)"
msgstr ""
msgid "AI|Features that use third-party AI services require transmission of data, including personal data."
msgstr ""
 
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册