feat: Adiciona a opção "Criar FAQ" no menu de contexto da mensagem, permitindo criar uma resposta com seleção de assistente.
This commit is contained in:
parent
fa061dc08c
commit
ebed1caeb4
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
@ -27,6 +27,22 @@ const route = useRoute();
|
||||
const dialogRef = ref(null);
|
||||
const responseForm = ref(null);
|
||||
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
|
||||
const assistantOptions = computed(() => {
|
||||
if (route.params.assistantId) return [];
|
||||
return assistants.value.map(assistant => ({
|
||||
label: assistant.name,
|
||||
value: assistant.id,
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!route.params.assistantId && !assistants.value.length) {
|
||||
store.dispatch('captainAssistants/get');
|
||||
}
|
||||
});
|
||||
|
||||
const updateResponse = responseDetails =>
|
||||
store.dispatch('captainResponses/update', {
|
||||
id: props.selectedResponse.id,
|
||||
@ -40,15 +56,17 @@ const createResponse = responseDetails =>
|
||||
|
||||
const handleSubmit = async updatedResponse => {
|
||||
try {
|
||||
const assistantId =
|
||||
route.params.assistantId || updatedResponse.assistant_id;
|
||||
if (props.type === 'edit') {
|
||||
await updateResponse({
|
||||
...updatedResponse,
|
||||
assistant_id: route.params.assistantId,
|
||||
assistant_id: assistantId,
|
||||
});
|
||||
} else {
|
||||
await createResponse({
|
||||
...updatedResponse,
|
||||
assistant_id: route.params.assistantId,
|
||||
assistant_id: assistantId,
|
||||
});
|
||||
}
|
||||
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
|
||||
@ -84,6 +102,7 @@ defineExpose({ dialogRef });
|
||||
ref="responseForm"
|
||||
:mode="type"
|
||||
:response="selectedResponse"
|
||||
:assistants="assistantOptions"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
|
||||
@ -8,6 +8,7 @@ import { useMapGetter } from 'dashboard/composables/store';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
@ -19,6 +20,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
assistants: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel']);
|
||||
@ -31,14 +36,23 @@ const formState = {
|
||||
const initialState = {
|
||||
question: '',
|
||||
answer: '',
|
||||
assistant_id: '',
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
|
||||
const validationRules = {
|
||||
question: { required, minLength: minLength(1) },
|
||||
answer: { required, minLength: minLength(1) },
|
||||
};
|
||||
const validationRules = computed(() => {
|
||||
const rules = {
|
||||
question: { required, minLength: minLength(1) },
|
||||
answer: { required, minLength: minLength(1) },
|
||||
};
|
||||
|
||||
if (props.assistants && props.assistants.length > 0) {
|
||||
rules.assistant_id = { required };
|
||||
}
|
||||
|
||||
return rules;
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
@ -53,6 +67,12 @@ const getErrorMessage = (field, errorKey) => {
|
||||
const formErrors = computed(() => ({
|
||||
question: getErrorMessage('question', 'QUESTION'),
|
||||
answer: getErrorMessage('answer', 'ANSWER'),
|
||||
assistant_id: v$.value.assistant_id?.$error
|
||||
? t(
|
||||
'CAPTAIN.RESPONSES.FORM.ASSISTANT.ERROR',
|
||||
'Por favor, selecione um assistente.'
|
||||
)
|
||||
: '',
|
||||
}));
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
@ -60,6 +80,7 @@ const handleCancel = () => emit('cancel');
|
||||
const prepareDocumentDetails = () => ({
|
||||
question: state.question,
|
||||
answer: state.answer,
|
||||
...(state.assistant_id ? { assistant_id: state.assistant_id } : {}),
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -74,18 +95,19 @@ const handleSubmit = async () => {
|
||||
const updateStateFromResponse = response => {
|
||||
if (!response) return;
|
||||
|
||||
const { question, answer } = response;
|
||||
const { question, answer, assistant_id } = response;
|
||||
|
||||
Object.assign(state, {
|
||||
question,
|
||||
answer,
|
||||
assistant_id: assistant_id || '',
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.response,
|
||||
newResponse => {
|
||||
if (props.mode === 'edit' && newResponse) {
|
||||
if (newResponse) {
|
||||
updateStateFromResponse(newResponse);
|
||||
}
|
||||
},
|
||||
@ -110,7 +132,27 @@ watch(
|
||||
:max-length="10000"
|
||||
:message-type="formErrors.answer ? 'error' : 'info'"
|
||||
/>
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<div
|
||||
v-if="assistants && assistants.length > 0"
|
||||
class="flex flex-col w-full gap-2"
|
||||
>
|
||||
<label class="text-sm font-medium text-n-slate-11">
|
||||
{{ t('CAPTAIN.RESPONSES.FORM.ASSISTANT.LABEL', 'Assistente') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
v-model="state.assistant_id"
|
||||
:options="assistants"
|
||||
:has-error="!!formErrors.assistant_id"
|
||||
:message="formErrors.assistant_id"
|
||||
:placeholder="
|
||||
t(
|
||||
'CAPTAIN.RESPONSES.FORM.ASSISTANT.PLACEHOLDER',
|
||||
'Por favor selecione o Assistente'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full gap-3 mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
|
||||
@ -379,7 +379,7 @@ const contextMenuEnabledOptions = computed(() => {
|
||||
(hasText || hasAttachments) &&
|
||||
!isFailedOrProcessing &&
|
||||
!isMessageDeleted.value,
|
||||
cannedResponse: isOutgoing && hasText && !isMessageDeleted.value,
|
||||
cannedResponse: !isMessageDeleted.value,
|
||||
copyLink: !isFailedOrProcessing,
|
||||
translate: !isFailedOrProcessing && !isMessageDeleted.value && hasText,
|
||||
replyTo:
|
||||
|
||||
@ -277,5 +277,107 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CAPTAIN": {
|
||||
"BANNER": {
|
||||
"RESPONSES": "Você usou mais de 80% do seu limite de respostas. Para continuar usando o Captain AI, faça um upgrade.",
|
||||
"DOCUMENTS": "Limite de documentos atingido. Faça um upgrade para continuar usando o Captain AI."
|
||||
},
|
||||
"FORM": {
|
||||
"CANCEL": "Cancelar",
|
||||
"CREATE": "Criar",
|
||||
"EDIT": "Atualizar"
|
||||
},
|
||||
"RESPONSES": {
|
||||
"HEADER": "FAQs",
|
||||
"PENDING_FAQS": "Pending FAQs",
|
||||
"ADD_NEW": "Criar nova FAQ",
|
||||
"DOCUMENTABLE": {
|
||||
"CONVERSATION": "Conversação #{id}"
|
||||
},
|
||||
"SELECTED": "{count} selecionado",
|
||||
"SELECT_ALL": "Selecionar todos ({count})",
|
||||
"UNSELECT_ALL": "Desmarcar todos ({count})",
|
||||
"SEARCH_PLACEHOLDER": "Pesquisar FAQs...",
|
||||
"BULK_APPROVE_BUTTON": "Aprovar",
|
||||
"BULK_DELETE_BUTTON": "Excluir",
|
||||
"BULK_APPROVE": {
|
||||
"SUCCESS_MESSAGE": "Perguntas Frequentes aprovadas com sucesso",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro ao aprovar as Perguntas Frequentes. Tente novamente."
|
||||
},
|
||||
"BULK_DELETE": {
|
||||
"TITLE": "Excluir as Perguntas Frequentes?",
|
||||
"DESCRIPTION": "Tem certeza que deseja excluir as Perguntas Frequentes selecionadas? Esta ação não pode ser desfeita.",
|
||||
"CONFIRM": "Sim, excluir todas",
|
||||
"SUCCESS_MESSAGE": "Perguntas Frequentes excluídas com sucesso/",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro ao excluir as Perguntas Frequentes, por favor tente novamente."
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Tem certeza que deseja excluir o FAQ?",
|
||||
"DESCRIPTION": "",
|
||||
"CONFIRM": "Sim, excluir",
|
||||
"SUCCESS_MESSAGE": "FAQ excluída com sucesso",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro ao excluir a FAQ, por favor tente novamente."
|
||||
},
|
||||
"FILTER": {
|
||||
"ASSISTANT": "Assistente: {selected}",
|
||||
"STATUS": "Status: {selected}",
|
||||
"ALL_ASSISTANTS": "Todos"
|
||||
},
|
||||
"STATUS": {
|
||||
"TITLE": "Status",
|
||||
"PENDING": "Pendentes",
|
||||
"APPROVED": "Aceito",
|
||||
"ALL": "Todos"
|
||||
},
|
||||
"PENDING_BANNER": {
|
||||
"TITLE": "Captain has found some FAQs your customers were looking for.",
|
||||
"ACTION": "Click here to review"
|
||||
},
|
||||
"FORM_DESCRIPTION": "Adicione uma pergunta e sua resposta correspondente à base de conhecimento e selecione o assistente ao qual deve estar associado.",
|
||||
"CREATE": {
|
||||
"TITLE": "Adicionar uma FAQ",
|
||||
"SUCCESS_MESSAGE": "A resposta foi adicionada com sucesso.",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro ao adicionar a resposta. Por favor, tente novamente."
|
||||
},
|
||||
"FORM": {
|
||||
"QUESTION": {
|
||||
"LABEL": "Pergunta",
|
||||
"PLACEHOLDER": "Digite a pergunta aqui",
|
||||
"ERROR": "Por favor, forneça uma pergunta válida."
|
||||
},
|
||||
"ANSWER": {
|
||||
"LABEL": "Responder",
|
||||
"PLACEHOLDER": "Digite a resposta aqui",
|
||||
"ERROR": "Por favor, forneça uma resposta válida."
|
||||
},
|
||||
"ASSISTANT": {
|
||||
"LABEL": "Assistente",
|
||||
"PLACEHOLDER": "Selecione um assistente",
|
||||
"ERROR": "Por favor, selecione um assistente."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Atualizar as Perguntas Frequentes",
|
||||
"SUCCESS_MESSAGE": "As Perguntas Frequentes foram atualizadas com sucesso",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro ao atualizar as Perguntas Frequentes, por favor tente novamente",
|
||||
"APPROVE_SUCCESS_MESSAGE": "As Perguntas Frequentes foram marcadas como aprovadas"
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPROVE": "Aprovar",
|
||||
"EDIT_RESPONSE": "Alterar",
|
||||
"DELETE_RESPONSE": "Excluir"
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "Nenhuma FAQ encontrada",
|
||||
"NO_PENDING_TITLE": "There are no more pending FAQs to review",
|
||||
"SUBTITLE": "Perguntas Frequentes ajudam seu assistente a fornecer respostas rápidas e precisas para perguntas de seus clientes. Eles podem ser gerados automaticamente a partir do seu conteúdo ou podem ser adicionados manualmente.",
|
||||
"CLEAR_SEARCH": "Clear active filters",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Captain FAQ",
|
||||
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { mapGetters } from 'vuex';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
|
||||
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
|
||||
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
||||
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
|
||||
@ -20,6 +21,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
export default {
|
||||
components: {
|
||||
AddCannedModal,
|
||||
CreateResponseDialog,
|
||||
MenuItem,
|
||||
ContextMenu,
|
||||
NextButton,
|
||||
@ -60,6 +62,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isCannedResponseModalOpen: false,
|
||||
isFaqModalOpen: false,
|
||||
showDeleteModal: false,
|
||||
showEditModal: false,
|
||||
editedContent: '',
|
||||
@ -71,6 +74,7 @@ export default {
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
getUISettings: 'getUISettings',
|
||||
copilotAssistant: 'getCopilotAssistant',
|
||||
}),
|
||||
plainTextContent() {
|
||||
return this.getPlainText(this.messageContent);
|
||||
@ -125,10 +129,29 @@ export default {
|
||||
showCannedResponseModal() {
|
||||
useTrack(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE);
|
||||
this.isCannedResponseModalOpen = true;
|
||||
this.handleClose();
|
||||
},
|
||||
hideCannedResponseModal() {
|
||||
this.isCannedResponseModalOpen = false;
|
||||
},
|
||||
async showFaqModal() {
|
||||
useTrack(ACCOUNT_EVENTS.ADDED_TO_CANNED_RESPONSE);
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
'getInboxCaptainAssistantById',
|
||||
this.conversationId
|
||||
);
|
||||
} catch (error) {
|
||||
// Silence error, we can still open the modal
|
||||
}
|
||||
this.isFaqModalOpen = true;
|
||||
this.handleClose();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.faqDialog?.dialogRef?.open();
|
||||
});
|
||||
},
|
||||
hideFaqModal() {
|
||||
this.isFaqModalOpen = false;
|
||||
},
|
||||
handleOpen(e) {
|
||||
this.$emit('open', e);
|
||||
@ -163,7 +186,7 @@ export default {
|
||||
messageId: this.messageId,
|
||||
});
|
||||
useAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
|
||||
this.handleClose();
|
||||
this.showDeleteModal = false;
|
||||
} catch (error) {
|
||||
useAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
|
||||
}
|
||||
@ -213,7 +236,7 @@ export default {
|
||||
<div class="context-menu">
|
||||
<!-- Add To Canned Responses -->
|
||||
<woot-modal
|
||||
v-if="isCannedResponseModalOpen && enabledOptions['cannedResponse']"
|
||||
v-if="isCannedResponseModalOpen"
|
||||
v-model:show="isCannedResponseModalOpen"
|
||||
:on-close="hideCannedResponseModal"
|
||||
>
|
||||
@ -222,9 +245,20 @@ export default {
|
||||
:on-close="hideCannedResponseModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
<!-- Add To FAQ -->
|
||||
<CreateResponseDialog
|
||||
v-if="isFaqModalOpen"
|
||||
ref="faqDialog"
|
||||
type="create"
|
||||
:selected-response="{
|
||||
question: plainTextContent,
|
||||
assistant_id: copilotAssistant?.id,
|
||||
}"
|
||||
@close="hideFaqModal"
|
||||
/>
|
||||
<!-- Confirm Deletion -->
|
||||
<woot-delete-modal
|
||||
v-if="showDeleteModal && enabledOptions['delete']"
|
||||
v-if="showDeleteModal"
|
||||
v-model:show="showDeleteModal"
|
||||
class="context-menu--delete-modal"
|
||||
:on-close="closeDeleteModal"
|
||||
@ -280,6 +314,7 @@ export default {
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
|
||||
<NextButton
|
||||
v-if="!hideButton"
|
||||
ghost
|
||||
@ -289,8 +324,9 @@ export default {
|
||||
class="invisible group-hover/context-menu:visible"
|
||||
@click="handleOpen"
|
||||
/>
|
||||
|
||||
<ContextMenu
|
||||
v-if="isOpen && !isCannedResponseModalOpen"
|
||||
v-if="isOpen"
|
||||
:x="contextMenuPosition.x"
|
||||
:y="contextMenuPosition.y"
|
||||
@close="handleClose"
|
||||
@ -337,10 +373,10 @@ export default {
|
||||
v-if="enabledOptions['cannedResponse']"
|
||||
:option="{
|
||||
icon: 'comment-add',
|
||||
label: $t('CONVERSATION.CONTEXT_MENU.CREATE_A_CANNED_RESPONSE'),
|
||||
label: 'Criar FAQ',
|
||||
}"
|
||||
variant="icon"
|
||||
@click.stop="showCannedResponseModal"
|
||||
@click.stop="showFaqModal"
|
||||
/>
|
||||
<MenuItem
|
||||
v-if="enabledOptions['edit']"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user