Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a053a8d29 | ||
|
|
3c64f04f27 | ||
|
|
a5126309c2 | ||
|
|
735a0bcc2c | ||
|
|
79a6599dc1 | ||
|
|
8d44829589 | ||
|
|
1d45c4f0e8 | ||
|
|
7d954a1aa4 | ||
|
|
f9a1c98e38 | ||
|
|
80f3f31aee | ||
|
|
d4eff76f71 | ||
|
|
666d296160 | ||
|
|
58c33743a1 | ||
|
|
b5f84a1625 | ||
|
|
c12abcbd42 | ||
|
|
6ac955eec0 | ||
|
|
501c5f0628 | ||
|
|
123765b676 | ||
|
|
bd571f5a7c | ||
|
|
9e8ea5d2f8 | ||
|
|
543dd1476d | ||
|
|
5f412b0baf | ||
|
|
98cb3e0bf8 | ||
|
|
16b09e8b3f | ||
|
|
653c47bd9d | ||
|
|
2553888a8b | ||
|
|
115ca2feb0 | ||
|
|
4cd9bcd938 | ||
|
|
244780eaa8 | ||
|
|
6609f7637a | ||
|
|
34e929c705 | ||
|
|
bcce9cf74b | ||
|
|
512334ee6b | ||
|
|
4b6b1fed71 | ||
|
|
1a07eb50c5 | ||
|
|
50b6c07114 | ||
|
|
939cf61b7c | ||
|
|
f12214f782 | ||
|
|
66fcaa5e01 | ||
|
|
a06964c0f1 | ||
|
|
333c1ca383 | ||
|
|
fb5cf32734 | ||
|
|
d024120afd | ||
|
|
ec6cb63f02 | ||
|
|
58acebc6e3 | ||
|
|
7a60a861f1 | ||
|
|
580d08a033 | ||
|
|
5e2678bc6d | ||
|
|
dba72e5a95 | ||
|
|
b522491310 | ||
|
|
d1ed7419ac |
@@ -134,10 +134,6 @@ class ConversationApi extends ApiClient {
|
||||
return axios.get(`${this.url}/${conversationId}/attachments`);
|
||||
}
|
||||
|
||||
requestCopilot(conversationId, body) {
|
||||
return axios.post(`${this.url}/${conversationId}/copilot`, body);
|
||||
}
|
||||
|
||||
getInboxAssistant(conversationId) {
|
||||
return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { nextTick, ref, watch, computed } from 'vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -8,8 +7,10 @@ import CopilotInput from './CopilotInput.vue';
|
||||
import CopilotLoader from './CopilotLoader.vue';
|
||||
import CopilotAgentMessage from './CopilotAgentMessage.vue';
|
||||
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
|
||||
import CopilotThinkingGroup from './CopilotThinkingGroup.vue';
|
||||
import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue';
|
||||
import Icon from '../icon/Icon.vue';
|
||||
import CopilotHeader from './CopilotHeader.vue';
|
||||
import CopilotEmptyState from './CopilotEmptyState.vue';
|
||||
|
||||
const props = defineProps({
|
||||
supportAgent: {
|
||||
@@ -26,7 +27,7 @@ const props = defineProps({
|
||||
},
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
assistants: {
|
||||
type: Array,
|
||||
@@ -38,22 +39,13 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'reset', 'setAssistant']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const COPILOT_USER_ROLES = ['assistant', 'system'];
|
||||
const emit = defineEmits(['sendMessage', 'reset', 'setAssistant', 'close']);
|
||||
|
||||
const sendMessage = message => {
|
||||
emit('sendMessage', message);
|
||||
useTrack(COPILOT_EVENTS.SEND_MESSAGE);
|
||||
};
|
||||
|
||||
const useSuggestion = opt => {
|
||||
emit('sendMessage', t(opt.prompt));
|
||||
useTrack(COPILOT_EVENTS.SEND_SUGGESTED);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset');
|
||||
};
|
||||
@@ -67,20 +59,28 @@ const scrollToBottom = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const promptOptions = [
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
|
||||
},
|
||||
];
|
||||
const groupedMessages = computed(() => {
|
||||
const result = [];
|
||||
let thinkingGroup = [];
|
||||
|
||||
props.messages.forEach(message => {
|
||||
if (message.role === 'assistant_thinking') {
|
||||
thinkingGroup.push(message);
|
||||
} else {
|
||||
if (thinkingGroup.length > 0) {
|
||||
result.push({ type: 'thinking_group', messages: thinkingGroup });
|
||||
thinkingGroup = [];
|
||||
}
|
||||
result.push({ type: 'message', message });
|
||||
}
|
||||
});
|
||||
|
||||
if (thinkingGroup.length > 0) {
|
||||
result.push({ type: 'thinking_group', messages: thinkingGroup });
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.messages, () => props.isCaptainTyping],
|
||||
@@ -93,60 +93,62 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full">
|
||||
<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
|
||||
v-if="message.role === 'user'"
|
||||
:support-agent="supportAgent"
|
||||
:message="message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="COPILOT_USER_ROLES.includes(message.role)"
|
||||
:message="message"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CopilotLoader v-if="isCaptainTyping" />
|
||||
</div>
|
||||
|
||||
<CopilotHeader
|
||||
:has-messages="messages.length > 0"
|
||||
@reset="handleReset"
|
||||
@close="$emit('close')"
|
||||
/>
|
||||
<div
|
||||
v-if="!messages.length"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
ref="chatContainer"
|
||||
class="flex-1 flex px-4 py-4 overflow-y-auto items-start"
|
||||
>
|
||||
<div class="h-fit px-3 py-3 space-y-1">
|
||||
<span class="text-xs text-n-slate-10">
|
||||
{{ $t('COPILOT.TRY_THESE_PROMPTS') }}
|
||||
</span>
|
||||
<button
|
||||
v-for="prompt in promptOptions"
|
||||
:key="prompt.label"
|
||||
class="px-2 py-1 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center gap-1"
|
||||
@click="() => useSuggestion(prompt)"
|
||||
<div v-if="messages.length" class="space-y-6 flex-1 flex flex-col w-full">
|
||||
<template
|
||||
v-for="item in groupedMessages"
|
||||
:key="item.type === 'message' ? item.message.id : 'thinking-group'"
|
||||
>
|
||||
<span>{{ t(prompt.label) }}</span>
|
||||
<Icon icon="i-lucide-chevron-right" />
|
||||
</button>
|
||||
<template v-if="item.type === 'message'">
|
||||
<CopilotAgentMessage
|
||||
v-if="item.message.role === 'user'"
|
||||
:support-agent="supportAgent"
|
||||
:message="item.message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="
|
||||
item.message.role === 'assistant' ||
|
||||
item.message.role === 'system'
|
||||
"
|
||||
:message="item.message"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
/>
|
||||
</template>
|
||||
<CopilotThinkingGroup
|
||||
v-else
|
||||
:messages="item.messages"
|
||||
:has-assistant-message-after="
|
||||
groupedMessages[groupedMessages.indexOf(item) + 1]?.message
|
||||
?.role === 'assistant'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CopilotLoader v-if="isCaptainTyping" />
|
||||
</div>
|
||||
<CopilotEmptyState
|
||||
v-if="!messages.length"
|
||||
@use-suggestion="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-px mb-2">
|
||||
<div class="flex items-center gap-2 justify-between w-full mb-1">
|
||||
<ToggleCopilotAssistant
|
||||
v-if="assistants.length"
|
||||
v-if="assistants.length > 1"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@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>
|
||||
|
||||
@@ -17,7 +17,7 @@ const props = defineProps({
|
||||
},
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Icon from '../icon/Icon.vue';
|
||||
|
||||
const emit = defineEmits(['useSuggestion']);
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const routePromptMap = {
|
||||
conversations: [
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
|
||||
},
|
||||
],
|
||||
dashboard: [
|
||||
{
|
||||
label: 'High priority conversations',
|
||||
prompt: 'Get me a summary of all high priority open conversations',
|
||||
},
|
||||
{
|
||||
label: 'List contacts',
|
||||
prompt: 'Show me the list of contacts',
|
||||
},
|
||||
{
|
||||
label: 'Summarize articles',
|
||||
prompt: 'List articles that are in draft',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const getCurrentRoute = () => {
|
||||
const path = route.path;
|
||||
if (path.includes('/conversations')) return 'conversations';
|
||||
if (path.includes('/dashboard')) return 'dashboard';
|
||||
if (path.includes('/contacts')) return 'contacts';
|
||||
if (path.includes('/articles')) return 'articles';
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
const promptOptions = computed(() => {
|
||||
const currentRoute = getCurrentRoute();
|
||||
return routePromptMap[currentRoute] || routePromptMap.conversations;
|
||||
});
|
||||
|
||||
const handleSuggestion = opt => {
|
||||
emit('useSuggestion', t(opt.prompt));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col gap-6 px-2">
|
||||
<div class="flex flex-col space-y-4 py-4">
|
||||
<Icon icon="i-woot-captain" class="text-n-slate-9 text-4xl" />
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-medium text-n-slate-12 leading-8">
|
||||
{{ $t('CAPTAIN.COPILOT.PANEL_TITLE') }}
|
||||
</h3>
|
||||
<p class="text-sm text-n-slate-11 leading-6">
|
||||
{{ $t('CAPTAIN.COPILOT.KICK_OFF_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full space-y-2">
|
||||
<span class="text-xs text-n-slate-10 block">
|
||||
{{ $t('CAPTAIN.COPILOT.TRY_THESE_PROMPTS') }}
|
||||
</span>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="prompt in promptOptions"
|
||||
:key="prompt.label"
|
||||
class="w-full px-3 py-2 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center justify-between hover:bg-n-slate-3 transition-colors"
|
||||
@click="handleSuggestion(prompt)"
|
||||
>
|
||||
<span>{{ t(prompt.label) }}</span>
|
||||
<Icon icon="i-lucide-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@ defineEmits(['reset', 'close']);
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between px-5 py-2 border-b border-n-weak h-12"
|
||||
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">
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const getUIState = useMapGetter('uiState/getUIState');
|
||||
const isSidebarOpen = computed(() => getUIState.value('isCopilotSidebarOpen'));
|
||||
|
||||
const isConversationRoute = computed(() => {
|
||||
const CONVERSATION_ROUTES = [
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'conversations_through_label',
|
||||
'team_conversations_through_label',
|
||||
'conversations_through_folders',
|
||||
'conversation_through_mentions',
|
||||
'conversation_through_unattended',
|
||||
'conversation_through_participating',
|
||||
];
|
||||
return CONVERSATION_ROUTES.includes(route.name);
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
store.dispatch('uiState/toggle', 'isCopilotSidebarOpen');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed bottom-4 right-4 z-50">
|
||||
<div
|
||||
v-if="!isSidebarOpen && !isConversationRoute"
|
||||
class="rounded-full bg-n-solid-2 p-2 hover:bg-n-solid-3"
|
||||
>
|
||||
<Button
|
||||
icon="i-woot-captain"
|
||||
class="!rounded-full !bg-n-alpha-2 text-n-slate-12 text-xl"
|
||||
lg
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</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"
|
||||
|
||||
@@ -824,7 +824,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-[21rem] 2xl:w-[26rem]',
|
||||
]"
|
||||
>
|
||||
<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 pt-3 pb-3"
|
||||
: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 }}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
||||
import ConversationAPI from 'dashboard/api/inbox/conversation';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
|
||||
const getUIState = useMapGetter('uiState/getUIState');
|
||||
const isSidebarOpen = computed(() => getUIState.value('isCopilotSidebarOpen'));
|
||||
|
||||
const messages = ref([]);
|
||||
const isCaptainTyping = ref(false);
|
||||
const selectedAssistantId = ref(null);
|
||||
const copilotConnector = ref(null);
|
||||
|
||||
const activeAssistant = computed(() => {
|
||||
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
||||
@@ -59,7 +52,12 @@ const handleReset = () => {
|
||||
messages.value = [];
|
||||
};
|
||||
|
||||
const sendMessage = async message => {
|
||||
const sendMessage = message => {
|
||||
// Ensure WebSocket is connected before sending
|
||||
if (!copilotConnector.value || !isSidebarOpen.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add user message
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
@@ -68,55 +66,71 @@ const sendMessage = async message => {
|
||||
});
|
||||
isCaptainTyping.value = true;
|
||||
|
||||
try {
|
||||
const { data } = await ConversationAPI.requestCopilot(
|
||||
props.conversationId,
|
||||
{
|
||||
previous_history: messages.value
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
.slice(0, -1),
|
||||
message,
|
||||
assistant_id: selectedAssistantId.value,
|
||||
}
|
||||
);
|
||||
messages.value.push({
|
||||
id: new Date().getTime(),
|
||||
role: 'assistant',
|
||||
content: data.message,
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(error);
|
||||
} finally {
|
||||
isCaptainTyping.value = false;
|
||||
}
|
||||
copilotConnector.value.sendMessage({
|
||||
assistantId: activeAssistant.value.id,
|
||||
conversationId: currentChat.value?.id,
|
||||
previousHistory: messages.value
|
||||
.filter(m => m.role !== 'assistant_thinking')
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
.slice(0, -1),
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.conversationId) {
|
||||
store.dispatch('getInboxCaptainAssistantById', props.conversationId);
|
||||
selectedAssistantId.value = activeAssistant.value?.id;
|
||||
}
|
||||
});
|
||||
// const onCopilotResponse = data => {
|
||||
// if (data.type === 'final_response') {
|
||||
// messages.value.push({
|
||||
// id: new Date().getTime(),
|
||||
// role: 'assistant',
|
||||
// content: data.response.response,
|
||||
// });
|
||||
// isCaptainTyping.value = false;
|
||||
// } else if (data.type === 'ui_event') {
|
||||
// if (data.event === 'ui:linear_ticket_create') {
|
||||
// emitter.emit('ui:linear_ticket_create');
|
||||
// setTimeout(() => {
|
||||
// emitter.emit('ui:linear_ticket_create_data', data.response);
|
||||
// }, 100);
|
||||
// }
|
||||
// } else {
|
||||
// messages.value.push({
|
||||
// id: new Date().getTime(),
|
||||
// role: 'assistant_thinking',
|
||||
// content: data.response.response,
|
||||
// reasoning: data.response.reasoning,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleClose = () => {
|
||||
store.dispatch('uiState/set', { isCopilotSidebarOpen: false });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Copilot
|
||||
:messages="messages"
|
||||
:support-agent="currentUser"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@set-assistant="setAssistant"
|
||||
@send-message="sendMessage"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
<div
|
||||
v-if="isSidebarOpen"
|
||||
class="border-l border-n-weak w-[20rem] min-w-[20rem]"
|
||||
>
|
||||
<Copilot
|
||||
:messages="messages"
|
||||
:support-agent="currentUser"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@set-assistant="setAssistant"
|
||||
@send-message="sendMessage"
|
||||
@reset="handleReset"
|
||||
@close="handleClose"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="hidden" />
|
||||
</template>
|
||||
|
||||
@@ -48,7 +48,7 @@ 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
|
||||
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
|
||||
<div v-if="hasOpenedAtleastOnce" class="flex-1">
|
||||
<div
|
||||
v-for="(configItem, index) in config"
|
||||
:key="index"
|
||||
@@ -115,6 +115,7 @@ export default {
|
||||
.dashboard-app--list iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dashboard-app_loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,11 +4,9 @@ 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,
|
||||
@@ -25,16 +23,11 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['contactPanelToggle'],
|
||||
data() {
|
||||
return { activeIndex: 0 };
|
||||
},
|
||||
@@ -57,9 +50,6 @@ export default {
|
||||
})),
|
||||
];
|
||||
},
|
||||
showContactPanel() {
|
||||
return this.isContactPanelOpen && this.currentChat.id;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'currentChat.inbox_id': {
|
||||
@@ -86,9 +76,6 @@ export default {
|
||||
}
|
||||
this.$store.dispatch('conversationLabels/get', this.currentChat.id);
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.$emit('contactPanelToggle');
|
||||
},
|
||||
onDashboardAppTabChange(index) {
|
||||
this.activeIndex = index;
|
||||
},
|
||||
@@ -98,7 +85,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 +94,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 border-t border-t-n-background bg-n-background dashboard-app--tabs"
|
||||
@change="onDashboardAppTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
@@ -125,23 +110,17 @@ export default {
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div v-show="!activeIndex" class="flex h-full min-h-0 m-0">
|
||||
<div v-show="!activeIndex" class="flex-1 h-full min-h-0 m-0">
|
||||
<MessagesView
|
||||
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';
|
||||
@@ -13,8 +12,6 @@ import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
@@ -23,7 +20,6 @@ export default {
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -31,10 +27,6 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -44,15 +36,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 +82,7 @@ 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 +110,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 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,20 +127,18 @@ 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"
|
||||
>
|
||||
<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')">
|
||||
<span
|
||||
class="text-base font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
</NextButton>
|
||||
<div class="flex items-center max-w-full gap-1 p-0 m-0 w-fit">
|
||||
<span
|
||||
class="text-sm font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
@@ -180,19 +155,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,8 @@
|
||||
<script setup>
|
||||
import { computed, ref } 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 Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
const props = defineProps({
|
||||
currentChat: {
|
||||
@@ -14,69 +11,48 @@ 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'
|
||||
const getUIState = useMapGetter('uiState/getUIState');
|
||||
const isCopilotSidebarOpen = computed(() =>
|
||||
getUIState.value('isCopilotSidebarOpen')
|
||||
);
|
||||
const isConversationSidebarOpen = computed(() =>
|
||||
getUIState.value('isConversationSidebarOpen')
|
||||
);
|
||||
|
||||
const showCopilotTab = computed(() =>
|
||||
isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN)
|
||||
const showConversationSidebar = computed(
|
||||
() =>
|
||||
props.currentChat.id &&
|
||||
!isCopilotSidebarOpen.value &&
|
||||
isConversationSidebarOpen.value
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
const closeConversationPanel = () => {
|
||||
store.dispatch('uiState/set', { isConversationSidebarOpen: false });
|
||||
};
|
||||
</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"
|
||||
v-if="showConversationSidebar"
|
||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-[20rem] min-w-[20rem] flex flex-col"
|
||||
>
|
||||
<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">
|
||||
<div class="flex flex-1 flex-col overflow-auto">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b border-n-weak h-12"
|
||||
>
|
||||
<span class="font-medium text-sm text-n-slate-12">
|
||||
{{ $t('CONVERSATION.SIDEBAR.ACTIONS') }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button icon="i-lucide-x" ghost sm @click="closeConversationPanel" />
|
||||
</div>
|
||||
</div>
|
||||
<ContactPanel
|
||||
v-if="!activeTab"
|
||||
:conversation-id="currentChat.id"
|
||||
:inbox-id="currentChat.inbox_id"
|
||||
:on-toggle="toggleContactPanel"
|
||||
/>
|
||||
<CopilotContainer
|
||||
v-else-if="activeTab === 1 && showCopilotTab"
|
||||
:key="currentChat.id"
|
||||
:conversation-inbox-type="channelType"
|
||||
:conversation-id="currentChat.id"
|
||||
class="flex-1"
|
||||
:conversation-id="props.currentChat.id"
|
||||
:inbox-id="props.currentChat.inbox_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="hidden" />
|
||||
</template>
|
||||
|
||||
@@ -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 { isEnterprise } = useConfig();
|
||||
@@ -199,12 +185,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;
|
||||
@@ -440,9 +420,6 @@ export default {
|
||||
relevantMessages
|
||||
);
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.$emit('contactPanelToggle');
|
||||
},
|
||||
setScrollParams() {
|
||||
this.heightBeforeLoad = this.conversationPanel.scrollHeight;
|
||||
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
|
||||
@@ -526,19 +503,7 @@ 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"
|
||||
class="conversation-panel"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import SearchableDropdown from './SearchableDropdown.vue';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -204,6 +205,12 @@ const createIssue = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEmitter('ui:linear_ticket_create_data', data => {
|
||||
formState.title = data.title;
|
||||
formState.description = data.description;
|
||||
formState.priority = data.priority;
|
||||
});
|
||||
|
||||
onMounted(getTeams);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTrack } from 'dashboard/composables';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -94,6 +95,10 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
useEmitter('ui:linear_ticket_create', () => {
|
||||
shouldShowPopup.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadLinkedIssue();
|
||||
});
|
||||
|
||||
@@ -234,8 +234,7 @@
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
"ACTIONS": "Actions"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
@@ -380,9 +379,6 @@
|
||||
"TWO": "{user} and {secondUser} are typing",
|
||||
"MULTIPLE": "{user} and {count} others are typing"
|
||||
},
|
||||
"COPILOT": {
|
||||
"TRY_THESE_PROMPTS": "Try these prompts"
|
||||
},
|
||||
"GALLERY_VIEW": {
|
||||
"ERROR_DOWNLOADING": "Unable to download attachment. Please try again"
|
||||
}
|
||||
|
||||
@@ -328,14 +328,17 @@
|
||||
"HEADER_KNOW_MORE": "Know more",
|
||||
"COPILOT": {
|
||||
"TITLE": "Copilot",
|
||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
||||
"PANEL_TITLE": "Get started with Copilot",
|
||||
"KICK_OFF_MESSAGE": "Need a quick summary, want to check past conversations, or draft a better reply? Copilot’s here to speed things up.",
|
||||
"SEND_MESSAGE": "Send message...",
|
||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||
"LOADER": "Captain is thinking",
|
||||
"YOU": "You",
|
||||
"USE": "Use this",
|
||||
"RESET": "Reset",
|
||||
"SHOW_STEPS": "Show steps",
|
||||
"SELECT_ASSISTANT": "Select Assistant",
|
||||
"SHOW_STEPS": "Show steps",
|
||||
"PROMPTS": {
|
||||
"SUMMARIZE": {
|
||||
"LABEL": "Summarize this conversation",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import NextSidebar from 'next/sidebar/Sidebar.vue';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
@@ -9,6 +18,8 @@ import AccountSelector from 'dashboard/components/layout/sidebarComponents/Accou
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import UpgradePage from 'dashboard/routes/dashboard/upgrade/UpgradePage.vue';
|
||||
import CopilotContainer from 'dashboard/components/copilot/CopilotContainer.vue';
|
||||
import CopilotLauncher from 'dashboard/components-next/copilot/CopilotLauncher.vue';
|
||||
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
@@ -16,178 +27,160 @@ import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const CommandBar = defineAsyncComponent(
|
||||
() => import('./commands/commandbar.vue')
|
||||
);
|
||||
|
||||
const Sidebar = defineAsyncComponent(
|
||||
() => import('../../components/layout/Sidebar.vue')
|
||||
);
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextSidebar,
|
||||
Sidebar,
|
||||
CommandBar,
|
||||
WootKeyShortcutModal,
|
||||
AddAccountModal,
|
||||
AccountSelector,
|
||||
AddLabelModal,
|
||||
NotificationPanel,
|
||||
UpgradePage,
|
||||
},
|
||||
setup() {
|
||||
const upgradePageRef = ref(null);
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { accountId } = useAccount();
|
||||
const route = useRoute();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
accountId,
|
||||
upgradePageRef,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
showShortcutModal: false,
|
||||
isNotificationPanel: false,
|
||||
displayLayoutType: '',
|
||||
hasBanner: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
currentRoute() {
|
||||
return ' ';
|
||||
},
|
||||
showUpgradePage() {
|
||||
return this.upgradePageRef?.shouldShowUpgradePage;
|
||||
},
|
||||
bypassUpgradePage() {
|
||||
return [
|
||||
'billing_settings_index',
|
||||
'settings_inbox_list',
|
||||
'agent_list',
|
||||
].includes(this.$route.name);
|
||||
},
|
||||
isSidebarOpen() {
|
||||
const { show_secondary_sidebar: showSecondarySidebar } = this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
},
|
||||
previouslyUsedDisplayType() {
|
||||
const {
|
||||
previously_used_conversation_display_type: conversationDisplayType,
|
||||
} = this.uiSettings;
|
||||
return conversationDisplayType;
|
||||
},
|
||||
previouslyUsedSidebarView() {
|
||||
const { previously_used_sidebar_view: showSecondarySidebar } =
|
||||
this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
},
|
||||
showNextSidebar() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
displayLayoutType() {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
this.updateUISettings({
|
||||
conversation_display_type:
|
||||
this.displayLayoutType === LAYOUT_TYPES.EXPANDED
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: this.previouslyUsedDisplayType,
|
||||
show_secondary_sidebar:
|
||||
this.displayLayoutType === LAYOUT_TYPES.EXPANDED
|
||||
? false
|
||||
: this.previouslyUsedSidebarView,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleResize();
|
||||
this.$nextTick(this.checkBanner);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
window.addEventListener('resize', this.checkBanner);
|
||||
emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
window.removeEventListener('resize', this.checkBanner);
|
||||
emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
const upgradePageRef = ref(null);
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
methods: {
|
||||
checkBanner() {
|
||||
this.hasBanner =
|
||||
document.getElementsByClassName('woot-banner').length > 0;
|
||||
},
|
||||
handleResize() {
|
||||
const { SMALL_SCREEN_BREAKPOINT, LAYOUT_TYPES } = wootConstants;
|
||||
let throttled = false;
|
||||
const delay = 150;
|
||||
const showAccountModal = ref(false);
|
||||
const showCreateAccountModal = ref(false);
|
||||
const showAddLabelModal = ref(false);
|
||||
const showShortcutModal = ref(false);
|
||||
const isNotificationPanel = ref(false);
|
||||
const displayLayoutType = ref('');
|
||||
const hasBanner = ref('');
|
||||
|
||||
if (throttled) {
|
||||
return;
|
||||
}
|
||||
throttled = true;
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
throttled = false;
|
||||
if (window.innerWidth <= SMALL_SCREEN_BREAKPOINT) {
|
||||
this.displayLayoutType = LAYOUT_TYPES.EXPANDED;
|
||||
} else {
|
||||
this.displayLayoutType = LAYOUT_TYPES.CONDENSED;
|
||||
}
|
||||
}, delay);
|
||||
},
|
||||
toggleSidebar() {
|
||||
this.updateUISettings({
|
||||
show_secondary_sidebar: !this.isSidebarOpen,
|
||||
previously_used_sidebar_view: !this.isSidebarOpen,
|
||||
});
|
||||
},
|
||||
openCreateAccountModal() {
|
||||
this.showAccountModal = false;
|
||||
this.showCreateAccountModal = true;
|
||||
},
|
||||
closeCreateAccountModal() {
|
||||
this.showCreateAccountModal = false;
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.showAccountModal = !this.showAccountModal;
|
||||
},
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
},
|
||||
closeKeyShortcutModal() {
|
||||
this.showShortcutModal = false;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabelModal = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabelModal = false;
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.isNotificationPanel = true;
|
||||
},
|
||||
closeNotificationPanel() {
|
||||
this.isNotificationPanel = false;
|
||||
},
|
||||
},
|
||||
const currentRoute = computed(() => ' ');
|
||||
|
||||
const showUpgradePage = computed(
|
||||
() => upgradePageRef.value?.shouldShowUpgradePage
|
||||
);
|
||||
|
||||
const bypassUpgradePage = computed(() =>
|
||||
['billing_settings_index', 'settings_inbox_list', 'agent_list'].includes(
|
||||
route.name
|
||||
)
|
||||
);
|
||||
|
||||
const isSidebarOpen = computed(() => {
|
||||
const { show_secondary_sidebar: showSecondarySidebar } = uiSettings;
|
||||
return showSecondarySidebar;
|
||||
});
|
||||
|
||||
const previouslyUsedDisplayType = computed(() => {
|
||||
const { previously_used_conversation_display_type: conversationDisplayType } =
|
||||
uiSettings;
|
||||
return conversationDisplayType;
|
||||
});
|
||||
|
||||
const previouslyUsedSidebarView = computed(() => {
|
||||
const { previously_used_sidebar_view: showSecondarySidebar } = uiSettings;
|
||||
return showSecondarySidebar;
|
||||
});
|
||||
|
||||
const showNextSidebar = computed(() =>
|
||||
isFeatureEnabledonAccount.value(
|
||||
currentAccount.value?.id,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
)
|
||||
);
|
||||
|
||||
const checkBanner = () => {
|
||||
hasBanner.value = document.getElementsByClassName('woot-banner').length > 0;
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
const { SMALL_SCREEN_BREAKPOINT, LAYOUT_TYPES } = wootConstants;
|
||||
let throttled = false;
|
||||
const delay = 150;
|
||||
|
||||
if (throttled) return;
|
||||
throttled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
throttled = false;
|
||||
displayLayoutType.value =
|
||||
window.innerWidth <= SMALL_SCREEN_BREAKPOINT
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: LAYOUT_TYPES.CONDENSED;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
updateUISettings({
|
||||
show_secondary_sidebar: !isSidebarOpen.value,
|
||||
previously_used_sidebar_view: !isSidebarOpen.value,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateAccountModal = () => {
|
||||
showAccountModal.value = false;
|
||||
showCreateAccountModal.value = true;
|
||||
};
|
||||
|
||||
const closeCreateAccountModal = () => {
|
||||
showCreateAccountModal.value = false;
|
||||
};
|
||||
|
||||
const toggleAccountModal = () => {
|
||||
showAccountModal.value = !showAccountModal.value;
|
||||
};
|
||||
|
||||
const toggleKeyShortcutModal = () => {
|
||||
showShortcutModal.value = true;
|
||||
};
|
||||
|
||||
const closeKeyShortcutModal = () => {
|
||||
showShortcutModal.value = false;
|
||||
};
|
||||
|
||||
const showAddLabelPopup = () => {
|
||||
showAddLabelModal.value = true;
|
||||
};
|
||||
|
||||
const hideAddLabelPopup = () => {
|
||||
showAddLabelModal.value = false;
|
||||
};
|
||||
|
||||
const openNotificationPanel = () => {
|
||||
isNotificationPanel.value = true;
|
||||
};
|
||||
|
||||
const closeNotificationPanel = () => {
|
||||
isNotificationPanel.value = false;
|
||||
};
|
||||
|
||||
watch(displayLayoutType, () => {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
updateUISettings({
|
||||
conversation_display_type:
|
||||
displayLayoutType.value === LAYOUT_TYPES.EXPANDED
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: previouslyUsedDisplayType.value,
|
||||
show_secondary_sidebar:
|
||||
displayLayoutType.value === LAYOUT_TYPES.EXPANDED
|
||||
? false
|
||||
: previouslyUsedSidebarView.value,
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
nextTick(checkBanner);
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('resize', checkBanner);
|
||||
emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, toggleSidebar);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('resize', checkBanner);
|
||||
emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, toggleSidebar);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -218,6 +211,8 @@ export default {
|
||||
/>
|
||||
<template v-if="!showUpgradePage">
|
||||
<router-view />
|
||||
<CopilotLauncher />
|
||||
<CopilotContainer />
|
||||
<CommandBar />
|
||||
<NotificationPanel
|
||||
v-if="isNotificationPanel"
|
||||
|
||||
@@ -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({
|
||||
@@ -107,11 +101,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<ContactInfo
|
||||
:contact="contact"
|
||||
:channel-type="channelType"
|
||||
@toggle-panel="onPanelToggle"
|
||||
/>
|
||||
<ContactInfo :contact="contact" :channel-type="channelType" />
|
||||
<div class="list-group pb-8">
|
||||
<Draggable
|
||||
:list="conversationSidebarItems"
|
||||
|
||||
@@ -10,13 +10,17 @@ 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 ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue';
|
||||
import PanelButtons from './PanelButtons.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatList,
|
||||
ConversationBox,
|
||||
ConversationSidebar,
|
||||
PopOverSearch,
|
||||
CmdBarConversationSnooze,
|
||||
PanelButtons,
|
||||
},
|
||||
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,14 +91,6 @@ export default {
|
||||
this.uiSettings;
|
||||
return conversationDisplayType !== CONDENSED;
|
||||
},
|
||||
isContactPanelOpen() {
|
||||
if (this.currentChat.id) {
|
||||
const { is_contact_sidebar_open: isContactSidebarOpen } =
|
||||
this.uiSettings;
|
||||
return isContactSidebarOpen;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
showPopOverSearch() {
|
||||
return !this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
@@ -189,11 +185,7 @@ export default {
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
}
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.updateUISettings({
|
||||
is_contact_sidebar_open: !this.isContactPanelOpen,
|
||||
});
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.showSearchModal = true;
|
||||
},
|
||||
@@ -225,10 +217,11 @@ export default {
|
||||
<ConversationBox
|
||||
v-if="showMessageView"
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
>
|
||||
<PanelButtons v-if="currentChat.id" />
|
||||
</ConversationBox>
|
||||
<ConversationSidebar :current-chat="currentChat" />
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleConversationSidebarToggle = () => {
|
||||
store.dispatch('uiState/set', {
|
||||
isConversationSidebarOpen: true,
|
||||
isCopilotSidebarOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopilotSidebarToggle = () => {
|
||||
store.dispatch('uiState/set', {
|
||||
isCopilotSidebarOpen: true,
|
||||
isConversationSidebarOpen: false,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center absolute top-24 right-1 bg-n-alpha-2 border border-n-weak rounded-lg bg-n-background gap-2 py-0.5 px-0.5"
|
||||
>
|
||||
<Button
|
||||
ghost
|
||||
slate
|
||||
sm
|
||||
class="!text-sm"
|
||||
icon="i-ph-user-bold"
|
||||
@click="handleConversationSidebarToggle"
|
||||
/>
|
||||
<Button
|
||||
ghost
|
||||
class="!text-sm !text-n-iris-11"
|
||||
sm
|
||||
icon="i-woot-captain"
|
||||
@click="handleCopilotSidebarToggle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -51,6 +51,7 @@ import captainDocuments from './captain/document';
|
||||
import captainResponses from './captain/response';
|
||||
import captainInboxes from './captain/inboxes';
|
||||
import captainBulkActions from './captain/bulkActions';
|
||||
import uiState from './modules/uiState';
|
||||
const plugins = [];
|
||||
|
||||
export default createStore({
|
||||
@@ -106,6 +107,7 @@ export default createStore({
|
||||
captainResponses,
|
||||
captainInboxes,
|
||||
captainBulkActions,
|
||||
uiState,
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const initialState = {
|
||||
isCopilotSidebarOpen: false,
|
||||
isConversationSidebarOpen: true,
|
||||
};
|
||||
|
||||
const types = {
|
||||
SET_UI_STATE: 'SET_UI_STATE',
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
[types.SET_UI_STATE](state, { key, value }) {
|
||||
state[key] = value;
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getUIState: state => key => state[key],
|
||||
};
|
||||
|
||||
const actions = {
|
||||
toggle({ commit, state }, key) {
|
||||
commit(types.SET_UI_STATE, { key, value: !state[key] });
|
||||
},
|
||||
set({ commit }, payload) {
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
commit(types.SET_UI_STATE, { key, value });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: initialState,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
||||
@@ -127,7 +127,6 @@ Rails.application.routes.draw do
|
||||
post :unread
|
||||
post :custom_attributes
|
||||
get :attachments
|
||||
post :copilot
|
||||
get :inbox_assistant
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,29 +4,6 @@ module Enterprise::Api::V1::Accounts::ConversationsController
|
||||
before_action :set_assistant, only: [:copilot]
|
||||
end
|
||||
|
||||
def copilot
|
||||
# First try to get the user's preferred assistant from UI settings or from the request
|
||||
assistant_id = copilot_params[:assistant_id] || current_user.ui_settings&.dig('preferred_captain_assistant_id')
|
||||
|
||||
# Find the assistant either by ID or from inbox
|
||||
assistant = if assistant_id.present?
|
||||
Captain::Assistant.find_by(id: assistant_id, account_id: Current.account.id)
|
||||
else
|
||||
@conversation.inbox.captain_assistant
|
||||
end
|
||||
|
||||
return render json: { message: I18n.t('captain.copilot_error') } unless assistant
|
||||
|
||||
response = Captain::Copilot::ChatService.new(
|
||||
assistant,
|
||||
previous_messages: copilot_params[:previous_messages],
|
||||
conversation_history: @conversation.to_llm_text,
|
||||
language: @conversation.account.locale_english_name
|
||||
).generate_response(copilot_params[:message])
|
||||
|
||||
render json: { message: response['response'] }
|
||||
end
|
||||
|
||||
def inbox_assistant
|
||||
assistant = @conversation.inbox.captain_assistant
|
||||
|
||||
|
||||
@@ -12,10 +12,13 @@ module Captain::ChatHelper
|
||||
)
|
||||
|
||||
handle_response(response)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error { "[CAPTAIN][ChatCompletion] #{e}" }
|
||||
raise e
|
||||
end
|
||||
|
||||
def handle_response(response)
|
||||
Rails.logger.debug { "[CAPTAIN][ChatCompletion] #{response}" }
|
||||
Rails.logger.info { "[CAPTAIN][ChatCompletion] #{response}" }
|
||||
message = response.dig('choices', 0, 'message')
|
||||
if message['tool_calls']
|
||||
process_tool_calls(message['tool_calls'])
|
||||
@@ -36,11 +39,13 @@ module Captain::ChatHelper
|
||||
arguments = JSON.parse(tool_call['function']['arguments'])
|
||||
function_name = tool_call['function']['name']
|
||||
tool_call_id = tool_call['id']
|
||||
function_name = tool_call['function']['name']
|
||||
arguments = JSON.parse(tool_call['function']['arguments'])
|
||||
|
||||
if @tool_registry.respond_to?(function_name)
|
||||
execute_tool(function_name, arguments, tool_call_id)
|
||||
else
|
||||
process_invalid_tool_call(tool_call_id)
|
||||
process_invalid_tool_call(tool_call_id, function_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,10 +61,6 @@ module Captain::ChatHelper
|
||||
}
|
||||
end
|
||||
|
||||
def process_invalid_tool_call(tool_call_id)
|
||||
append_tool_response('Tool not available', tool_call_id)
|
||||
end
|
||||
|
||||
def append_tool_response(content, tool_call_id)
|
||||
@messages << {
|
||||
role: 'tool',
|
||||
@@ -67,4 +68,37 @@ module Captain::ChatHelper
|
||||
content: content
|
||||
}
|
||||
end
|
||||
|
||||
def publish_to_stream(response)
|
||||
@stream_writer&.call(response)
|
||||
end
|
||||
|
||||
def execute_tool_call(tool_call_id, function_name, arguments)
|
||||
publish_to_stream(
|
||||
{
|
||||
response: { response: "Processing tool call #{function_name}" },
|
||||
type: 'tool_calls_start'
|
||||
}
|
||||
)
|
||||
result = @tool_registry.send(function_name, arguments.merge('user_id' => @user_id))
|
||||
append_tool_response(result, tool_call_id)
|
||||
publish_to_stream(
|
||||
{
|
||||
response: { response: "Received tool response #{function_name}" },
|
||||
type: 'tool_response',
|
||||
tool: function_name
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def process_invalid_tool_call(tool_call_id, function_name)
|
||||
append_tool_response('Tool not implemented', tool_call_id)
|
||||
publish_to_stream(
|
||||
{
|
||||
response: { response: 'Tool not implemented' },
|
||||
type: 'tool_error',
|
||||
tool: function_name
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
class Captain::ProcessCopilotMessageJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(assistant_id:, message:, options: {})
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Starting job for assistant_id=#{assistant_id}")
|
||||
ensure_assistant(assistant_id)
|
||||
ensure_user(options[:user_id])
|
||||
process_message(message, options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_assistant(assistant_id)
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Finding assistant with id=#{assistant_id}")
|
||||
@assistant = Captain::Assistant.find(assistant_id)
|
||||
@account = @assistant.account
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Found assistant for account_id=#{@account.id}")
|
||||
end
|
||||
|
||||
def ensure_user(user_id)
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Finding user with id=#{user_id}")
|
||||
@user = @account.users.find(user_id)
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Found user #{@user.name}")
|
||||
end
|
||||
|
||||
def process_message(message, options)
|
||||
return unless @assistant
|
||||
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Processing message with conversation_id=#{options[:conversation_id]}")
|
||||
conversation = find_conversation(options[:conversation_id])
|
||||
generate_chat_response(message, conversation, options[:previous_history])
|
||||
end
|
||||
|
||||
def find_conversation(conversation_id)
|
||||
return unless conversation_id
|
||||
|
||||
@account.conversations.find_by(display_id: conversation_id)
|
||||
end
|
||||
|
||||
def generate_chat_response(message, conversation, previous_history)
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Generating chat response for conversation_id=#{conversation&.display_id}, user_id=#{@user.id}")
|
||||
Captain::Copilot::ChatService.new(
|
||||
@assistant,
|
||||
previous_messages: previous_history || [],
|
||||
stream_writer: ->(data) { broadcast_response(data) },
|
||||
additional_info: {
|
||||
language: @account.locale_english_name,
|
||||
conversation_id: conversation&.display_id,
|
||||
contact_id: conversation&.contact_id,
|
||||
user_id: @user.id
|
||||
}
|
||||
).generate_response(message)
|
||||
end
|
||||
|
||||
def broadcast_response(data)
|
||||
Rails.logger.info("[Captain::ProcessCopilotMessageJob] Broadcasting response to account_id=#{@account.id} user_id=#{@user.id}")
|
||||
ActionCable.server.broadcast(
|
||||
"copilot_#{@account.id}_#{@user.id}",
|
||||
{ event: 'copilot.response', data: data }
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,27 +1,50 @@
|
||||
require 'openai'
|
||||
|
||||
class Captain::Copilot::ChatService < Llm::BaseOpenAiService
|
||||
include Captain::ChatHelper
|
||||
|
||||
def initialize(assistant, config)
|
||||
attr_reader :assistant, :language
|
||||
|
||||
def initialize(assistant, config = {})
|
||||
super()
|
||||
|
||||
@assistant = assistant
|
||||
@conversation_history = config[:conversation_history]
|
||||
@previous_messages = config[:previous_messages] || []
|
||||
@language = config[:language] || 'english'
|
||||
|
||||
@tool_registry = Captain::ToolRegistryService.new(@assistant)
|
||||
@stream_writer = config[:stream_writer]
|
||||
setup_additional_info(config)
|
||||
register_tools
|
||||
@messages = [system_message, conversation_history_context] + @previous_messages
|
||||
@response = ''
|
||||
@messages = build_initial_messages(config)
|
||||
end
|
||||
|
||||
def setup_additional_info(config)
|
||||
additional_info = config[:additional_info] || {}
|
||||
@language = additional_info[:language] || 'english'
|
||||
@conversation_id = additional_info[:conversation_id]
|
||||
@contact_id = additional_info[:contact_id]
|
||||
@user_id = additional_info[:user_id]
|
||||
Rails.logger.info("[Captain::Copilot::ChatService::User] user_id: #{@user_id}")
|
||||
end
|
||||
|
||||
def build_initial_messages(config)
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Building initial messages for conversation_id=#{@conversation_id}")
|
||||
messages = [system_message]
|
||||
messages += (config[:previous_messages] || [])
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Added #{config[:previous_messages]&.length || 0} previous messages")
|
||||
messages << current_viewing_history if @conversation_id
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Total messages built: #{messages.length}")
|
||||
messages
|
||||
end
|
||||
|
||||
def generate_response(input)
|
||||
@messages << { role: 'user', content: input } if input.present?
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Initial Prompt: #{@messages}")
|
||||
response = request_chat_completion
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Incrementing response usage for #{@assistant.account.id}")
|
||||
@assistant.account.increment_response_usage
|
||||
|
||||
publish_to_stream(
|
||||
{
|
||||
response: response,
|
||||
type: 'final_response'
|
||||
}
|
||||
)
|
||||
response
|
||||
end
|
||||
|
||||
@@ -30,22 +53,35 @@ class Captain::Copilot::ChatService < Llm::BaseOpenAiService
|
||||
def register_tools
|
||||
@tool_registry = Captain::ToolRegistryService.new(@assistant)
|
||||
@tool_registry.register_tool(Captain::Tools::SearchDocumentationService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::GetArticleService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::GetContactService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::GetConversationService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchArticlesService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchContactsService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchConversationsService)
|
||||
@tool_registry.register_tool(Captain::Tools::Copilot::SearchLinearIssuesService)
|
||||
end
|
||||
|
||||
def system_message
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Generating system message for product=#{@assistant.config['product_name']} language=#{@language}")
|
||||
{
|
||||
role: 'system',
|
||||
content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'], @language)
|
||||
content: Captain::Llm::SystemPromptsService.copilot_response_generator(@assistant.config['product_name'], @language, @assistant.account_id)
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_history_context
|
||||
def current_viewing_history
|
||||
Rails.logger.info("[CAPTAIN][CopilotChatService] Fetching viewing history for conversation_id=#{@conversation_id}")
|
||||
return unless @conversation_id
|
||||
|
||||
{
|
||||
role: 'system',
|
||||
content: "
|
||||
Message History with the user is below:
|
||||
#{@conversation_history}
|
||||
"
|
||||
content: <<~HISTORY.strip
|
||||
You are currently viewing the conversation with the user with the following details:
|
||||
Conversation ID: #{@conversation_id}
|
||||
Contact ID: #{@contact_id}
|
||||
Account ID: #{@assistant.account.id}
|
||||
HISTORY
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,14 +56,15 @@ class Captain::Llm::SystemPromptsService
|
||||
SYSTEM_PROMPT_MESSAGE
|
||||
end
|
||||
|
||||
def copilot_response_generator(product_name, language)
|
||||
def copilot_response_generator(product_name, language, account_id)
|
||||
<<~SYSTEM_PROMPT_MESSAGE
|
||||
[Identity]
|
||||
You are Captain, a helpful and friendly copilot assistant for support agents using the product #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions.
|
||||
You should only provide information related to #{product_name} and must not address queries about other products or external events.
|
||||
You are an assistant for the account with ID: #{account_id}
|
||||
|
||||
[Context]
|
||||
You will be provided with the message history between the support agent and the customer. Use this context to understand the conversation flow, identify unresolved queries, and ensure responses are relevant and consistent with previous interactions. Always maintain a coherent and professional tone throughout the conversation.
|
||||
Identify unresolved queries, and ensure responses are relevant and consistent with previous interactions. Always maintain a coherent and professional tone throughout the conversation.
|
||||
|
||||
[Response Guidelines]
|
||||
- Use natural, polite, and conversational language that is clear and easy to follow. Keep sentences short and use simple words.
|
||||
@@ -90,6 +91,9 @@ class Captain::Llm::SystemPromptsService
|
||||
7. Write the response in multiple paragraphs and in markdown format.
|
||||
8. DO NOT use headings in Markdown
|
||||
9. Cite the sources if you used a tool to find the response.
|
||||
10. Do not use your own training data or assumptions to answer queries. Base responses strictly on the provided information.
|
||||
11. Always provide a reasoning for the response.
|
||||
12. Always double the check the information with tools unless you are very sure of the information.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user