feat: Add a review step for FAQs generated from conversations before using it (#10693)
This PR introduces a review step for generated FAQs, allowing a human to validate and approve them before use in customer interactions. While hallucinations are minimal, this step ensures accurate and reliable FAQs for Captain to use during LLM calls when responding to customers. - Added a status field for the FAQ - Allow the filter on the UI. <img width="1072" alt="Screenshot 2025-01-15 at 6 39 26 PM" src="https://github.com/user-attachments/assets/81dfc038-31e9-40e6-8a09-586ebc4e8384" />
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
@@ -16,13 +21,75 @@ const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const selectedResponse = ref(null);
|
||||
const deleteDialog = ref(null);
|
||||
|
||||
const selectedStatus = ref('all');
|
||||
const selectedAssistant = ref('all');
|
||||
const dialogType = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const createDialog = ref(null);
|
||||
|
||||
const isStatusFilterOpen = ref(false);
|
||||
const isAssistantFilterOpen = ref(false);
|
||||
|
||||
const statusOptions = computed(() =>
|
||||
['all', 'pending', 'approved'].map(key => ({
|
||||
label: t(`CAPTAIN.RESPONSES.STATUS.${key.toUpperCase()}`),
|
||||
value: key,
|
||||
action: 'filter',
|
||||
}))
|
||||
);
|
||||
|
||||
const selectedStatusLabel = computed(() => {
|
||||
const status = statusOptions.value.find(
|
||||
option => option.value === selectedStatus.value
|
||||
);
|
||||
return t('CAPTAIN.RESPONSES.FILTER.STATUS', {
|
||||
selected: status ? status.label : '',
|
||||
});
|
||||
});
|
||||
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const assistantOptions = computed(() => [
|
||||
{
|
||||
label: t(`CAPTAIN.RESPONSES.FILTER.ALL_ASSISTANTS`),
|
||||
value: 'all',
|
||||
action: 'filter',
|
||||
},
|
||||
...assistants.value.map(assistant => ({
|
||||
value: assistant.id,
|
||||
label: assistant.name,
|
||||
action: 'filter',
|
||||
})),
|
||||
]);
|
||||
|
||||
const selectedAssistantLabel = computed(() => {
|
||||
const assistant = assistantOptions.value.find(
|
||||
option => option.value === selectedAssistant.value
|
||||
);
|
||||
return t('CAPTAIN.RESPONSES.FILTER.ASSISTANT', {
|
||||
selected: assistant ? assistant.label : '',
|
||||
});
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const createDialog = ref(null);
|
||||
const handleAccept = async () => {
|
||||
try {
|
||||
await store.dispatch('captainResponses/update', {
|
||||
id: selectedResponse.value.id,
|
||||
status: 'approved',
|
||||
});
|
||||
useAlert(t(`CAPTAIN.RESPONSES.EDIT.APPROVE_SUCCESS_MESSAGE`));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t(`CAPTAIN.RESPONSES.EDIT.ERROR_MESSAGE`);
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
selectedResponse.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create';
|
||||
@@ -43,6 +110,9 @@ const handleAction = ({ action, id }) => {
|
||||
if (action === 'edit') {
|
||||
handleEdit();
|
||||
}
|
||||
if (action === 'approve') {
|
||||
handleAccept();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -52,11 +122,30 @@ const handleCreateClose = () => {
|
||||
};
|
||||
|
||||
const fetchResponses = (page = 1) => {
|
||||
store.dispatch('captainResponses/get', { page });
|
||||
const filterParams = {};
|
||||
if (selectedStatus.value !== 'all') {
|
||||
filterParams.status = selectedStatus.value;
|
||||
}
|
||||
if (selectedAssistant.value !== 'all') {
|
||||
filterParams.assistantId = selectedAssistant.value;
|
||||
}
|
||||
store.dispatch('captainResponses/get', { page, ...filterParams });
|
||||
};
|
||||
|
||||
const onPageChange = page => fetchResponses(page);
|
||||
|
||||
const handleStatusFilterChange = ({ value }) => {
|
||||
selectedStatus.value = value;
|
||||
isStatusFilterOpen.value = false;
|
||||
fetchResponses();
|
||||
};
|
||||
|
||||
const handleAssistantFilterChange = ({ value }) => {
|
||||
selectedAssistant.value = value;
|
||||
isAssistantFilterOpen.value = false;
|
||||
fetchResponses();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
fetchResponses();
|
||||
@@ -73,12 +162,52 @@ onMounted(() => {
|
||||
@update:current-page="onPageChange"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<div v-if="!isFetching" class="mb-4 -mt-3 flex gap-3">
|
||||
<OnClickOutside @trigger="isStatusFilterOpen = false">
|
||||
<Button
|
||||
:label="selectedStatusLabel"
|
||||
icon="i-lucide-chevron-down"
|
||||
size="sm"
|
||||
color="slate"
|
||||
trailing-icon
|
||||
class="max-w-48"
|
||||
@click="isStatusFilterOpen = !isStatusFilterOpen"
|
||||
/>
|
||||
|
||||
<DropdownMenu
|
||||
v-if="isStatusFilterOpen"
|
||||
:menu-items="statusOptions"
|
||||
class="mt-2"
|
||||
@action="handleStatusFilterChange"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
|
||||
<OnClickOutside @trigger="isAssistantFilterOpen = false">
|
||||
<Button
|
||||
:label="selectedAssistantLabel"
|
||||
icon="i-lucide-chevron-down"
|
||||
size="sm"
|
||||
color="slate"
|
||||
trailing-icon
|
||||
class="max-w-48"
|
||||
@click="isAssistantFilterOpen = !isAssistantFilterOpen"
|
||||
/>
|
||||
|
||||
<DropdownMenu
|
||||
v-if="isAssistantFilterOpen"
|
||||
:menu-items="assistantOptions"
|
||||
class="mt-2"
|
||||
@action="handleAssistantFilterChange"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
<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"
|
||||
@@ -87,6 +216,7 @@ onMounted(() => {
|
||||
:question="response.question"
|
||||
:answer="response.answer"
|
||||
:assistant="response.assistant"
|
||||
:status="response.status"
|
||||
:created-at="response.created_at"
|
||||
:updated-at="response.updated_at"
|
||||
@action="handleAction"
|
||||
|
||||
Reference in New Issue
Block a user