@gitlab/duo-ui 8.2.1 → 8.4.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.
@@ -25,17 +25,20 @@ import {
25
25
  CHAT_INCLUDE_MESSAGE,
26
26
  MESSAGE_MODEL_ROLES,
27
27
  } from './constants';
28
+ import { VIEW_TYPES } from './components/duo_chat_header/constants';
28
29
  import DuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
29
30
  import DuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
30
31
  import DuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation.vue';
32
+ import DuoChatHeader from './components/duo_chat_header/duo_chat_header.vue';
33
+ import DuoChatThreads from './components/duo_chat_threads/duo_chat_threads.vue';
31
34
 
32
35
  export const i18n = {
33
36
  CHAT_DEFAULT_TITLE: translate('DuoChat.chatDefaultTitle', 'GitLab Duo Chat'),
37
+ CHAT_HISTORY_TITLE: translate('GlDuoChat.chatHistoryTitle', 'Chat history'),
34
38
  CHAT_DISCLAMER: translate(
35
39
  'GlDuoChat.chatDisclamer',
36
40
  'Responses may be inaccurate. Verify before use.'
37
41
  ),
38
- CHAT_CLOSE_LABEL: translate('DuoChat.chatCloseLabel', 'Close the Code Explanation'),
39
42
  CHAT_EMPTY_STATE_TITLE: translate(
40
43
  'DuoChat.chatEmptyStateTitle',
41
44
  '👋 I am GitLab Duo Chat, your personal AI-powered assistant. How can I help you today?'
@@ -72,6 +75,17 @@ const itemsValidator = (items) => items.every(isMessage);
72
75
  // eslint-disable-next-line unicorn/no-array-callback-reference
73
76
  const slashCommandsValidator = (commands) => commands.every(isSlashCommand);
74
77
 
78
+ const isThread = (thread) =>
79
+ typeof thread === 'object' &&
80
+ typeof thread.id === 'string' &&
81
+ typeof thread.lastUpdatedAt === 'string' &&
82
+ typeof thread.createdAt === 'string' &&
83
+ typeof thread.conversationType === 'string' &&
84
+ (thread.title === null || typeof thread.title === 'string');
85
+
86
+ // eslint-disable-next-line unicorn/no-array-callback-reference
87
+ const threadListValidator = (threads) => threads.every(isThread);
88
+
75
89
  export default {
76
90
  name: 'DuoChat',
77
91
  components: {
@@ -84,6 +98,8 @@ export default {
84
98
  DuoChatLoader,
85
99
  DuoChatPredefinedPrompts,
86
100
  DuoChatConversation,
101
+ DuoChatHeader,
102
+ DuoChatThreads,
87
103
  GlCard,
88
104
  GlDropdownItem,
89
105
  VueResizable,
@@ -138,6 +154,23 @@ export default {
138
154
  default: () => [],
139
155
  validator: itemsValidator,
140
156
  },
157
+ /**
158
+ * The ID of the active thread (if any).
159
+ */
160
+ activeThreadId: {
161
+ type: String,
162
+ required: false,
163
+ default: () => '',
164
+ },
165
+ /**
166
+ * The chat page that should be shown.
167
+ */
168
+ multiThreadedView: {
169
+ type: String,
170
+ required: false,
171
+ default: VIEW_TYPES.LIST,
172
+ validator: (value) => [VIEW_TYPES.LIST, VIEW_TYPES.CHAT].includes(value),
173
+ },
141
174
  /**
142
175
  * Array of RequestIds that have been canceled.
143
176
  */
@@ -154,6 +187,16 @@ export default {
154
187
  required: false,
155
188
  default: '',
156
189
  },
190
+ /**
191
+ * Array of messages to display in the chat.
192
+ */
193
+ threadList: {
194
+ type: Array,
195
+ required: false,
196
+ default: () => [],
197
+ validator: threadListValidator,
198
+ },
199
+
157
200
  /**
158
201
  * Whether the chat is currently fetching a response from AI.
159
202
  */
@@ -236,6 +279,14 @@ export default {
236
279
  required: false,
237
280
  default: '',
238
281
  },
282
+ /**
283
+ * Whether the chat is running in multi-threaded mode
284
+ */
285
+ isMultithreaded: {
286
+ type: Boolean,
287
+ required: false,
288
+ default: false,
289
+ },
239
290
  },
240
291
  data() {
241
292
  return {
@@ -247,9 +298,13 @@ export default {
247
298
  compositionJustEnded: false,
248
299
  contextItemsMenuIsOpen: false,
249
300
  contextItemMenuRef: null,
301
+ currentView: this.multiThreadedView,
250
302
  };
251
303
  },
252
304
  computed: {
305
+ shouldShowThreadList() {
306
+ return this.isMultithreaded && this.currentView === VIEW_TYPES.LIST;
307
+ },
253
308
  withSlashCommands() {
254
309
  return this.slashCommands.length > 0;
255
310
  },
@@ -331,6 +386,9 @@ export default {
331
386
  },
332
387
  },
333
388
  watch: {
389
+ multiThreadedView(newView) {
390
+ this.currentView = newView;
391
+ },
334
392
  isLoading(newVal) {
335
393
  if (!newVal && !this.isStreaming) {
336
394
  this.displaySubmitButton = true; // Re-enable submit button when loading stops
@@ -363,6 +421,12 @@ export default {
363
421
  },
364
422
 
365
423
  methods: {
424
+ onGoBack() {
425
+ this.$emit('back-to-list');
426
+ },
427
+ onNewChat() {
428
+ this.$emit('new-chat');
429
+ },
366
430
  updateSize(e) {
367
431
  this.$emit('chat-resize', e);
368
432
  },
@@ -551,6 +615,20 @@ export default {
551
615
  setContextItemsMenuRef(ref) {
552
616
  this.contextItemMenuRef = ref;
553
617
  },
618
+ onSelectThread(thread) {
619
+ /**
620
+ * Emitted when a thread is selected from the history.
621
+ * @param {Object} thread The selected thread object
622
+ */
623
+ this.$emit('thread-selected', thread);
624
+ },
625
+ onDeleteThread(threadId) {
626
+ /**
627
+ * Emitted when a thread is deleted from the history.
628
+ * @param {String} threadId The ID of the thread to delete
629
+ */
630
+ this.$emit('delete-thread', threadId);
631
+ },
554
632
  },
555
633
  i18n,
556
634
  };
@@ -585,184 +663,177 @@ export default {
585
663
  role="complementary"
586
664
  data-testid="chat-component"
587
665
  >
588
- <header
666
+ <duo-chat-header
589
667
  v-if="showHeader"
590
- data-testid="chat-header"
591
- :class="{
592
- 'gl-z-200': !shouldRenderResizable,
593
- }"
594
- class="duo-chat-drawer-header duo-chat-drawer-header-sticky gl-border-0 gl-bg-default !gl-p-0"
668
+ :active-thread-id="activeThreadId"
669
+ :title="
670
+ isMultithreaded && currentView === 'list' ? $options.i18n.CHAT_HISTORY_TITLE : title
671
+ "
672
+ :error="error"
673
+ :is-multithreaded="isMultithreaded"
674
+ :current-view="currentView"
675
+ :should-render-resizable="shouldRenderResizable"
676
+ :badge-type="isMultithreaded ? null : badgeType"
677
+ @go-back="onGoBack"
678
+ @new-chat="onNewChat"
679
+ @close="hideChat"
595
680
  >
596
- <div class="drawer-title gl-flex gl-items-center gl-justify-start gl-p-5">
597
- <h3 class="gl-my-0 gl-text-size-h2">{{ title }}</h3>
598
- <gl-experiment-badge v-if="badgeType" :type="badgeType" container-id="chat-component" />
599
- <gl-button
600
- category="tertiary"
601
- variant="default"
602
- icon="close"
603
- size="small"
604
- class="gl-ml-auto"
605
- data-testid="chat-close-button"
606
- :aria-label="$options.i18n.CHAT_CLOSE_LABEL"
607
- @click="hideChat"
608
- />
609
- </div>
610
-
611
- <!--
612
- @slot Subheader to be rendered right after the title. It is sticky and stays on top of the chat no matter the number of messages.
613
- -->
614
- <slot name="subheader"></slot>
681
+ <template #subheader>
682
+ <slot name="subheader"></slot>
683
+ </template>
684
+ </duo-chat-header>
615
685
 
616
- <!-- Ensure that the global error is not scrolled away -->
617
- <gl-alert
618
- v-if="error"
619
- key="error"
620
- :dismissible="false"
621
- variant="danger"
622
- class="!gl-pl-9"
623
- role="alert"
624
- data-testid="chat-error"
686
+ <div v-if="shouldShowThreadList" class="gl-h-full">
687
+ <duo-chat-threads
688
+ :threads="threadList"
689
+ @new-chat="onNewChat"
690
+ @select-thread="onSelectThread"
691
+ @delete-thread="onDeleteThread"
692
+ @close="hideChat"
693
+ />
694
+ </div>
695
+ <span v-else class="gl-h-full gl-flex gl-flex-col gl-justify-end">
696
+ <div
697
+ class="duo-chat-drawer-body gl-bg-default"
698
+ data-testid="chat-history"
699
+ @scroll="handleScrollingTrottled"
625
700
  >
626
- <span v-safe-html="error"></span>
627
- </gl-alert>
628
- </header>
629
-
630
- <div
631
- class="duo-chat-drawer-body gl-bg-default"
632
- data-testid="chat-history"
633
- @scroll="handleScrollingTrottled"
634
- >
635
- <transition-group
636
- tag="section"
637
- name="message"
638
- class="duo-chat-history gl-flex gl-flex-col gl-justify-end"
639
- :class="[
640
- {
641
- 'gl-h-full': !hasMessages,
642
- 'force-scroll-bar': hasMessages,
643
- },
644
- ]"
701
+ <transition-group
702
+ tag="section"
703
+ name="message"
704
+ class="duo-chat-history gl-flex gl-flex-col gl-justify-end"
705
+ :class="[
706
+ {
707
+ 'gl-h-full': !hasMessages,
708
+ 'force-scroll-bar': hasMessages,
709
+ },
710
+ ]"
711
+ >
712
+ <duo-chat-conversation
713
+ v-for="(conversation, index) in conversations"
714
+ :key="`conversation-${index}`"
715
+ :enable-code-insertion="enableCodeInsertion"
716
+ :messages="conversation"
717
+ :canceled-request-ids="canceledRequestIds"
718
+ :show-delimiter="index > 0"
719
+ @track-feedback="onTrackFeedback"
720
+ @insert-code-snippet="onInsertCodeSnippet"
721
+ @copy-code-snippet="onCopyCodeSnippet"
722
+ @get-context-item-content="onGetContextItemContent"
723
+ />
724
+ <template v-if="!hasMessages && !isLoading">
725
+ <div
726
+ key="empty-state-message"
727
+ class="duo-chat-message gl-rounded-bl-none gl-border-1 gl-border-solid gl-border-gray-50 gl-bg-gray-10 gl-p-4 gl-leading-20 gl-text-gray-900 gl-break-anywhere"
728
+ data-testid="gl-duo-chat-empty-state"
729
+ >
730
+ <p
731
+ v-if="emptyStateTitle"
732
+ data-testid="gl-duo-chat-empty-state-title"
733
+ class="gl-m-0"
734
+ >
735
+ {{ emptyStateTitle }}
736
+ </p>
737
+ <duo-chat-predefined-prompts
738
+ key="predefined-prompts"
739
+ :prompts="predefinedPrompts"
740
+ @click="sendPredefinedPrompt"
741
+ />
742
+ </div>
743
+ </template>
744
+ <duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
745
+ <div key="anchor" ref="anchor" class="scroll-anchor"></div>
746
+ </transition-group>
747
+ </div>
748
+ <footer
749
+ v-if="isChatAvailable"
750
+ data-testid="chat-footer"
751
+ class="duo-chat-drawer-footer duo-chat-drawer-footer-sticky gl-border-0 gl-bg-default gl-pb-3"
752
+ :class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
645
753
  >
646
- <duo-chat-conversation
647
- v-for="(conversation, index) in conversations"
648
- :key="`conversation-${index}`"
649
- :enable-code-insertion="enableCodeInsertion"
650
- :messages="conversation"
651
- :canceled-request-ids="canceledRequestIds"
652
- :show-delimiter="index > 0"
653
- @track-feedback="onTrackFeedback"
654
- @insert-code-snippet="onInsertCodeSnippet"
655
- @copy-code-snippet="onCopyCodeSnippet"
656
- @get-context-item-content="onGetContextItemContent"
657
- />
658
- <template v-if="!hasMessages && !isLoading">
659
- <div
660
- key="empty-state-message"
661
- class="duo-chat-message gl-rounded-bl-none gl-border-1 gl-border-solid gl-border-subtle gl-bg-subtle gl-p-4 gl-leading-20 gl-text-primary gl-break-anywhere"
662
- data-testid="gl-duo-chat-empty-state"
663
- >
664
- <p v-if="emptyStateTitle" data-testid="gl-duo-chat-empty-state-title" class="gl-m-0">
665
- {{ emptyStateTitle }}
666
- </p>
667
- <duo-chat-predefined-prompts
668
- key="predefined-prompts"
669
- :prompts="predefinedPrompts"
670
- @click="sendPredefinedPrompt"
671
- />
672
- </div>
673
- </template>
674
- <duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
675
- <div key="anchor" ref="anchor" class="scroll-anchor"></div>
676
- </transition-group>
677
- </div>
678
- <footer
679
- v-if="isChatAvailable"
680
- data-testid="chat-footer"
681
- class="duo-chat-drawer-footer duo-chat-drawer-footer-sticky gl-border-0 gl-bg-default gl-pb-3"
682
- :class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
683
- >
684
- <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
685
- <div class="gl-relative gl-max-w-full">
686
- <!--
754
+ <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
755
+ <div class="gl-relative gl-max-w-full">
756
+ <!--
687
757
  @slot For integrating `<gl-context-items-menu>` component if pinned-context should be available. The following scopedSlot properties are provided: `isOpen`, `onClose`, `setRef`, `focusPrompt`, which should be passed to the `<gl-context-items-menu>` component when rendering, e.g. `<template #context-items-menu="{ isOpen, onClose, setRef, focusPrompt }">` `<duo-chat-context-item-menu :ref="setRef" :open="isOpen" @close="onClose" @focus-prompt="focusPrompt" ...`
688
758
  -->
689
- <slot
690
- name="context-items-menu"
691
- :is-open="contextItemsMenuIsOpen"
692
- :on-close="closeContextItemsMenuOpen"
693
- :set-ref="setContextItemsMenuRef"
694
- :focus-prompt="focusChatInput"
695
- ></slot>
696
- </div>
759
+ <slot
760
+ name="context-items-menu"
761
+ :is-open="contextItemsMenuIsOpen"
762
+ :on-close="closeContextItemsMenuOpen"
763
+ :set-ref="setContextItemsMenuRef"
764
+ :focus-prompt="focusChatInput"
765
+ ></slot>
766
+ </div>
697
767
 
698
- <gl-form-input-group>
699
- <div
700
- class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-align-top"
701
- :data-value="prompt"
702
- >
703
- <gl-card
704
- v-if="shouldShowSlashCommands"
705
- ref="commands"
706
- class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
707
- body-class="!gl-p-2"
768
+ <gl-form-input-group>
769
+ <div
770
+ class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-align-top"
771
+ :data-value="prompt"
708
772
  >
709
- <gl-dropdown-item
710
- v-for="(command, index) in filteredSlashCommands"
711
- :key="command.name"
712
- :class="{ 'active-command': index === activeCommandIndex }"
713
- @mouseenter.native="activeCommandIndex = index"
714
- @click="selectSlashCommand(index)"
773
+ <gl-card
774
+ v-if="shouldShowSlashCommands"
775
+ ref="commands"
776
+ class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
777
+ body-class="!gl-p-2"
715
778
  >
716
- <span class="gl-flex gl-justify-between">
717
- <span class="gl-block">{{ command.name }}</span>
718
- <small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
719
- command.description
720
- }}</small>
721
- </span>
722
- </gl-dropdown-item>
723
- </gl-card>
779
+ <gl-dropdown-item
780
+ v-for="(command, index) in filteredSlashCommands"
781
+ :key="command.name"
782
+ :class="{ 'active-command': index === activeCommandIndex }"
783
+ @mouseenter.native="activeCommandIndex = index"
784
+ @click="selectSlashCommand(index)"
785
+ >
786
+ <span class="gl-flex gl-justify-between">
787
+ <span class="gl-block">{{ command.name }}</span>
788
+ <small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
789
+ command.description
790
+ }}</small>
791
+ </span>
792
+ </gl-dropdown-item>
793
+ </gl-card>
724
794
 
725
- <gl-form-textarea
726
- ref="prompt"
727
- v-model="prompt"
728
- data-testid="chat-prompt-input"
729
- class="gl-absolute !gl-h-full gl-rounded-br-none gl-rounded-tr-none !gl-bg-transparent !gl-py-4 !gl-shadow-none"
730
- :class="{ 'gl-truncate': !prompt }"
731
- :placeholder="inputPlaceholder"
732
- autofocus
733
- @keydown.enter.exact.native.prevent
734
- @keyup.native="onInputKeyup"
735
- @compositionend="compositionEnd"
736
- />
737
- </div>
738
- <template #append>
739
- <gl-button
740
- v-if="displaySubmitButton"
741
- icon="paper-airplane"
742
- category="primary"
743
- variant="confirm"
744
- class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
745
- type="submit"
746
- data-testid="chat-prompt-submit-button"
747
- :aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
748
- />
749
- <gl-button
750
- v-else
751
- icon="stop"
752
- category="primary"
753
- variant="default"
754
- class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
755
- data-testid="chat-prompt-cancel-button"
756
- :aria-label="$options.i18n.CHAT_CANCEL_LABEL"
757
- @click="cancelPrompt"
758
- />
759
- </template>
760
- </gl-form-input-group>
761
- </gl-form>
762
- <p class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary">
763
- {{ $options.i18n.CHAT_DISCLAMER }}
764
- </p>
765
- </footer>
795
+ <gl-form-textarea
796
+ ref="prompt"
797
+ v-model="prompt"
798
+ data-testid="chat-prompt-input"
799
+ class="gl-absolute !gl-h-full gl-rounded-br-none gl-rounded-tr-none !gl-bg-transparent !gl-py-4 !gl-shadow-none"
800
+ :class="{ 'gl-truncate': !prompt }"
801
+ :placeholder="inputPlaceholder"
802
+ autofocus
803
+ @keydown.enter.exact.native.prevent
804
+ @keyup.native="onInputKeyup"
805
+ @compositionend="compositionEnd"
806
+ />
807
+ </div>
808
+ <template #append>
809
+ <gl-button
810
+ v-if="displaySubmitButton"
811
+ icon="paper-airplane"
812
+ category="primary"
813
+ variant="confirm"
814
+ class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
815
+ type="submit"
816
+ data-testid="chat-prompt-submit-button"
817
+ :aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
818
+ />
819
+ <gl-button
820
+ v-else
821
+ icon="stop"
822
+ category="primary"
823
+ variant="default"
824
+ class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
825
+ data-testid="chat-prompt-cancel-button"
826
+ :aria-label="$options.i18n.CHAT_CANCEL_LABEL"
827
+ @click="cancelPrompt"
828
+ />
829
+ </template>
830
+ </gl-form-input-group>
831
+ </gl-form>
832
+ <p class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary">
833
+ {{ $options.i18n.CHAT_DISCLAMER }}
834
+ </p>
835
+ </footer>
836
+ </span>
766
837
  </aside>
767
838
  </component>
768
839
  </template>
@@ -176,3 +176,86 @@ export const INCLUDE_SLASH_COMMAND = {
176
176
  name: CHAT_INCLUDE_MESSAGE,
177
177
  description: 'Include additional context in the conversation.',
178
178
  };
179
+
180
+ export const THREADLIST = [
181
+ {
182
+ __typename: 'AiConversationsThread',
183
+ id: 'gid://gitlab/Ai::Conversation::Thread/14',
184
+ lastUpdatedAt: '2025-01-31T11:12:05Z',
185
+ createdAt: '2025-01-31T11:12:00Z',
186
+ conversationType: 'DUO_CHAT',
187
+ title: 'On the Proper Filing of Form B-789 in Triplicate',
188
+ },
189
+ {
190
+ __typename: 'AiConversationsThread',
191
+ id: 'gid://gitlab/Ai::Conversation::Thread/13',
192
+ lastUpdatedAt: '2025-01-31T14:30:42Z',
193
+ createdAt: '2025-01-31T14:30:36Z',
194
+ conversationType: 'DUO_CHAT',
195
+ title: 'The Case of the Missing Permission Slip',
196
+ },
197
+ {
198
+ __typename: 'AiConversationsThread',
199
+ id: 'gid://gitlab/Ai::Conversation::Thread/12',
200
+ lastUpdatedAt: '2025-01-31T14:30:08Z',
201
+ createdAt: '2025-01-31T14:30:03Z',
202
+ conversationType: 'DUO_CHAT',
203
+ title: 'Regarding the Metamorphosis of Code Review #2187',
204
+ },
205
+ {
206
+ __typename: 'AiConversationsThread',
207
+ id: 'gid://gitlab/Ai::Conversation::Thread/11',
208
+ lastUpdatedAt: '2025-01-24T14:29:51Z',
209
+ createdAt: '2025-01-24T14:29:36Z',
210
+ conversationType: 'DUO_CHAT',
211
+ title: "An Investigation into the Disappearance of Yesterday's Commits",
212
+ },
213
+ {
214
+ __typename: 'AiConversationsThread',
215
+ id: 'gid://gitlab/Ai::Conversation::Thread/10',
216
+ lastUpdatedAt: '2025-01-24T14:29:25Z',
217
+ createdAt: '2025-01-24T14:28:50Z',
218
+ conversationType: 'DUO_CHAT',
219
+ title: 'The Curious Incident of the Bug in the Runtime',
220
+ },
221
+ {
222
+ __typename: 'AiConversationsThread',
223
+ id: 'gid://gitlab/Ai::Conversation::Thread/9',
224
+ lastUpdatedAt: '2025-01-15T14:24:30Z',
225
+ createdAt: '2025-01-15T14:24:26Z',
226
+ conversationType: 'DUO_CHAT',
227
+ title: 'Notes on the Inexplicable Behavior of the Legacy System',
228
+ },
229
+ {
230
+ __typename: 'AiConversationsThread',
231
+ id: 'gid://gitlab/Ai::Conversation::Thread/8',
232
+ lastUpdatedAt: '2025-01-15T14:24:21Z',
233
+ createdAt: '2025-01-15T14:24:17Z',
234
+ conversationType: 'DUO_CHAT',
235
+ title: 'The Trial of the Undefined Variable',
236
+ },
237
+ {
238
+ __typename: 'AiConversationsThread',
239
+ id: 'gid://gitlab/Ai::Conversation::Thread/7',
240
+ lastUpdatedAt: '2025-01-15T14:12:13Z',
241
+ createdAt: '2025-01-15T14:12:08Z',
242
+ conversationType: 'DUO_CHAT',
243
+ title: 'Waiting for the Database Response',
244
+ },
245
+ {
246
+ __typename: 'AiConversationsThread',
247
+ id: 'gid://gitlab/Ai::Conversation::Thread/6',
248
+ lastUpdatedAt: '2025-01-08T14:11:46Z',
249
+ createdAt: '2025-01-08T14:11:38Z',
250
+ conversationType: 'DUO_CHAT',
251
+ title: 'The Endless Corridor of Dependencies',
252
+ },
253
+ {
254
+ __typename: 'AiConversationsThread',
255
+ id: 'gid://gitlab/Ai::Conversation::Thread/5',
256
+ lastUpdatedAt: '2025-01-08T14:11:24Z',
257
+ createdAt: '2025-01-08T14:11:24Z',
258
+ conversationType: 'DUO_CHAT',
259
+ title: 'Before the Configuration Committee',
260
+ },
261
+ ];
@@ -0,0 +1,24 @@
1
+ export function getOrdinalSuffix(day) {
2
+ if (day > 3 && day < 21) return 'th';
3
+ switch (day % 10) {
4
+ case 1:
5
+ return 'st';
6
+ case 2:
7
+ return 'nd';
8
+ case 3:
9
+ return 'rd';
10
+ default:
11
+ return 'th';
12
+ }
13
+ }
14
+
15
+ export function formatDate(dateStr) {
16
+ const date = new Date(dateStr);
17
+ const day = date.getDate();
18
+ const suffix = getOrdinalSuffix(day);
19
+
20
+ const month = new Intl.DateTimeFormat('en-US', { month: 'long' }).format(date);
21
+ const year = new Intl.DateTimeFormat('en-US', { year: 'numeric' }).format(date);
22
+
23
+ return `${month} ${day}${suffix}, ${year}`;
24
+ }
package/translations.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable import/no-default-export */
2
2
  export default {
3
+ 'DuoChat.chatBackLabel': 'Back to History',
3
4
  'DuoChat.chatCancelLabel': 'Cancel',
4
- 'DuoChat.chatCloseLabel': 'Close the Code Explanation',
5
5
  'DuoChat.chatDefaultPredefinedPromptsChangePassword': 'How do I change my password in GitLab?',
6
6
  'DuoChat.chatDefaultPredefinedPromptsCloneRepository': 'How do I clone a repository?',
7
7
  'DuoChat.chatDefaultPredefinedPromptsCreateTemplate': 'How do I create a template?',
@@ -9,9 +9,18 @@ export default {
9
9
  'DuoChat.chatDefaultTitle': 'GitLab Duo Chat',
10
10
  'DuoChat.chatEmptyStateTitle':
11
11
  '👋 I am GitLab Duo Chat, your personal AI-powered assistant. How can I help you today?',
12
+ 'DuoChat.chatHistoryInfo': 'Chats with no activity in the last 30 days are deleted.',
13
+ 'DuoChat.chatNewLabel': 'New Chat',
12
14
  'DuoChat.chatPromptPlaceholderDefault': 'GitLab Duo Chat',
13
15
  'DuoChat.chatPromptPlaceholderWithCommands': 'Type /help to learn more',
14
16
  'DuoChat.chatSubmitLabel': 'Send chat message.',
17
+ 'DuoChat.chatTitle': 'GitLab Duo Chat',
18
+ 'DuoChat.closeChatHeaderLabel': 'Close Chat',
19
+ 'DuoChat.emptyHistoryAlt':
20
+ 'Clock icon with circular arrow, indicating chat history or time-based functionality',
21
+ 'DuoChat.emptyHistoryCopy': 'Your previous chats will appear here.',
22
+ 'DuoChat.emptyHistoryTitle': 'See your chat history',
23
+ 'DuoChat.threadDeleteLabel': 'Delete this thread.',
15
24
  'DuoChatContextItemDetailsModal.contentErrorMessage': 'Item content could not be displayed.',
16
25
  'DuoChatContextItemDetailsModal.title': 'Preview',
17
26
  'DuoChatContextItemMenu.emptyStateMessage': 'No results found',
@@ -38,6 +47,7 @@ export default {
38
47
  'DuoChatMessage.modalTitle': 'Give feedback on GitLab Duo Chat',
39
48
  'DuoChatMessageSources.messageSources': null,
40
49
  'GlDuoChat.chatDisclamer': 'Responses may be inaccurate. Verify before use.',
50
+ 'GlDuoChat.chatHistoryTitle': 'Chat history',
41
51
  'GlDuoWorkflowPanel.collapseButtonTitle': 'Collapse',
42
52
  'GlDuoWorkflowPanel.expandButtonTitle': 'Expand',
43
53
  'GlDuoWorkflowPrompt.cancelButtonText': 'Cancel',