feat: Implement UI for Agent Bots in settings and remove CSML support (#11276)
- Add agent bots management UI in settings with avatar upload - Enable agent bot configuration for all inbox types - Implement proper CRUD operations with webhook URL support - Fix agent bots menu item visibility in settings sidebar - Remove all CSML-related code and features - Add migration to convert existing CSML bots to webhook bots - Simplify agent bot model and services to focus on webhook bots - Improve UI to differentiate between system bots and account bots ## Video https://github.com/user-attachments/assets/3f4edbb7-b758-468c-8dd6-a9537b983f7d --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
co-authored by
iamsivin
Sivin Varghese
Muhsin Keloth
Pranav
parent
72509f9e38
commit
630826baed
@@ -1,102 +1,191 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
import AgentBotRow from './components/AgentBotRow.vue';
|
||||
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import AgentBotModal from './components/AgentBotModal.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
|
||||
const MODAL_TYPES = {
|
||||
CREATE: 'create',
|
||||
EDIT: 'edit',
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
const agentBots = useMapGetter('agentBots/getBots');
|
||||
const uiFlags = useMapGetter('agentBots/getUIFlags');
|
||||
|
||||
const confirmDialog = ref(null);
|
||||
const selectedBot = ref({});
|
||||
const loading = ref({});
|
||||
const modalType = ref(MODAL_TYPES.CREATE);
|
||||
const agentBotModalRef = ref(null);
|
||||
const agentBotDeleteDialogRef = ref(null);
|
||||
|
||||
const onConfigureNewBot = () => {
|
||||
router.push({
|
||||
name: 'agent_bots_csml_new',
|
||||
});
|
||||
const tableHeaders = computed(() => {
|
||||
return [
|
||||
t('AGENT_BOTS.LIST.TABLE_HEADER.DETAILS'),
|
||||
t('AGENT_BOTS.LIST.TABLE_HEADER.URL'),
|
||||
];
|
||||
});
|
||||
|
||||
const selectedBotName = computed(() => selectedBot.value?.name || '');
|
||||
|
||||
const openAddModal = () => {
|
||||
modalType.value = MODAL_TYPES.CREATE;
|
||||
selectedBot.value = {};
|
||||
agentBotModalRef.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const openEditModal = bot => {
|
||||
modalType.value = MODAL_TYPES.EDIT;
|
||||
selectedBot.value = bot;
|
||||
agentBotModalRef.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const openDeletePopup = bot => {
|
||||
selectedBot.value = bot;
|
||||
agentBotDeleteDialogRef.value.open();
|
||||
};
|
||||
|
||||
const deleteAgentBot = async id => {
|
||||
try {
|
||||
await store.dispatch('agentBots/delete', id);
|
||||
useAlert(t('AGENT_BOTS.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('AGENT_BOTS.DELETE.API.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
loading.value[id] = false;
|
||||
selectedBot.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = () => {
|
||||
loading.value[selectedBot.value.id] = true;
|
||||
deleteAgentBot(selectedBot.value.id);
|
||||
agentBotDeleteDialogRef.value.close();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('agentBots/get');
|
||||
});
|
||||
|
||||
const onDeleteAgentBot = async bot => {
|
||||
const ok = await confirmDialog.value.showConfirmation();
|
||||
|
||||
if (ok) {
|
||||
try {
|
||||
await store.dispatch('agentBots/delete', bot.id);
|
||||
useAlert(t('AGENT_BOTS.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('AGENT_BOTS.DELETE.API.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEditAgentBot = bot => {
|
||||
router.push(
|
||||
frontendURL(
|
||||
`accounts/${accountId.value}/settings/agent-bots/csml/${bot.id}`
|
||||
)
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:loading-message="$t('AGENT_BOTS.LIST.LOADING')"
|
||||
:loading-message="t('AGENT_BOTS.LIST.LOADING')"
|
||||
:no-records-found="!agentBots.length"
|
||||
:no-records-message="$t('AGENT_BOTS.LIST.404')"
|
||||
:no-records-message="t('AGENT_BOTS.LIST.404')"
|
||||
>
|
||||
<template #header>
|
||||
<BaseSettingsHeader
|
||||
:title="$t('AGENT_BOTS.HEADER')"
|
||||
:description="$t('AGENT_BOTS.DESCRIPTION')"
|
||||
:link-text="$t('AGENT_BOTS.LEARN_MORE')"
|
||||
:title="t('AGENT_BOTS.HEADER')"
|
||||
:description="t('AGENT_BOTS.DESCRIPTION')"
|
||||
:link-text="t('AGENT_BOTS.LEARN_MORE')"
|
||||
feature-name="agent_bots"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
icon="i-lucide-circle-plus"
|
||||
:label="$t('AGENT_BOTS.ADD.TITLE')"
|
||||
@click="onConfigureNewBot"
|
||||
@click="openAddModal"
|
||||
/>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<table class="divide-y divide-slate-75 dark:divide-slate-700">
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
||||
<AgentBotRow
|
||||
v-for="(agentBot, index) in agentBots"
|
||||
:key="agentBot.id"
|
||||
:agent-bot="agentBot"
|
||||
:index="index"
|
||||
@delete="onDeleteAgentBot"
|
||||
@edit="onEditAgentBot"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<woot-confirm-modal
|
||||
ref="confirmDialog"
|
||||
:title="$t('AGENT_BOTS.DELETE.TITLE')"
|
||||
:description="$t('AGENT_BOTS.DELETE.DESCRIPTION')"
|
||||
/>
|
||||
</div>
|
||||
<table class="min-w-full overflow-x-auto divide-y divide-n-strong">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in tableHeaders"
|
||||
:key="thHeader"
|
||||
class="py-4 font-semibold text-left ltr:pr-4 rtl:pl-4 text-n-slate-11"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody class="flex-1 divide-y divide-n-weak text-n-slate-12">
|
||||
<tr v-for="bot in agentBots" :key="bot.id">
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<Avatar
|
||||
:name="bot.name"
|
||||
:src="bot.thumbnail"
|
||||
:size="40"
|
||||
rounded-full
|
||||
/>
|
||||
<div>
|
||||
<span class="block font-medium break-words">
|
||||
{{ bot.name }}
|
||||
<span
|
||||
v-if="bot.system_bot"
|
||||
class="text-xs text-n-slate-12 bg-n-blue-5 inline-block rounded-md py-0.5 px-1 ltr:ml-1 rtl:mr-1"
|
||||
>
|
||||
{{ $t('AGENT_BOTS.GLOBAL_BOT_BADGE') }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ bot.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4 text-sm">
|
||||
{{ bot.outgoing_url || bot.bot_config?.webhook_url }}
|
||||
</td>
|
||||
<td class="py-4 min-w-xs">
|
||||
<div class="flex gap-1 justify-end">
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('AGENT_BOTS.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openEditModal(bot)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('AGENT_BOTS.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openDeletePopup(bot)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<AgentBotModal
|
||||
ref="agentBotModalRef"
|
||||
:type="modalType"
|
||||
:selected-bot="selectedBot"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
ref="agentBotDeleteDialogRef"
|
||||
type="alert"
|
||||
:title="t('AGENT_BOTS.DELETE.CONFIRM.TITLE')"
|
||||
:description="
|
||||
t('AGENT_BOTS.DELETE.CONFIRM.MESSAGE', { name: selectedBotName })
|
||||
"
|
||||
:is-loading="uiFlags.isDeleting"
|
||||
:confirm-button-label="t('AGENT_BOTS.DELETE.CONFIRM.YES')"
|
||||
:cancel-button-label="t('AGENT_BOTS.DELETE.CONFIRM.NO')"
|
||||
@confirm="confirmDeletion"
|
||||
/>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import Bot from './Index.vue';
|
||||
import CsmlEditBot from './csml/Edit.vue';
|
||||
import CsmlNewBot from './csml/New.vue';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import SettingsContent from '../Wrapper.vue';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
|
||||
export default {
|
||||
@@ -26,36 +23,5 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/agent-bots'),
|
||||
component: SettingsContent,
|
||||
props: () => {
|
||||
return {
|
||||
headerTitle: 'AGENT_BOTS.HEADER',
|
||||
icon: 'bot',
|
||||
showBackButton: true,
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'csml/new',
|
||||
name: 'agent_bots_csml_new',
|
||||
component: CsmlNewBot,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'csml/:botId',
|
||||
name: 'agent_bots_csml_edit',
|
||||
component: CsmlEditBot,
|
||||
meta: {
|
||||
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
<script setup>
|
||||
import { ref, computed, reactive, watch } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { required, helpers, url } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
validator: value => ['create', 'edit'].includes(value),
|
||||
},
|
||||
selectedBot: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const MODAL_TYPES = {
|
||||
CREATE: 'create',
|
||||
EDIT: 'edit',
|
||||
};
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const dialogRef = ref(null);
|
||||
const uiFlags = useMapGetter('agentBots/getUIFlags');
|
||||
|
||||
const formState = reactive({
|
||||
botName: '',
|
||||
botDescription: '',
|
||||
botUrl: '',
|
||||
botAvatar: null,
|
||||
botAvatarUrl: '',
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
botName: {
|
||||
required: helpers.withMessage(
|
||||
() => t('AGENT_BOTS.FORM.ERRORS.NAME'),
|
||||
required
|
||||
),
|
||||
},
|
||||
botUrl: {
|
||||
required: helpers.withMessage(
|
||||
() => t('AGENT_BOTS.FORM.ERRORS.URL'),
|
||||
required
|
||||
),
|
||||
url: helpers.withMessage(
|
||||
() => t('AGENT_BOTS.FORM.ERRORS.VALID_URL'),
|
||||
url
|
||||
),
|
||||
},
|
||||
},
|
||||
formState
|
||||
);
|
||||
|
||||
const isLoading = computed(() =>
|
||||
props.type === MODAL_TYPES.CREATE
|
||||
? uiFlags.value.isCreating
|
||||
: uiFlags.value.isUpdating
|
||||
);
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
props.type === MODAL_TYPES.CREATE
|
||||
? t('AGENT_BOTS.ADD.TITLE')
|
||||
: t('AGENT_BOTS.EDIT.TITLE')
|
||||
);
|
||||
|
||||
const confirmButtonLabel = computed(() =>
|
||||
props.type === MODAL_TYPES.CREATE
|
||||
? t('AGENT_BOTS.FORM.CREATE')
|
||||
: t('AGENT_BOTS.FORM.UPDATE')
|
||||
);
|
||||
|
||||
const botNameError = computed(() =>
|
||||
v$.value.botName.$error ? v$.value.botName.$errors[0]?.$message : ''
|
||||
);
|
||||
|
||||
const botUrlError = computed(() =>
|
||||
v$.value.botUrl.$error ? v$.value.botUrl.$errors[0]?.$message : ''
|
||||
);
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formState, {
|
||||
botName: '',
|
||||
botDescription: '',
|
||||
botUrl: '',
|
||||
botAvatar: null,
|
||||
botAvatarUrl: '',
|
||||
});
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const handleImageUpload = ({ file, url: avatarUrl }) => {
|
||||
formState.botAvatar = file;
|
||||
formState.botAvatarUrl = avatarUrl;
|
||||
};
|
||||
|
||||
const handleAvatarDelete = async () => {
|
||||
if (props.selectedBot?.id) {
|
||||
try {
|
||||
await store.dispatch(
|
||||
'agentBots/deleteAgentBotAvatar',
|
||||
props.selectedBot.id
|
||||
);
|
||||
formState.botAvatar = null;
|
||||
formState.botAvatarUrl = '';
|
||||
useAlert(t('AGENT_BOTS.AVATAR.SUCCESS_DELETE'));
|
||||
} catch (error) {
|
||||
useAlert(t('AGENT_BOTS.AVATAR.ERROR_DELETE'));
|
||||
}
|
||||
} else {
|
||||
formState.botAvatar = null;
|
||||
formState.botAvatarUrl = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
const botData = {
|
||||
name: formState.botName,
|
||||
description: formState.botDescription,
|
||||
outgoing_url: formState.botUrl,
|
||||
bot_type: 'webhook',
|
||||
avatar: formState.botAvatar,
|
||||
};
|
||||
|
||||
const isCreate = props.type === MODAL_TYPES.CREATE;
|
||||
|
||||
try {
|
||||
const actionPayload = isCreate
|
||||
? botData
|
||||
: { id: props.selectedBot.id, data: botData };
|
||||
|
||||
await store.dispatch(
|
||||
`agentBots/${isCreate ? 'create' : 'update'}`,
|
||||
actionPayload
|
||||
);
|
||||
|
||||
const alertKey = isCreate
|
||||
? t('AGENT_BOTS.ADD.API.SUCCESS_MESSAGE')
|
||||
: t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE');
|
||||
useAlert(alertKey);
|
||||
|
||||
dialogRef.value.close();
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
const errorKey = isCreate
|
||||
? t('AGENT_BOTS.ADD.API.ERROR_MESSAGE')
|
||||
: t('AGENT_BOTS.EDIT.API.ERROR_MESSAGE');
|
||||
useAlert(errorKey);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeForm = () => {
|
||||
if (props.selectedBot && Object.keys(props.selectedBot).length) {
|
||||
const { name, description, outgoing_url, thumbnail, bot_config } =
|
||||
props.selectedBot;
|
||||
formState.botName = name || '';
|
||||
formState.botDescription = description || '';
|
||||
formState.botUrl = outgoing_url || bot_config?.webhook_url || '';
|
||||
formState.botAvatarUrl = thumbnail || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.selectedBot, initializeForm, { immediate: true, deep: true });
|
||||
|
||||
defineExpose({ dialogRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
:title="dialogTitle"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
@close="v$.$reset()"
|
||||
>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<div class="mb-2 flex flex-col items-start">
|
||||
<span class="mb-2 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
|
||||
</span>
|
||||
<Avatar
|
||||
:src="formState.botAvatarUrl"
|
||||
:name="formState.botName"
|
||||
:size="68"
|
||||
allow-upload
|
||||
@upload="handleImageUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="formState.botName"
|
||||
:label="$t('AGENT_BOTS.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.NAME.PLACEHOLDER')"
|
||||
:message="botNameError"
|
||||
:message-type="botNameError ? 'error' : 'info'"
|
||||
@blur="v$.botName.$touch()"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="formState.botDescription"
|
||||
:label="$t('AGENT_BOTS.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
/>
|
||||
|
||||
<Input
|
||||
v-model="formState.botUrl"
|
||||
:label="$t('AGENT_BOTS.FORM.WEBHOOK_URL.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.WEBHOOK_URL.PLACEHOLDER')"
|
||||
:message="botUrlError"
|
||||
:message-type="botUrlError ? 'error' : 'info'"
|
||||
@blur="v$.botUrl.$touch()"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('AGENT_BOTS.FORM.CANCEL')"
|
||||
@click="dialogRef.close()"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
data-testid="label-submit"
|
||||
:label="confirmButtonLabel"
|
||||
:is-loading="isLoading"
|
||||
:disabled="v$.$invalid"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import AgentBotType from './AgentBotType.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
agentBot: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'delete']);
|
||||
|
||||
const isACSMLTypeBot = computed(() => {
|
||||
const { bot_type: botType } = props.agentBot;
|
||||
return botType === 'csml';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr class="space-x-2">
|
||||
<td class="py-4 ltr:pl-0 ltr:pr-4 rtl:pl-4 rtl:pr-0">
|
||||
<div class="flex items-center break-words font-medium">
|
||||
{{ agentBot.name }}
|
||||
(<AgentBotType :bot-type="agentBot.bot_type" />)
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<ShowMore :text="agentBot.description || ''" :limit="120" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="flex justify-end gap-1 h-full items-center">
|
||||
<Button
|
||||
v-if="isACSMLTypeBot"
|
||||
v-tooltip.top="$t('AGENT_BOTS.EDIT.BUTTON_TEXT')"
|
||||
icon="i-lucide-pen"
|
||||
slate
|
||||
xs
|
||||
faded
|
||||
@click="emit('edit', agentBot)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="$t('AGENT_BOTS.DELETE.BUTTON_TEXT')"
|
||||
icon="i-lucide-trash-2"
|
||||
xs
|
||||
ruby
|
||||
faded
|
||||
@click="emit('delete', agentBot, index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
botType: {
|
||||
type: String,
|
||||
default: 'webhook',
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const botTypeConfig = computed(() => ({
|
||||
csml: {
|
||||
label: t('AGENT_BOTS.TYPES.CSML'),
|
||||
thumbnail: '/dashboard/images/agent-bots/csml.png',
|
||||
},
|
||||
webhook: {
|
||||
label: t('AGENT_BOTS.TYPES.WEBHOOK'),
|
||||
thumbnail: '/dashboard/images/agent-bots/webhook.svg',
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<img
|
||||
v-tooltip="botTypeConfig[botType].label"
|
||||
class="h-3 w-auto"
|
||||
:src="botTypeConfig[botType].thumbnail"
|
||||
:alt="botTypeConfig[botType].label"
|
||||
/>
|
||||
<span>{{ botTypeConfig[botType].label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import CsmlMonacoEditor from './CSMLMonacoEditor.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: { CsmlMonacoEditor, NextButton },
|
||||
props: {
|
||||
agentBot: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['submit'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
validations: {
|
||||
bot: {
|
||||
name: { required },
|
||||
csmlContent: { required },
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bot: {
|
||||
name: this.agentBot.name || '',
|
||||
description: this.agentBot.description || '',
|
||||
csmlContent: this.agentBot.bot_config.csml_content || '',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.$emit('submit', {
|
||||
id: this.agentBot.id || '',
|
||||
...this.bot,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div class="flex flex-row">
|
||||
<div class="w-[68%]">
|
||||
<div class="h-[calc(100vh-56px)] relative">
|
||||
<CsmlMonacoEditor v-model="bot.csmlContent" class="w-full h-full" />
|
||||
<div
|
||||
v-if="v$.bot.csmlContent.$error"
|
||||
class="bg-red-100 dark:bg-red-200 text-white dark:text-white absolute bottom-0 w-full p-2.5 flex items-center text-xs justify-center flex-shrink-0"
|
||||
>
|
||||
<span>{{ $t('AGENT_BOTS.CSML_BOT_EDITOR.BOT_CONFIG.ERROR') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[32%] overflow-auto p-4 h-[calc(100vh-56px)]">
|
||||
<form
|
||||
class="flex flex-col justify-between h-full"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<div>
|
||||
<label :class="{ error: v$.bot.name.$error }">
|
||||
{{ $t('AGENT_BOTS.CSML_BOT_EDITOR.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="bot.name"
|
||||
type="text"
|
||||
:placeholder="$t('AGENT_BOTS.CSML_BOT_EDITOR.NAME.PLACEHOLDER')"
|
||||
/>
|
||||
<span v-if="v$.bot.name.$error" class="message">
|
||||
{{ $t('AGENT_BOTS.CSML_BOT_EDITOR.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('AGENT_BOTS.CSML_BOT_EDITOR.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="bot.description"
|
||||
rows="4"
|
||||
:placeholder="
|
||||
$t('AGENT_BOTS.CSML_BOT_EDITOR.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('AGENT_BOTS.CSML_BOT_EDITOR.SUBMIT')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
<script>
|
||||
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: { LoadingState },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
iframeLoading: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
window.onmessage = e => {
|
||||
if (
|
||||
typeof e.data !== 'string' ||
|
||||
!e.data.startsWith('chatwoot-csml-editor:update')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const csmlContent = e.data.replace('chatwoot-csml-editor:update', '');
|
||||
this.$emit('update:modelValue', csmlContent);
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onEditorLoad() {
|
||||
const frameElement = document.getElementById(`csml-editor--frame`);
|
||||
const eventData = {
|
||||
event: 'editorContext',
|
||||
data: this.modelValue || '',
|
||||
};
|
||||
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||
this.iframeLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="csml-editor--container">
|
||||
<LoadingState
|
||||
v-if="iframeLoading"
|
||||
:message="$t('AGENT_BOTS.LOADING_EDITOR')"
|
||||
class="dashboard-app_loading-container"
|
||||
/>
|
||||
<iframe
|
||||
id="csml-editor--frame"
|
||||
:src="globalConfig.csmlEditorHost"
|
||||
@load="onEditorLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#csml-editor--frame {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import CsmlBotEditor from '../components/CSMLBotEditor.vue';
|
||||
|
||||
export default {
|
||||
components: { Spinner, CsmlBotEditor },
|
||||
computed: {
|
||||
agentBot() {
|
||||
return this.$store.getters['agentBots/getBot'](this.$route.params.botId);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('agentBots/show', this.$route.params.botId);
|
||||
},
|
||||
methods: {
|
||||
async updateBot(bot) {
|
||||
try {
|
||||
await this.$store.dispatch('agentBots/update', {
|
||||
id: bot.id,
|
||||
name: bot.name,
|
||||
description: bot.description,
|
||||
bot_type: 'csml',
|
||||
bot_config: { csml_content: bot.csmlContent },
|
||||
});
|
||||
useAlert(this.$t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('AGENT_BOTS.CSML_BOT_EDITOR.BOT_CONFIG.API_ERROR'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CsmlBotEditor v-if="agentBot.id" :agent-bot="agentBot" @submit="updateBot" />
|
||||
<div v-else class="flex flex-col h-auto overflow-auto no-padding">
|
||||
<Spinner />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { frontendURL } from '../../../../../helper/URLHelper';
|
||||
import CsmlBotEditor from '../components/CSMLBotEditor.vue';
|
||||
|
||||
export default {
|
||||
components: { CsmlBotEditor },
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async saveBot(bot) {
|
||||
try {
|
||||
const agentBot = await this.$store.dispatch('agentBots/create', {
|
||||
name: bot.name,
|
||||
description: bot.description,
|
||||
bot_type: 'csml',
|
||||
bot_config: { csml_content: bot.csmlContent },
|
||||
});
|
||||
if (agentBot) {
|
||||
this.$router.replace(
|
||||
frontendURL(
|
||||
`accounts/${this.accountId}/settings/agent-bots/csml/${agentBot.id}`
|
||||
)
|
||||
);
|
||||
}
|
||||
useAlert(this.$t('AGENT_BOTS.ADD.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('AGENT_BOTS.ADD.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CsmlBotEditor :agent-bot="{ bot_config: {} }" @submit="saveBot" />
|
||||
</template>
|
||||
@@ -141,11 +141,7 @@ export default {
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.AGENT_BOTS
|
||||
) &&
|
||||
!(this.isAnEmailChannel || this.isATwitterInbox)
|
||||
this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.AGENT_BOTS)
|
||||
) {
|
||||
visibleToAllChannelTabs = [
|
||||
...visibleToAllChannelTabs,
|
||||
|
||||
Reference in New Issue
Block a user