@gitlab/duo-ui 15.8.3 → 15.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,64 @@
1
1
  <script>
2
- import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
2
+ import { nextTick } from 'vue';
3
+ import { GlButton, GlIcon, GlTooltipDirective, GlFormTextarea, GlForm } from '@gitlab/ui';
3
4
  import { translate } from '@gitlab/ui/dist/utils/i18n';
4
5
 
5
6
  export const i18n = {
6
7
  THANKS: translate('AgenticBinaryFeedback.thanks', 'Thanks for the feedback!'),
7
8
  THUMBS_UP: translate('AgenticBinaryFeedback.thumbsUp', 'Helpful'),
8
9
  THUMBS_DOWN: translate('AgenticBinaryFeedback.thumbsDown', 'Not helpful'),
10
+ TELL_US_MORE: translate('AgenticBinaryFeedback.tellUsMore', 'Tell us more'),
11
+ SUBMIT: translate('AgenticBinaryFeedback.submit', 'Submit'),
12
+ BACK: translate('AgenticBinaryFeedback.back', 'Back'),
13
+ CLOSE: translate('AgenticBinaryFeedback.close', 'Close'),
14
+ FEEDBACK_PLACEHOLDER: translate('AgenticBinaryFeedback.placeholder', 'Share your feedback...'),
15
+ THUMBS_UP_HEADER: translate('AgenticBinaryFeedback.thumbsUpHeader', 'What made this helpful?'),
16
+ THUMBS_DOWN_HEADER: translate(
17
+ 'AgenticBinaryFeedback.thumbsDownHeader',
18
+ 'What would have been more helpful?'
19
+ ),
20
+ TOO_GENERIC: translate('AgenticBinaryFeedback.tooGeneric', 'Too generic'),
21
+ MISSING_STEPS: translate('AgenticBinaryFeedback.missingSteps', 'Missing steps'),
22
+ WRONG_CONTEXT: translate('AgenticBinaryFeedback.wrongContext', 'Wrong context'),
23
+ OUTDATED_INCORRECT: translate('AgenticBinaryFeedback.outdatedIncorrect', 'Outdated/incorrect'),
24
+ SOLVED_PROBLEM: translate('AgenticBinaryFeedback.solvedProblem', 'Solved my problem'),
25
+ SAVED_TIME: translate('AgenticBinaryFeedback.savedTime', 'Saved me time'),
26
+ GOOD_EXAMPLES: translate('AgenticBinaryFeedback.goodExamples', 'Good examples'),
27
+ ACCURATE_INFO: translate('AgenticBinaryFeedback.accurateInfo', 'Accurate information'),
28
+ };
29
+
30
+ export const thumbsDownReasons = [
31
+ { key: 'too_generic', label: i18n.TOO_GENERIC },
32
+ { key: 'missing_steps', label: i18n.MISSING_STEPS },
33
+ { key: 'wrong_context', label: i18n.WRONG_CONTEXT },
34
+ { key: 'outdated_incorrect', label: i18n.OUTDATED_INCORRECT },
35
+ { key: 'tell_us_more', label: i18n.TELL_US_MORE, isCustom: true },
36
+ ];
37
+
38
+ export const thumbsUpReasons = [
39
+ { key: 'solved_problem', label: i18n.SOLVED_PROBLEM },
40
+ { key: 'saved_time', label: i18n.SAVED_TIME },
41
+ { key: 'good_examples', label: i18n.GOOD_EXAMPLES },
42
+ { key: 'accurate_info', label: i18n.ACCURATE_INFO },
43
+ { key: 'tell_us_more', label: i18n.TELL_US_MORE, isCustom: true },
44
+ ];
45
+
46
+ export const FEEDBACK_VIEW = {
47
+ CLOSED: 'closed',
48
+ REASON_LIST: 'reason_list',
49
+ TELL_US_MORE: 'tell_us_more',
9
50
  };
10
51
 
11
52
  export default {
12
53
  name: 'AgenticBinaryFeedback',
13
54
  i18n,
55
+ thumbsUpReasons,
56
+ thumbsDownReasons,
14
57
  components: {
15
58
  GlButton,
16
59
  GlIcon,
60
+ GlFormTextarea,
61
+ GlForm,
17
62
  },
18
63
  directives: {
19
64
  GlTooltip: GlTooltipDirective,
@@ -21,34 +66,108 @@ export default {
21
66
  data() {
22
67
  return {
23
68
  feedbackType: null,
69
+ feedbackView: FEEDBACK_VIEW.CLOSED,
70
+ customFeedback: '',
71
+ feedbackSubmitted: false,
24
72
  };
25
73
  },
26
74
  computed: {
27
- feedbackGiven() {
28
- return this.feedbackType !== null;
29
- },
30
75
  isThumbsUp() {
31
76
  return this.feedbackType === 'thumbs_up';
32
77
  },
33
78
  isThumbsDown() {
34
79
  return this.feedbackType === 'thumbs_down';
35
80
  },
81
+ reasonOptions() {
82
+ return this.isThumbsUp ? this.$options.thumbsUpReasons : this.$options.thumbsDownReasons;
83
+ },
84
+ reasonHeader() {
85
+ return this.isThumbsUp
86
+ ? this.$options.i18n.THUMBS_UP_HEADER
87
+ : this.$options.i18n.THUMBS_DOWN_HEADER;
88
+ },
89
+ isOverCharacterLimit() {
90
+ return this.customFeedback.length > 140;
91
+ },
92
+ trimmedFeedback() {
93
+ return this.customFeedback.trim();
94
+ },
95
+ isTextareaValid() {
96
+ return this.trimmedFeedback.length === 0 ? null : !this.isOverCharacterLimit;
97
+ },
98
+ canSubmitCustomFeedback() {
99
+ return this.trimmedFeedback.length > 0 && !this.isOverCharacterLimit;
100
+ },
101
+ characterCountText() {
102
+ const remaining = 140 - this.customFeedback.length;
103
+ if (remaining >= 0) {
104
+ return remaining === 1
105
+ ? `${remaining} character remaining`
106
+ : `${remaining} characters remaining`;
107
+ }
108
+ const over = Math.abs(remaining);
109
+ return over === 1 ? `${over} character over limit` : `${over} characters over limit`;
110
+ },
111
+ isDropdownOpen() {
112
+ return this.feedbackView !== FEEDBACK_VIEW.CLOSED;
113
+ },
114
+ isReasonListVisible() {
115
+ return this.feedbackView === FEEDBACK_VIEW.REASON_LIST;
116
+ },
117
+ isTellUsMoreVisible() {
118
+ return this.feedbackView === FEEDBACK_VIEW.TELL_US_MORE;
119
+ },
36
120
  },
37
121
  methods: {
38
- submitFeedback(type) {
39
- /**
40
- * Notify listeners about the feedback submission.
41
- * @param {string} type The feedback type ('thumbs_up' or 'thumbs_down').
42
- */
43
- this.$emit('feedback', { feedbackType: type });
122
+ selectFeedbackType(type) {
44
123
  this.feedbackType = type;
124
+ this.feedbackView = FEEDBACK_VIEW.REASON_LIST;
125
+ this.customFeedback = '';
126
+ this.scrollToDropdown();
127
+ },
128
+ scrollToDropdown() {
129
+ nextTick(() => {
130
+ const el = this.$refs.dropdown?.$el || this.$refs.dropdown;
131
+ el?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
132
+ });
133
+ },
134
+ selectReason(reason) {
135
+ if (reason.isCustom) {
136
+ this.feedbackView = FEEDBACK_VIEW.TELL_US_MORE;
137
+ return;
138
+ }
139
+ this.submitFeedback(reason.key);
140
+ },
141
+ submitCustomFeedback() {
142
+ this.submitFeedback(`custom_${this.customFeedback.trim()}`);
143
+ },
144
+ submitFeedback(feedbackReason) {
145
+ this.$emit('feedback', {
146
+ feedbackType: this.feedbackType,
147
+ feedbackReason,
148
+ });
149
+ this.feedbackView = FEEDBACK_VIEW.CLOSED;
150
+ this.feedbackSubmitted = true;
151
+ },
152
+ goBackToReasons() {
153
+ this.feedbackView = FEEDBACK_VIEW.REASON_LIST;
154
+ this.customFeedback = '';
155
+ },
156
+ closeDropdown() {
157
+ this.feedbackView = FEEDBACK_VIEW.CLOSED;
158
+ this.customFeedback = '';
159
+ this.feedbackType = null;
45
160
  },
46
161
  },
47
162
  };
48
163
  </script>
164
+
49
165
  <template>
50
- <div class="agentic-binary-feedback gl-flex gl-items-center gl-gap-2">
51
- <template v-if="!feedbackGiven">
166
+ <div
167
+ class="agentic-binary-feedback gl-flex gl-items-center gl-gap-2"
168
+ :class="{ 'gl-relative': !isDropdownOpen }"
169
+ >
170
+ <template v-if="!feedbackSubmitted && !isDropdownOpen">
52
171
  <gl-button
53
172
  v-gl-tooltip
54
173
  :title="$options.i18n.THUMBS_UP"
@@ -57,7 +176,7 @@ export default {
57
176
  category="tertiary"
58
177
  size="small"
59
178
  data-testid="thumb-up-button"
60
- @click="submitFeedback('thumbs_up')"
179
+ @click="selectFeedbackType('thumbs_up')"
61
180
  />
62
181
  <gl-button
63
182
  v-gl-tooltip
@@ -67,9 +186,109 @@ export default {
67
186
  category="tertiary"
68
187
  size="small"
69
188
  data-testid="thumb-down-button"
70
- @click="submitFeedback('thumbs_down')"
189
+ @click="selectFeedbackType('thumbs_down')"
71
190
  />
72
191
  </template>
192
+
193
+ <template v-else-if="isDropdownOpen">
194
+ <div class="gl-flex gl-h-6 gl-items-center">
195
+ <gl-icon
196
+ :name="isThumbsUp ? 'thumb-up' : 'thumb-down'"
197
+ :size="16"
198
+ :class="isThumbsUp ? 'gl-text-success' : 'gl-text-danger'"
199
+ data-testid="selected-thumb-icon"
200
+ />
201
+ </div>
202
+
203
+ <div
204
+ ref="dropdown"
205
+ class="feedback-reason-dropdown gl-l-0 gl-r-0 gl-border gl-absolute gl-top-full gl-mb-4 gl-mt-4 gl-w-full gl-rounded-lg gl-border-default gl-bg-subtle gl-p-3"
206
+ data-testid="feedback-reason-dropdown"
207
+ >
208
+ <div class="gl-mb-3 gl-flex gl-items-center gl-justify-between gl-gap-2">
209
+ <p class="gl-m-0 gl-text-sm gl-font-bold" data-testid="reason-header">
210
+ {{ reasonHeader }}
211
+ </p>
212
+ <div class="gl-flex gl-shrink-0 gl-items-center gl-gap-1">
213
+ <gl-button
214
+ v-if="isTellUsMoreVisible"
215
+ icon="go-back"
216
+ category="tertiary"
217
+ size="small"
218
+ :aria-label="$options.i18n.BACK"
219
+ data-testid="back-button"
220
+ @click="goBackToReasons"
221
+ />
222
+ <gl-button
223
+ icon="close"
224
+ category="tertiary"
225
+ size="small"
226
+ class="gl-mr-[-5px]"
227
+ :aria-label="$options.i18n.CLOSE"
228
+ data-testid="close-dropdown-button"
229
+ @click="closeDropdown"
230
+ />
231
+ </div>
232
+ </div>
233
+
234
+ <div
235
+ v-if="isReasonListVisible"
236
+ class="gl-flex gl-flex-wrap gl-gap-2"
237
+ data-testid="reason-buttons"
238
+ >
239
+ <gl-button
240
+ v-for="reason in reasonOptions"
241
+ :key="reason.key"
242
+ category="secondary"
243
+ size="small"
244
+ class="gl-rounded-full"
245
+ :icon="reason.isCustom ? 'pencil' : undefined"
246
+ :data-testid="`reason-${reason.key}`"
247
+ @click="selectReason(reason)"
248
+ >
249
+ {{ reason.label }}
250
+ </gl-button>
251
+ </div>
252
+
253
+ <div
254
+ v-if="isTellUsMoreVisible"
255
+ class="gl-flex gl-w-full gl-flex-col gl-gap-3"
256
+ data-testid="tell-us-more-view"
257
+ >
258
+ <gl-form
259
+ class="gl-flex gl-w-full gl-flex-col gl-gap-3"
260
+ @submit.prevent="canSubmitCustomFeedback ? submitCustomFeedback() : null"
261
+ >
262
+ <gl-form-textarea
263
+ v-model="customFeedback"
264
+ :state="isTextareaValid"
265
+ :placeholder="$options.i18n.FEEDBACK_PLACEHOLDER"
266
+ :rows="3"
267
+ :max-rows="5"
268
+ data-testid="custom-feedback-textarea"
269
+ />
270
+ <div class="gl-flex gl-items-center gl-justify-between">
271
+ <gl-button
272
+ variant="confirm"
273
+ size="small"
274
+ type="submit"
275
+ data-testid="submit-custom-feedback-button"
276
+ >
277
+ {{ $options.i18n.SUBMIT }}
278
+ </gl-button>
279
+ <span
280
+ :class="isOverCharacterLimit ? 'gl-text-danger' : 'gl-text-subtle'"
281
+ class="gl-text-sm"
282
+ data-testid="character-count"
283
+ >
284
+ {{ characterCountText }}
285
+ </span>
286
+ </div>
287
+ </gl-form>
288
+ </div>
289
+ </div>
290
+ </template>
291
+
73
292
  <div v-else class="gl-relative gl-flex gl-items-center gl-gap-2" data-testid="feedback-given">
74
293
  <div class="feedback-colored-icon" data-testid="feedback-colored-icon">
75
294
  <gl-icon
@@ -346,7 +346,7 @@ export default {
346
346
  <template>
347
347
  <div
348
348
  ref="content-wrapper"
349
- class="duo-chat-message-container gl-group gl-flex gl-flex-col gl-gap-4 gl-text-base gl-break-anywhere"
349
+ class="duo-chat-message-container gl-group gl-relative gl-flex gl-flex-col gl-gap-4 gl-text-base gl-break-anywhere"
350
350
  :class="{
351
351
  'gl-pr-7': isAssistantMessage,
352
352
  'gl-justify-end gl-pl-7': isUserMessage,
package/translations.js CHANGED
@@ -1,8 +1,23 @@
1
1
  /* eslint-disable import/no-default-export */
2
2
  export default {
3
+ 'AgenticBinaryFeedback.accurateInfo': 'Accurate information',
4
+ 'AgenticBinaryFeedback.back': 'Back',
5
+ 'AgenticBinaryFeedback.close': 'Close',
6
+ 'AgenticBinaryFeedback.goodExamples': 'Good examples',
7
+ 'AgenticBinaryFeedback.missingSteps': 'Missing steps',
8
+ 'AgenticBinaryFeedback.outdatedIncorrect': 'Outdated/incorrect',
9
+ 'AgenticBinaryFeedback.placeholder': 'Share your feedback...',
10
+ 'AgenticBinaryFeedback.savedTime': 'Saved me time',
11
+ 'AgenticBinaryFeedback.solvedProblem': 'Solved my problem',
12
+ 'AgenticBinaryFeedback.submit': 'Submit',
13
+ 'AgenticBinaryFeedback.tellUsMore': 'Tell us more',
3
14
  'AgenticBinaryFeedback.thanks': 'Thanks for the feedback!',
4
15
  'AgenticBinaryFeedback.thumbsDown': 'Not helpful',
16
+ 'AgenticBinaryFeedback.thumbsDownHeader': 'What would have been more helpful?',
5
17
  'AgenticBinaryFeedback.thumbsUp': 'Helpful',
18
+ 'AgenticBinaryFeedback.thumbsUpHeader': 'What made this helpful?',
19
+ 'AgenticBinaryFeedback.tooGeneric': 'Too generic',
20
+ 'AgenticBinaryFeedback.wrongContext': 'Wrong context',
6
21
  'AgenticDuoChat.chatCancelLabel': 'Cancel',
7
22
  'AgenticDuoChat.chatDefaultPredefinedPromptsChangePassword':
8
23
  'How do I change my password in GitLab?',