mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Update the UI to support the change for Copilot as a universal copilot (#11618)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
		| @@ -3,7 +3,7 @@ | ||||
| } | ||||
|  | ||||
| .tabs--container--with-border { | ||||
|   @apply border-b border-n-weak; | ||||
|   @apply border-b border-b-n-weak; | ||||
| } | ||||
|  | ||||
| .tabs--container--compact.tab--chat-type { | ||||
|   | ||||
| @@ -0,0 +1,87 @@ | ||||
| <script setup> | ||||
| import Button from 'dashboard/components-next/button/Button.vue'; | ||||
| import { useUISettings } from 'dashboard/composables/useUISettings'; | ||||
| import { computed } from 'vue'; | ||||
| import { FEATURE_FLAGS } from 'dashboard/featureFlags'; | ||||
| import { useMapGetter } from 'dashboard/composables/store'; | ||||
| import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; | ||||
|  | ||||
| const { updateUISettings } = useUISettings(); | ||||
|  | ||||
| const currentAccountId = useMapGetter('getCurrentAccountId'); | ||||
| const isFeatureEnabledonAccount = useMapGetter( | ||||
|   'accounts/isFeatureEnabledonAccount' | ||||
| ); | ||||
|  | ||||
| const showCopilotTab = computed(() => | ||||
|   isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN) | ||||
| ); | ||||
|  | ||||
| const { uiSettings } = useUISettings(); | ||||
| const isContactSidebarOpen = computed( | ||||
|   () => uiSettings.value.is_contact_sidebar_open | ||||
| ); | ||||
| const isCopilotPanelOpen = computed( | ||||
|   () => uiSettings.value.is_copilot_panel_open | ||||
| ); | ||||
|  | ||||
| const toggleConversationSidebarToggle = () => { | ||||
|   updateUISettings({ | ||||
|     is_contact_sidebar_open: !isContactSidebarOpen.value, | ||||
|     is_copilot_panel_open: false, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const handleConversationSidebarToggle = () => { | ||||
|   updateUISettings({ | ||||
|     is_contact_sidebar_open: true, | ||||
|     is_copilot_panel_open: false, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const handleCopilotSidebarToggle = () => { | ||||
|   updateUISettings({ | ||||
|     is_contact_sidebar_open: false, | ||||
|     is_copilot_panel_open: true, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const keyboardEvents = { | ||||
|   'Alt+KeyO': { | ||||
|     action: toggleConversationSidebarToggle, | ||||
|   }, | ||||
| }; | ||||
| useKeyboardEvents(keyboardEvents); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="flex flex-col justify-center items-center absolute top-24 ltr:right-2 rtl:left-2 bg-n-solid-2 border border-n-weak rounded-full gap-2 p-1" | ||||
|   > | ||||
|     <Button | ||||
|       v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')" | ||||
|       ghost | ||||
|       slate | ||||
|       sm | ||||
|       class="!rounded-full" | ||||
|       :class="{ | ||||
|         'bg-n-alpha-2': isContactSidebarOpen, | ||||
|       }" | ||||
|       icon="i-ph-user-bold" | ||||
|       @click="handleConversationSidebarToggle" | ||||
|     /> | ||||
|     <Button | ||||
|       v-if="showCopilotTab" | ||||
|       v-tooltip.bottom="$t('CONVERSATION.SIDEBAR.COPILOT')" | ||||
|       ghost | ||||
|       slate | ||||
|       class="!rounded-full" | ||||
|       :class="{ | ||||
|         'bg-n-alpha-2 !text-n-iris-9': isCopilotPanelOpen, | ||||
|       }" | ||||
|       sm | ||||
|       icon="i-woot-captain" | ||||
|       @click="handleCopilotSidebarToggle" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,21 +1,29 @@ | ||||
| <script setup> | ||||
| import CopilotHeader from './CopilotHeader.vue'; | ||||
| import SidebarActionsHeader from './SidebarActionsHeader.vue'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Story | ||||
|     title="Captain/Copilot/CopilotHeader" | ||||
|     title="Components/SidebarActionsHeader" | ||||
|     :layout="{ type: 'grid', width: '800px' }" | ||||
|   > | ||||
|     <!-- Default State --> | ||||
|     <Variant title="Default State"> | ||||
|       <CopilotHeader /> | ||||
|       <SidebarActionsHeader title="Default State" /> | ||||
|     </Variant> | ||||
| 
 | ||||
|     <!-- With New Conversation Button --> | ||||
|     <Variant title="With New Conversation Button"> | ||||
|       <!-- eslint-disable-next-line vue/prefer-true-attribute-shorthand --> | ||||
|       <CopilotHeader :has-messages="true" /> | ||||
|       <SidebarActionsHeader | ||||
|         title="With New Conversation Button" | ||||
|         :buttons="[ | ||||
|           { | ||||
|             key: 'new_conversation', | ||||
|             icon: 'i-lucide-plus', | ||||
|           }, | ||||
|         ]" | ||||
|       /> | ||||
|     </Variant> | ||||
|   </Story> | ||||
| </template> | ||||
| @@ -0,0 +1,47 @@ | ||||
| <script setup> | ||||
| import Button from './button/Button.vue'; | ||||
| defineProps({ | ||||
|   title: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   buttons: { | ||||
|     type: Array, | ||||
|     default: () => [], | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits(['click', 'close']); | ||||
|  | ||||
| const handleButtonClick = button => { | ||||
|   emit('click', button.key); | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="flex items-center justify-between px-4 py-2 border-b border-n-weak h-12" | ||||
|   > | ||||
|     <div class="flex items-center justify-between gap-2 flex-1"> | ||||
|       <span class="font-medium text-sm text-n-slate-12">{{ title }}</span> | ||||
|       <div class="flex items-center"> | ||||
|         <Button | ||||
|           v-for="button in buttons" | ||||
|           :key="button.key" | ||||
|           v-tooltip="button.tooltip" | ||||
|           :icon="button.icon" | ||||
|           ghost | ||||
|           sm | ||||
|           @click="handleButtonClick(button)" | ||||
|         /> | ||||
|         <Button | ||||
|           v-tooltip="$t('GENERAL.CLOSE')" | ||||
|           icon="i-lucide-x" | ||||
|           ghost | ||||
|           sm | ||||
|           @click="$emit('close')" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -3,13 +3,15 @@ import { nextTick, ref, watch } from 'vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import { useTrack } from 'dashboard/composables'; | ||||
| import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; | ||||
| import { useUISettings } from 'dashboard/composables/useUISettings'; | ||||
|  | ||||
| import CopilotInput from './CopilotInput.vue'; | ||||
| import CopilotLoader from './CopilotLoader.vue'; | ||||
| import CopilotAgentMessage from './CopilotAgentMessage.vue'; | ||||
| import CopilotAssistantMessage from './CopilotAssistantMessage.vue'; | ||||
| import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue'; | ||||
| import Icon from '../icon/Icon.vue'; | ||||
| import Icon from 'dashboard/components-next/icon/Icon.vue'; | ||||
| import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   supportAgent: { | ||||
| @@ -54,10 +56,6 @@ const useSuggestion = opt => { | ||||
|   useTrack(COPILOT_EVENTS.SEND_SUGGESTED); | ||||
| }; | ||||
|  | ||||
| const handleReset = () => { | ||||
|   emit('reset'); | ||||
| }; | ||||
|  | ||||
| const chatContainer = ref(null); | ||||
|  | ||||
| const scrollToBottom = async () => { | ||||
| @@ -82,6 +80,21 @@ const promptOptions = [ | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const { updateUISettings } = useUISettings(); | ||||
|  | ||||
| const closeCopilotPanel = () => { | ||||
|   updateUISettings({ | ||||
|     is_copilot_panel_open: false, | ||||
|     is_contact_sidebar_open: false, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const handleSidebarAction = action => { | ||||
|   if (action === 'reset') { | ||||
|     emit('reset'); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| watch( | ||||
|   [() => props.messages, () => props.isCaptainTyping], | ||||
|   () => { | ||||
| @@ -93,6 +106,18 @@ watch( | ||||
|  | ||||
| <template> | ||||
|   <div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full"> | ||||
|     <SidebarActionsHeader | ||||
|       :title="$t('CAPTAIN.COPILOT.TITLE')" | ||||
|       :buttons="[ | ||||
|         { | ||||
|           key: 'reset', | ||||
|           icon: 'i-lucide-refresh-ccw', | ||||
|           tooltip: $t('CAPTAIN.COPILOT.RESET'), | ||||
|         }, | ||||
|       ]" | ||||
|       @click="handleSidebarAction" | ||||
|       @close="closeCopilotPanel" | ||||
|     /> | ||||
|     <div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto"> | ||||
|       <template v-for="message in messages" :key="message.id"> | ||||
|         <CopilotAgentMessage | ||||
| @@ -139,14 +164,6 @@ watch( | ||||
|           @set-assistant="$event => emit('setAssistant', $event)" | ||||
|         /> | ||||
|         <div v-else /> | ||||
|         <button | ||||
|           v-if="messages.length" | ||||
|           class="text-xs flex items-center gap-1 hover:underline" | ||||
|           @click="handleReset" | ||||
|         > | ||||
|           <i class="i-lucide-refresh-ccw" /> | ||||
|           <span>{{ $t('CAPTAIN.COPILOT.RESET') }}</span> | ||||
|         </button> | ||||
|       </div> | ||||
|       <CopilotInput class="mb-1 w-full" @send="sendMessage" /> | ||||
|     </div> | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| <script setup> | ||||
| import Button from '../button/Button.vue'; | ||||
| defineProps({ | ||||
|   hasMessages: { | ||||
|     type: Boolean, | ||||
|     default: false, | ||||
|   }, | ||||
| }); | ||||
| defineEmits(['reset', 'close']); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="flex items-center justify-between px-5 py-2 border-b border-n-weak h-12" | ||||
|   > | ||||
|     <div class="flex items-center justify-between gap-2 flex-1"> | ||||
|       <span class="font-medium text-sm text-n-slate-12"> | ||||
|         {{ $t('CAPTAIN.COPILOT.TITLE') }} | ||||
|       </span> | ||||
|       <div class="flex items-center"> | ||||
|         <Button | ||||
|           v-if="hasMessages" | ||||
|           icon="i-lucide-plus" | ||||
|           ghost | ||||
|           sm | ||||
|           @click="$emit('reset')" | ||||
|         /> | ||||
|         <Button icon="i-lucide-x" ghost sm @click="$emit('close')" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -519,7 +519,7 @@ const menuItems = computed(() => { | ||||
|       </div> | ||||
|     </section> | ||||
|     <nav class="grid flex-grow gap-2 px-2 pb-5 overflow-y-scroll no-scrollbar"> | ||||
|       <ul class="flex flex-col gap-2 m-0 list-none"> | ||||
|       <ul class="flex flex-col gap-1.5 m-0 list-none"> | ||||
|         <SidebarGroup | ||||
|           v-for="item in menuItems" | ||||
|           :key="item.name" | ||||
|   | ||||
| @@ -814,7 +814,7 @@ watch(conversationFilters, (newVal, oldVal) => { | ||||
|     class="flex flex-col flex-shrink-0 bg-n-solid-1 conversations-list-wrap" | ||||
|     :class="[ | ||||
|       { hidden: !showConversationList }, | ||||
|       isOnExpandedLayout ? 'basis-full' : 'w-[360px] 2xl:w-[420px]', | ||||
|       isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]', | ||||
|     ]" | ||||
|   > | ||||
|     <slot /> | ||||
|   | ||||
| @@ -80,16 +80,14 @@ const toggleConversationLayout = () => { | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="flex items-center justify-between gap-2 px-4" | ||||
|     class="flex items-center justify-between gap-2 px-3 h-12" | ||||
|     :class="{ | ||||
|       'pb-3 border-b border-n-strong': hasAppliedFiltersOrActiveFolders, | ||||
|       'pt-3 pb-2': showV4View, | ||||
|       'mb-2 pb-0': !showV4View, | ||||
|       'border-b border-n-strong': hasAppliedFiltersOrActiveFolders, | ||||
|     }" | ||||
|   > | ||||
|     <div class="flex items-center justify-center min-w-0"> | ||||
|       <h1 | ||||
|         class="text-lg font-medium truncate text-n-slate-12" | ||||
|         class="text-base font-medium truncate text-n-slate-12" | ||||
|         :title="pageTitle" | ||||
|       > | ||||
|         {{ pageTitle }} | ||||
|   | ||||
| @@ -48,12 +48,13 @@ useKeyboardEvents(keyboardEvents); | ||||
| <template> | ||||
|   <woot-tabs | ||||
|     :index="activeTabIndex" | ||||
|     class="w-full px-4 py-0 tab--chat-type" | ||||
|     class="w-full px-3 -mt-1 py-0 tab--chat-type" | ||||
|     @change="onTabChange" | ||||
|   > | ||||
|     <woot-tabs-item | ||||
|       v-for="(item, index) in items" | ||||
|       :key="item.key" | ||||
|       class="text-sm" | ||||
|       :index="index" | ||||
|       :name="item.name" | ||||
|       :count="item.count" | ||||
|   | ||||
| @@ -4,17 +4,14 @@ import ConversationHeader from './ConversationHeader.vue'; | ||||
| import DashboardAppFrame from '../DashboardApp/Frame.vue'; | ||||
| import EmptyState from './EmptyState/EmptyState.vue'; | ||||
| import MessagesView from './MessagesView.vue'; | ||||
| import ConversationSidebar from './ConversationSidebar.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     ConversationSidebar, | ||||
|     ConversationHeader, | ||||
|     DashboardAppFrame, | ||||
|     EmptyState, | ||||
|     MessagesView, | ||||
|   }, | ||||
|  | ||||
|   props: { | ||||
|     inboxId: { | ||||
|       type: [Number, String], | ||||
| @@ -34,7 +31,6 @@ export default { | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   emits: ['contactPanelToggle'], | ||||
|   data() { | ||||
|     return { activeIndex: 0 }; | ||||
|   }, | ||||
| @@ -86,9 +82,6 @@ export default { | ||||
|       } | ||||
|       this.$store.dispatch('conversationLabels/get', this.currentChat.id); | ||||
|     }, | ||||
|     onToggleContactPanel() { | ||||
|       this.$emit('contactPanelToggle'); | ||||
|     }, | ||||
|     onDashboardAppTabChange(index) { | ||||
|       this.activeIndex = index; | ||||
|     }, | ||||
| @@ -98,7 +91,7 @@ export default { | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="conversation-details-wrap bg-n-background" | ||||
|     class="conversation-details-wrap bg-n-background relative" | ||||
|     :class="{ | ||||
|       'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout, | ||||
|     }" | ||||
| @@ -107,14 +100,12 @@ export default { | ||||
|       v-if="currentChat.id" | ||||
|       :chat="currentChat" | ||||
|       :is-inbox-view="isInboxView" | ||||
|       :is-contact-panel-open="isContactPanelOpen" | ||||
|       :show-back-button="isOnExpandedLayout && !isInboxView" | ||||
|       @contact-panel-toggle="onToggleContactPanel" | ||||
|     /> | ||||
|     <woot-tabs | ||||
|       v-if="dashboardApps.length && currentChat.id" | ||||
|       :index="activeIndex" | ||||
|       class="-mt-px bg-white dashboard-app--tabs dark:bg-slate-900" | ||||
|       class="-mt-px dashboard-app--tabs border-t border-t-n-background" | ||||
|       @change="onDashboardAppTabChange" | ||||
|     > | ||||
|       <woot-tabs-item | ||||
| @@ -130,18 +121,12 @@ export default { | ||||
|         v-if="currentChat.id" | ||||
|         :inbox-id="inboxId" | ||||
|         :is-inbox-view="isInboxView" | ||||
|         :is-contact-panel-open="isContactPanelOpen" | ||||
|         @contact-panel-toggle="onToggleContactPanel" | ||||
|       /> | ||||
|       <EmptyState | ||||
|         v-if="!currentChat.id && !isInboxView" | ||||
|         :is-on-expanded-layout="isOnExpandedLayout" | ||||
|       /> | ||||
|       <ConversationSidebar | ||||
|         v-if="showContactPanel" | ||||
|         :current-chat="currentChat" | ||||
|         @toggle-contact-panel="onToggleContactPanel" | ||||
|       /> | ||||
|       <slot /> | ||||
|     </div> | ||||
|     <DashboardAppFrame | ||||
|       v-for="(dashboardApp, index) in dashboardApps" | ||||
|   | ||||
| @@ -243,7 +243,7 @@ export default { | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group" | ||||
|     class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-3 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group" | ||||
|     :class="{ | ||||
|       'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak': | ||||
|         isActiveChat, | ||||
| @@ -278,7 +278,7 @@ export default { | ||||
|         :badge="inboxBadge" | ||||
|         :username="currentContact.name" | ||||
|         :status="currentContact.availability_status" | ||||
|         size="40px" | ||||
|         size="32px" | ||||
|       /> | ||||
|     </div> | ||||
|     <div | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; | ||||
| import BackButton from '../BackButton.vue'; | ||||
| import inboxMixin from 'shared/mixins/inboxMixin'; | ||||
| import InboxName from '../InboxName.vue'; | ||||
| @@ -29,11 +28,7 @@ export default { | ||||
|   props: { | ||||
|     chat: { | ||||
|       type: Object, | ||||
|       default: () => {}, | ||||
|     }, | ||||
|     isContactPanelOpen: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|     showBackButton: { | ||||
|       type: Boolean, | ||||
| @@ -44,15 +39,6 @@ export default { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   emits: ['contactPanelToggle'], | ||||
|   setup(props, { emit }) { | ||||
|     const keyboardEvents = { | ||||
|       'Alt+KeyO': { | ||||
|         action: () => emit('contactPanelToggle'), | ||||
|       }, | ||||
|     }; | ||||
|     useKeyboardEvents(keyboardEvents); | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       currentChat: 'getSelectedChat', | ||||
| @@ -99,13 +85,6 @@ export default { | ||||
|       } | ||||
|       return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY'); | ||||
|     }, | ||||
|     contactPanelToggleText() { | ||||
|       return `${ | ||||
|         this.isContactPanelOpen | ||||
|           ? this.$t('CONVERSATION.HEADER.CLOSE') | ||||
|           : this.$t('CONVERSATION.HEADER.OPEN') | ||||
|       } ${this.$t('CONVERSATION.HEADER.DETAILS')}`; | ||||
|     }, | ||||
|     inbox() { | ||||
|       const { inbox_id: inboxId } = this.chat; | ||||
|       return this.$store.getters['inboxes/getInbox'](inboxId); | ||||
| @@ -133,7 +112,7 @@ export default { | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="flex flex-col items-center justify-between px-4 py-2 border-b bg-n-background border-n-weak md:flex-row" | ||||
|     class="flex flex-col items-center justify-between px-3 py-2 border-b bg-n-background border-n-weak md:flex-row h-12" | ||||
|   > | ||||
|     <div | ||||
|       class="flex flex-col items-center justify-center flex-1 w-full min-w-0" | ||||
| @@ -150,6 +129,7 @@ export default { | ||||
|           :badge="inboxBadge" | ||||
|           :username="currentContact.name" | ||||
|           :status="currentContact.availability_status" | ||||
|           size="32px" | ||||
|         /> | ||||
|         <div | ||||
|           class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit" | ||||
| @@ -157,9 +137,9 @@ export default { | ||||
|           <div | ||||
|             class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit" | ||||
|           > | ||||
|             <NextButton link slate @click.prevent="$emit('contactPanelToggle')"> | ||||
|             <NextButton link slate> | ||||
|               <span | ||||
|                 class="text-base font-medium truncate leading-tight text-n-slate-12" | ||||
|                 class="text-sm font-medium truncate leading-tight text-n-slate-12" | ||||
|               > | ||||
|                 {{ currentContact.name }} | ||||
|               </span> | ||||
| @@ -180,19 +160,11 @@ export default { | ||||
|             <span v-if="isSnoozed" class="font-medium text-n-amber-10"> | ||||
|               {{ snoozedDisplayText }} | ||||
|             </span> | ||||
|             <NextButton | ||||
|               link | ||||
|               xs | ||||
|               blue | ||||
|               :label="contactPanelToggleText" | ||||
|               @click="$emit('contactPanelToggle')" | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div | ||||
|         class="flex flex-row items-center justify-end flex-grow gap-2 mt-3 header-actions-wrap lg:mt-0" | ||||
|         :class="{ 'justify-end': isContactPanelOpen }" | ||||
|       > | ||||
|         <SLACardLabel v-if="hasSlaPolicyId" :chat="chat" show-extended-info /> | ||||
|         <Linear | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| <script setup> | ||||
| import { computed, ref } from 'vue'; | ||||
| import { computed } from 'vue'; | ||||
| import CopilotContainer from '../../copilot/CopilotContainer.vue'; | ||||
| import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue'; | ||||
| import TabBar from 'dashboard/components-next/tabbar/TabBar.vue'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import { useMapGetter } from 'dashboard/composables/store'; | ||||
| import { FEATURE_FLAGS } from '../../../featureFlags'; | ||||
| import { useUISettings } from 'dashboard/composables/useUISettings'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   currentChat: { | ||||
| @@ -14,33 +13,8 @@ const props = defineProps({ | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits(['toggleContactPanel']); | ||||
|  | ||||
| const { t } = useI18n(); | ||||
|  | ||||
| const channelType = computed(() => props.currentChat?.meta?.channel || ''); | ||||
|  | ||||
| const CONTACT_TABS_OPTIONS = [ | ||||
|   { key: 'CONTACT', value: 'contact' }, | ||||
|   { key: 'COPILOT', value: 'copilot' }, | ||||
| ]; | ||||
|  | ||||
| const tabs = computed(() => { | ||||
|   return CONTACT_TABS_OPTIONS.map(tab => ({ | ||||
|     label: t(`CONVERSATION.SIDEBAR.${tab.key}`), | ||||
|     value: tab.value, | ||||
|   })); | ||||
| }); | ||||
| const activeTab = ref(0); | ||||
| const toggleContactPanel = () => { | ||||
|   emit('toggleContactPanel'); | ||||
| }; | ||||
|  | ||||
| const handleTabChange = selectedTab => { | ||||
|   activeTab.value = tabs.value.findIndex( | ||||
|     tabItem => tabItem.value === selectedTab.value | ||||
|   ); | ||||
| }; | ||||
| const currentAccountId = useMapGetter('getCurrentAccountId'); | ||||
| const isFeatureEnabledonAccount = useMapGetter( | ||||
|   'accounts/isFeatureEnabledonAccount' | ||||
| @@ -49,29 +23,37 @@ const isFeatureEnabledonAccount = useMapGetter( | ||||
| const showCopilotTab = computed(() => | ||||
|   isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN) | ||||
| ); | ||||
|  | ||||
| const { uiSettings } = useUISettings(); | ||||
|  | ||||
| const activeTab = computed(() => { | ||||
|   const { | ||||
|     is_contact_sidebar_open: isContactSidebarOpen, | ||||
|     is_copilot_panel_open: isCopilotPanelOpen, | ||||
|   } = uiSettings.value; | ||||
|  | ||||
|   if (isContactSidebarOpen) { | ||||
|     return 0; | ||||
|   } | ||||
|   if (isCopilotPanelOpen) { | ||||
|     return 1; | ||||
|   } | ||||
|   return null; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-80 min-w-80 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background" | ||||
|   > | ||||
|     <div v-if="showCopilotTab" class="p-2"> | ||||
|       <TabBar | ||||
|         :tabs="tabs" | ||||
|         :initial-active-tab="activeTab" | ||||
|         class="w-full [&>button]:w-full" | ||||
|         @tab-changed="handleTabChange" | ||||
|       /> | ||||
|     </div> | ||||
|     <div class="flex flex-1 overflow-auto"> | ||||
|       <ContactPanel | ||||
|         v-if="!activeTab" | ||||
|         v-show="activeTab === 0" | ||||
|         :conversation-id="currentChat.id" | ||||
|         :inbox-id="currentChat.inbox_id" | ||||
|         :on-toggle="toggleContactPanel" | ||||
|       /> | ||||
|       <CopilotContainer | ||||
|         v-else-if="activeTab === 1 && showCopilotTab" | ||||
|         v-show="activeTab === 1 && showCopilotTab" | ||||
|         :key="currentChat.id" | ||||
|         :conversation-inbox-type="channelType" | ||||
|         :conversation-id="currentChat.id" | ||||
|   | ||||
| @@ -38,8 +38,6 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; | ||||
| import { FEATURE_FLAGS } from '../../../featureFlags'; | ||||
| import { INBOX_TYPES } from 'dashboard/helper/inbox'; | ||||
|  | ||||
| import NextButton from 'dashboard/components-next/button/Button.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Message, | ||||
| @@ -47,20 +45,8 @@ export default { | ||||
|     ReplyBox, | ||||
|     Banner, | ||||
|     ConversationLabelSuggestion, | ||||
|     NextButton, | ||||
|   }, | ||||
|   mixins: [inboxMixin], | ||||
|   props: { | ||||
|     isContactPanelOpen: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     isInboxView: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   emits: ['contactPanelToggle'], | ||||
|   setup() { | ||||
|     const isPopOutReplyBox = ref(false); | ||||
|     const conversationPanelRef = ref(null); | ||||
| @@ -203,12 +189,6 @@ export default { | ||||
|     isATweet() { | ||||
|       return this.conversationType === 'tweet'; | ||||
|     }, | ||||
|     isRightOrLeftIcon() { | ||||
|       if (this.isContactPanelOpen) { | ||||
|         return 'arrow-chevron-right'; | ||||
|       } | ||||
|       return 'arrow-chevron-left'; | ||||
|     }, | ||||
|     getLastSeenAt() { | ||||
|       const { contact_last_seen_at: contactLastSeenAt } = this.currentChat; | ||||
|       return contactLastSeenAt; | ||||
| @@ -444,9 +424,6 @@ export default { | ||||
|         relevantMessages | ||||
|       ); | ||||
|     }, | ||||
|     onToggleContactPanel() { | ||||
|       this.$emit('contactPanelToggle'); | ||||
|     }, | ||||
|     setScrollParams() { | ||||
|       this.heightBeforeLoad = this.conversationPanel.scrollHeight; | ||||
|       this.scrollTopBeforeLoad = this.conversationPanel.scrollTop; | ||||
| @@ -530,19 +507,6 @@ export default { | ||||
|       class="mx-2 mt-2 overflow-hidden rounded-lg" | ||||
|       :banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')" | ||||
|     /> | ||||
|     <div class="flex justify-end"> | ||||
|       <NextButton | ||||
|         faded | ||||
|         xs | ||||
|         slate | ||||
|         class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10" | ||||
|         :icon=" | ||||
|           isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill' | ||||
|         " | ||||
|         :class="isInboxView ? 'top-52 md:top-40' : 'top-32'" | ||||
|         @click="onToggleContactPanel" | ||||
|       /> | ||||
|     </div> | ||||
|     <NextMessageList | ||||
|       v-if="showNextBubbles" | ||||
|       ref="conversationPanelRef" | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|     "PHONE_INPUT": { | ||||
|       "PLACEHOLDER": "Search", | ||||
|       "EMPTY_STATE": "No results found" | ||||
|     } | ||||
|     }, | ||||
|     "CLOSE": "Close" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -11,14 +11,14 @@ import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue'; | ||||
| import ContactConversations from './ContactConversations.vue'; | ||||
| import ConversationAction from './ConversationAction.vue'; | ||||
| import ConversationParticipant from './ConversationParticipant.vue'; | ||||
|  | ||||
| import ContactInfo from './contact/ContactInfo.vue'; | ||||
| import ContactNotes from './contact/ContactNotes.vue'; | ||||
| import ConversationInfo from './ConversationInfo.vue'; | ||||
| import CustomAttributes from './customAttributes/CustomAttributes.vue'; | ||||
| import Draggable from 'vuedraggable'; | ||||
| import MacrosList from './Macros/List.vue'; | ||||
| import ShopifyOrdersList from '../../../components/widgets/conversation/ShopifyOrdersList.vue'; | ||||
| import ShopifyOrdersList from 'dashboard/components/widgets/conversation/ShopifyOrdersList.vue'; | ||||
| import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   conversationId: { | ||||
| @@ -29,10 +29,6 @@ const props = defineProps({ | ||||
|     type: Number, | ||||
|     default: undefined, | ||||
|   }, | ||||
|   onToggle: { | ||||
|     type: Function, | ||||
|     default: () => {}, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const { | ||||
| @@ -89,8 +85,6 @@ watch(conversationId, (newConversationId, prevConversationId) => { | ||||
|  | ||||
| watch(contactId, getContactDetails); | ||||
|  | ||||
| const onPanelToggle = props.onToggle; | ||||
|  | ||||
| const onDragEnd = () => { | ||||
|   dragging.value = false; | ||||
|   updateUISettings({ | ||||
| @@ -98,6 +92,13 @@ const onDragEnd = () => { | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| const closeContactPanel = () => { | ||||
|   updateUISettings({ | ||||
|     is_contact_sidebar_open: false, | ||||
|     is_copilot_panel_open: false, | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| onMounted(() => { | ||||
|   conversationSidebarItems.value = conversationSidebarItemsOrder.value; | ||||
|   getContactDetails(); | ||||
| @@ -107,11 +108,11 @@ onMounted(() => { | ||||
|  | ||||
| <template> | ||||
|   <div class="w-full"> | ||||
|     <ContactInfo | ||||
|       :contact="contact" | ||||
|       :channel-type="channelType" | ||||
|       @toggle-panel="onPanelToggle" | ||||
|     <SidebarActionsHeader | ||||
|       :title="$t('CONVERSATION.SIDEBAR.CONTACT')" | ||||
|       @close="closeContactPanel" | ||||
|     /> | ||||
|     <ContactInfo :contact="contact" :channel-type="channelType" /> | ||||
|     <div class="list-group pb-8"> | ||||
|       <Draggable | ||||
|         :list="conversationSidebarItems" | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue'; | ||||
| import { emitter } from 'shared/helpers/mitt'; | ||||
| import { FEATURE_FLAGS } from 'dashboard/featureFlags'; | ||||
| import SidepanelSwitch from 'dashboard/components-next/Conversation/SidepanelSwitch.vue'; | ||||
| import ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
| @@ -17,6 +19,8 @@ export default { | ||||
|     ConversationBox, | ||||
|     PopOverSearch, | ||||
|     CmdBarConversationSnooze, | ||||
|     SidepanelSwitch, | ||||
|     ConversationSidebar, | ||||
|   }, | ||||
|   beforeRouteLeave(to, from, next) { | ||||
|     // Clear selected state if navigating away from a conversation to a route without a conversationId to prevent stale data issues | ||||
| @@ -87,13 +91,17 @@ export default { | ||||
|         this.uiSettings; | ||||
|       return conversationDisplayType !== CONDENSED; | ||||
|     }, | ||||
|     isContactPanelOpen() { | ||||
|       if (this.currentChat.id) { | ||||
|         const { is_contact_sidebar_open: isContactSidebarOpen } = | ||||
|           this.uiSettings; | ||||
|         return isContactSidebarOpen; | ||||
|  | ||||
|     shouldShowSidebar() { | ||||
|       if (!this.currentChat.id) { | ||||
|         return false; | ||||
|       } | ||||
|       return false; | ||||
|  | ||||
|       const { | ||||
|         is_contact_sidebar_open: isContactSidebarOpen, | ||||
|         is_copilot_panel_open: isCopilotPanelOpen, | ||||
|       } = this.uiSettings; | ||||
|       return isContactSidebarOpen || isCopilotPanelOpen; | ||||
|     }, | ||||
|     showPopOverSearch() { | ||||
|       return !this.isFeatureEnabledonAccount( | ||||
| @@ -189,11 +197,6 @@ export default { | ||||
|         this.$store.dispatch('clearSelectedState'); | ||||
|       } | ||||
|     }, | ||||
|     onToggleContactPanel() { | ||||
|       this.updateUISettings({ | ||||
|         is_contact_sidebar_open: !this.isContactPanelOpen, | ||||
|       }); | ||||
|     }, | ||||
|     onSearch() { | ||||
|       this.showSearchModal = true; | ||||
|     }, | ||||
| @@ -225,10 +228,11 @@ export default { | ||||
|     <ConversationBox | ||||
|       v-if="showMessageView" | ||||
|       :inbox-id="inboxId" | ||||
|       :is-contact-panel-open="isContactPanelOpen" | ||||
|       :is-on-expanded-layout="isOnExpandedLayout" | ||||
|       @contact-panel-toggle="onToggleContactPanel" | ||||
|     /> | ||||
|     > | ||||
|       <SidepanelSwitch v-if="currentChat.id" /> | ||||
|     </ConversationBox> | ||||
|     <ConversationSidebar v-if="shouldShowSidebar" :current-chat="currentChat" /> | ||||
|     <CmdBarConversationSnooze /> | ||||
|   </section> | ||||
| </template> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pranav
					Pranav