feat(ee): Add Captain features (#10665)
Migration Guide: https://chwt.app/v4/migration This PR imports all the work related to Captain into the EE codebase. Captain represents the AI-based features in Chatwoot and includes the following key components: - Assistant: An assistant has a persona, the product it would be trained on. At the moment, the data at which it is trained is from websites. Future integrations on Notion documents, PDF etc. This PR enables connecting an assistant to an inbox. The assistant would run the conversation every time before transferring it to an agent. - Copilot for Agents: When an agent is supporting a customer, we will be able to offer additional help to lookup some data or fetch information from integrations etc via copilot. - Conversation FAQ generator: When a conversation is resolved, the Captain integration would identify questions which were not in the knowledge base. - CRM memory: Learns from the conversations and identifies important information about the contact. --------- Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com> Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -1,107 +0,0 @@
|
||||
<script setup>
|
||||
import { nextTick, watch, computed } from 'vue';
|
||||
import IntegrationsAPI from 'dashboard/api/integrations';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { makeRouter, setupApp } from '@chatwoot/captain';
|
||||
|
||||
const props = defineProps({
|
||||
page: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const routeMap = {
|
||||
documents: '/app/accounts/[account_id]/documents/',
|
||||
playground: '/app/accounts/[account_id]/playground/',
|
||||
responses: '/app/accounts/[account_id]/responses/',
|
||||
};
|
||||
|
||||
const resolvedRoute = computed(() => routeMap[props.page]);
|
||||
|
||||
let router = null;
|
||||
|
||||
watch(
|
||||
() => props.page,
|
||||
() => {
|
||||
if (router) {
|
||||
router.push({ name: resolvedRoute.value });
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const buildApp = () => {
|
||||
router = makeRouter();
|
||||
setupApp('#captain', {
|
||||
router,
|
||||
fetchFn: async (source, options) => {
|
||||
const parsedSource = new URL(source);
|
||||
let path = parsedSource.pathname;
|
||||
if (path === `/api/sessions/profile`) {
|
||||
path = '/sessions/profile';
|
||||
} else {
|
||||
path = path.replace(/^\/api\/accounts\/\d+/, '');
|
||||
}
|
||||
|
||||
// include search params
|
||||
path = `${path}${parsedSource.search}`;
|
||||
|
||||
const response = await IntegrationsAPI.requestCaptain({
|
||||
method: options.method ?? 'GET',
|
||||
route: path,
|
||||
body: options.body ? JSON.parse(options.body) : null,
|
||||
});
|
||||
|
||||
return {
|
||||
json: () => {
|
||||
return response.data;
|
||||
},
|
||||
ok: response.status >= 200 && response.status < 300,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
router.push({ name: resolvedRoute.value });
|
||||
};
|
||||
|
||||
const captainIntegration = computed(() =>
|
||||
getters['integrations/getIntegration'].value('captain', null)
|
||||
);
|
||||
|
||||
watch(
|
||||
() => captainIntegration.value,
|
||||
(newValue, prevValue) => {
|
||||
if (!prevValue && newValue) {
|
||||
nextTick(() => buildApp());
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!captainIntegration">
|
||||
{{ $t('INTEGRATION_SETTINGS.CAPTAIN.DISABLED') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!captainIntegration.enabled"
|
||||
class="flex-1 flex flex-col gap-2 items-center justify-center"
|
||||
>
|
||||
<div>{{ $t('INTEGRATION_SETTINGS.CAPTAIN.DISABLED') }}</div>
|
||||
<router-link :to="{ name: 'settings_applications' }">
|
||||
<woot-button class="clear link">
|
||||
{{ $t('INTEGRATION_SETTINGS.CAPTAIN.CLICK_HERE_TO_CONFIGURE') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-else id="captain" class="w-full" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import '@chatwoot/captain/dist/style.css';
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import AssistantCard from 'dashboard/components-next/captain/assistant/AssistantCard.vue';
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const store = useStore();
|
||||
const dialogType = ref('');
|
||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const selectedAssistant = ref(null);
|
||||
const deleteAssistantDialog = ref(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteAssistantDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const createAssistantDialog = ref(null);
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create';
|
||||
nextTick(() => createAssistantDialog.value.dialogRef.open());
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
dialogType.value = 'edit';
|
||||
nextTick(() => createAssistantDialog.value.dialogRef.open());
|
||||
};
|
||||
|
||||
const handleViewConnectedInboxes = () => {
|
||||
router.push({
|
||||
name: 'captain_assistants_inboxes_index',
|
||||
params: { assistantId: selectedAssistant.value.id },
|
||||
});
|
||||
};
|
||||
|
||||
const handleAction = ({ action, id }) => {
|
||||
selectedAssistant.value = assistants.value.find(
|
||||
assistant => id === assistant.id
|
||||
);
|
||||
nextTick(() => {
|
||||
if (action === 'delete') {
|
||||
handleDelete();
|
||||
}
|
||||
if (action === 'edit') {
|
||||
handleEdit();
|
||||
}
|
||||
if (action === 'viewConnectedInboxes') {
|
||||
handleViewConnectedInboxes();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateClose = () => {
|
||||
dialogType.value = '';
|
||||
selectedAssistant.value = null;
|
||||
};
|
||||
|
||||
onMounted(() => store.dispatch('captainAssistants/get'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.ASSISTANTS.HEADER')"
|
||||
:button-label="$t('CAPTAIN.ASSISTANTS.ADD_NEW')"
|
||||
:show-pagination-footer="false"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="assistants.length" class="flex flex-col gap-4">
|
||||
<AssistantCard
|
||||
v-for="assistant in assistants"
|
||||
:id="assistant.id"
|
||||
:key="assistant.id"
|
||||
:name="assistant.name"
|
||||
:description="assistant.description"
|
||||
:updated-at="assistant.updated_at || assistant.created_at"
|
||||
:created-at="assistant.created_at"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>{{ 'No assistants found' }}</div>
|
||||
|
||||
<DeleteDialog
|
||||
v-if="selectedAssistant"
|
||||
ref="deleteAssistantDialog"
|
||||
:entity="selectedAssistant"
|
||||
type="Assistants"
|
||||
/>
|
||||
|
||||
<CreateAssistantDialog
|
||||
v-if="dialogType"
|
||||
ref="createAssistantDialog"
|
||||
:type="dialogType"
|
||||
:selected-assistant="selectedAssistant"
|
||||
@close="handleCreateClose"
|
||||
/>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeMount, onMounted, ref, nextTick } from 'vue';
|
||||
import {
|
||||
useMapGetter,
|
||||
useStore,
|
||||
useStoreGetters,
|
||||
} from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import BackButton from 'dashboard/components/widgets/BackButton.vue';
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import ConnectInboxDialog from 'dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue';
|
||||
import InboxCard from 'dashboard/components-next/captain/assistant/InboxCard.vue';
|
||||
|
||||
const store = useStore();
|
||||
const dialogType = ref('');
|
||||
const route = useRoute();
|
||||
const assistantUiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||
const uiFlags = useMapGetter('captainInboxes/getUIFlags');
|
||||
const isFetchingAssistant = computed(() => assistantUiFlags.value.fetchingItem);
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const captainInboxes = useMapGetter('captainInboxes/getRecords');
|
||||
|
||||
const selectedInbox = ref(null);
|
||||
const disconnectInboxDialog = ref(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
disconnectInboxDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const connectInboxDialog = ref(null);
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create';
|
||||
nextTick(() => connectInboxDialog.value.dialogRef.open());
|
||||
};
|
||||
const handleAction = ({ action, id }) => {
|
||||
selectedInbox.value = captainInboxes.value.find(inbox => id === inbox.id);
|
||||
nextTick(() => {
|
||||
if (action === 'delete') {
|
||||
handleDelete();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateClose = () => {
|
||||
dialogType.value = '';
|
||||
selectedInbox.value = null;
|
||||
};
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const assistantId = Number(route.params.assistantId);
|
||||
const assistant = computed(() =>
|
||||
getters['captainAssistants/getRecord'].value(assistantId)
|
||||
);
|
||||
onBeforeMount(() => store.dispatch('captainAssistants/show', assistantId));
|
||||
|
||||
onMounted(() =>
|
||||
store.dispatch('captainInboxes/get', {
|
||||
assistantId: assistantId,
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isFetchingAssistant"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<PageLayout
|
||||
v-else
|
||||
:button-label="$t('CAPTAIN.INBOXES.ADD_NEW')"
|
||||
:show-pagination-footer="false"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<BackButton compact />
|
||||
<span class="flex items-center gap-1 text-lg">
|
||||
{{ assistant.name }}
|
||||
<span class="i-lucide-chevron-right text-xl text-n-slate-10" />
|
||||
{{ $t('CAPTAIN.INBOXES.HEADER') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="captainInboxes.length" class="flex flex-col gap-4">
|
||||
<InboxCard
|
||||
v-for="captainInbox in captainInboxes"
|
||||
:id="captainInbox.id"
|
||||
:key="captainInbox.id"
|
||||
:inbox="captainInbox"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>{{ 'There are no connected inboxes' }}</div>
|
||||
|
||||
<DeleteDialog
|
||||
v-if="selectedInbox"
|
||||
ref="disconnectInboxDialog"
|
||||
:entity="selectedInbox"
|
||||
:delete-payload="{
|
||||
assistantId: assistantId,
|
||||
inboxId: selectedInbox.id,
|
||||
}"
|
||||
type="Inboxes"
|
||||
/>
|
||||
|
||||
<ConnectInboxDialog
|
||||
v-if="dialogType"
|
||||
ref="connectInboxDialog"
|
||||
:assistant-id="assistantId"
|
||||
:type="dialogType"
|
||||
@close="handleCreateClose"
|
||||
/>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import AssistantIndex from './assistants/Index.vue';
|
||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
||||
import DocumentsIndex from './documents/Index.vue';
|
||||
import ResponsesIndex from './responses/Index.vue';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/assistants'),
|
||||
component: AssistantIndex,
|
||||
name: 'captain_assistants_index',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
|
||||
),
|
||||
component: AssistantInboxesIndex,
|
||||
name: 'captain_assistants_inboxes_index',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/documents'),
|
||||
component: DocumentsIndex,
|
||||
name: 'captain_documents_index',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/responses'),
|
||||
component: ResponsesIndex,
|
||||
name: 'captain_responses_index',
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import DocumentCard from 'dashboard/components-next/captain/assistant/DocumentCard.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import RelatedResponses from 'dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue';
|
||||
import CreateDocumentDialog from '../../../../components-next/captain/pageComponents/document/CreateDocumentDialog.vue';
|
||||
const store = useStore();
|
||||
|
||||
const uiFlags = useMapGetter('captainDocuments/getUIFlags');
|
||||
const documents = useMapGetter('captainDocuments/getRecords');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const documentsMeta = useMapGetter('captainDocuments/getMeta');
|
||||
|
||||
const selectedDocument = ref(null);
|
||||
const deleteDocumentDialog = ref(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteDocumentDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const showRelatedResponses = ref(false);
|
||||
const showCreateDialog = ref(false);
|
||||
const createDocumentDialog = ref(null);
|
||||
const relationQuestionDialog = ref(null);
|
||||
|
||||
const handleShowRelatedDocument = () => {
|
||||
showRelatedResponses.value = true;
|
||||
nextTick(() => relationQuestionDialog.value.dialogRef.open());
|
||||
};
|
||||
const handleCreateDocument = () => {
|
||||
showCreateDialog.value = true;
|
||||
nextTick(() => createDocumentDialog.value.dialogRef.open());
|
||||
};
|
||||
|
||||
const handleRelatedResponseClose = () => {
|
||||
showRelatedResponses.value = false;
|
||||
};
|
||||
|
||||
const handleCreateDialogClose = () => {
|
||||
showCreateDialog.value = false;
|
||||
};
|
||||
|
||||
const handleAction = ({ action, id }) => {
|
||||
selectedDocument.value = documents.value.find(
|
||||
captainDocument => id === captainDocument.id
|
||||
);
|
||||
|
||||
nextTick(() => {
|
||||
if (action === 'delete') {
|
||||
handleDelete();
|
||||
} else if (action === 'viewRelatedQuestions') {
|
||||
handleShowRelatedDocument();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDocuments = (page = 1) => {
|
||||
store.dispatch('captainDocuments/get', { page });
|
||||
};
|
||||
|
||||
const onPageChange = page => fetchDocuments(page);
|
||||
|
||||
onMounted(() => {
|
||||
if (!assistants.value.length) {
|
||||
store.dispatch('captainAssistants/get');
|
||||
}
|
||||
fetchDocuments();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.DOCUMENTS.HEADER')"
|
||||
:button-label="$t('CAPTAIN.DOCUMENTS.ADD_NEW')"
|
||||
:total-count="documentsMeta.totalCount"
|
||||
:current-page="documentsMeta.page"
|
||||
:show-pagination-footer="!isFetching && !!documents.length"
|
||||
@update:current-page="onPageChange"
|
||||
@click="handleCreateDocument"
|
||||
>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="documents.length" class="flex flex-col gap-4">
|
||||
<DocumentCard
|
||||
v-for="doc in documents"
|
||||
:id="doc.id"
|
||||
:key="doc.id"
|
||||
:name="doc.name || doc.external_link"
|
||||
:external-link="doc.external_link"
|
||||
:assistant="doc.assistant"
|
||||
:created-at="doc.created_at"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>{{ 'No documents found' }}</div>
|
||||
<RelatedResponses
|
||||
v-if="showRelatedResponses"
|
||||
ref="relationQuestionDialog"
|
||||
:captain-document="selectedDocument"
|
||||
@close="handleRelatedResponseClose"
|
||||
/>
|
||||
<CreateDocumentDialog
|
||||
v-if="showCreateDialog"
|
||||
ref="createDocumentDialog"
|
||||
@close="handleCreateDialogClose"
|
||||
/>
|
||||
<DeleteDialog
|
||||
v-if="selectedDocument"
|
||||
ref="deleteDocumentDialog"
|
||||
:entity="selectedDocument"
|
||||
type="Documents"
|
||||
/>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
||||
|
||||
const store = useStore();
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const selectedResponse = ref(null);
|
||||
const deleteDialog = ref(null);
|
||||
const dialogType = ref('');
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const createDialog = ref(null);
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create';
|
||||
nextTick(() => createDialog.value.dialogRef.open());
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
dialogType.value = 'edit';
|
||||
nextTick(() => createDialog.value.dialogRef.open());
|
||||
};
|
||||
|
||||
const handleAction = ({ action, id }) => {
|
||||
selectedResponse.value = responses.value.find(response => id === response.id);
|
||||
nextTick(() => {
|
||||
if (action === 'delete') {
|
||||
handleDelete();
|
||||
}
|
||||
if (action === 'edit') {
|
||||
handleEdit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateClose = () => {
|
||||
dialogType.value = '';
|
||||
selectedResponse.value = null;
|
||||
};
|
||||
|
||||
const fetchResponses = (page = 1) => {
|
||||
store.dispatch('captainResponses/get', { page });
|
||||
};
|
||||
|
||||
const onPageChange = page => fetchResponses(page);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
fetchResponses();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
:total-count="responseMeta.totalCount"
|
||||
:current-page="responseMeta.page"
|
||||
:header-title="$t('CAPTAIN.RESPONSES.HEADER')"
|
||||
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
|
||||
:show-pagination-footer="!isFetching && !!responses.length"
|
||||
@update:current-page="onPageChange"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="responses.length" class="flex flex-col gap-4">
|
||||
<ResponseCard
|
||||
v-for="response in responses"
|
||||
:id="response.id"
|
||||
:key="response.id"
|
||||
:question="response.question"
|
||||
:answer="response.answer"
|
||||
:assistant="response.assistant"
|
||||
:created-at="response.created_at"
|
||||
:updated-at="response.updated_at"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>{{ 'No responses found' }}</div>
|
||||
|
||||
<DeleteDialog
|
||||
v-if="selectedResponse"
|
||||
ref="deleteDialog"
|
||||
:entity="selectedResponse"
|
||||
type="Responses"
|
||||
/>
|
||||
|
||||
<CreateResponseDialog
|
||||
v-if="dialogType"
|
||||
ref="createDialog"
|
||||
:type="dialogType"
|
||||
:selected-response="selectedResponse"
|
||||
@close="handleCreateClose"
|
||||
/>
|
||||
</PageLayout>
|
||||
</template>
|
||||
@@ -7,11 +7,8 @@ import { routes as inboxRoutes } from './inbox/routes';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||
import campaignsRoutes from './campaigns/campaigns.routes';
|
||||
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import { routes as captainRoutes } from './captain/captain.routes';
|
||||
import AppContainer from './Dashboard.vue';
|
||||
import Captain from './Captain.vue';
|
||||
import Suspended from './suspended/Index.vue';
|
||||
|
||||
export default {
|
||||
@@ -20,16 +17,7 @@ export default {
|
||||
path: frontendURL('accounts/:accountId'),
|
||||
component: AppContainer,
|
||||
children: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/:page'),
|
||||
name: 'captain',
|
||||
component: Captain,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
...captainRoutes,
|
||||
...inboxRoutes,
|
||||
...conversation.routes,
|
||||
...settings.routes,
|
||||
|
||||
Reference in New Issue
Block a user