mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-31 19:17:48 +00:00
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:
@@ -37,7 +37,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content])
|
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_avatar_from_url
|
def process_avatar_from_url
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DashboardController < ActionController::Base
|
|||||||
'LOGOUT_REDIRECT_LINK',
|
'LOGOUT_REDIRECT_LINK',
|
||||||
'DISABLE_USER_PROFILE_UPDATE',
|
'DISABLE_USER_PROFILE_UPDATE',
|
||||||
'DEPLOYMENT_ENV',
|
'DEPLOYMENT_ENV',
|
||||||
'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN'
|
'INSTALLATION_PRICING_PLAN'
|
||||||
).merge(app_config)
|
).merge(app_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
|
/* global axios */
|
||||||
import ApiClient from './ApiClient';
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class AgentBotsAPI extends ApiClient {
|
class AgentBotsAPI extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('agent_bots', { accountScoped: true });
|
super('agent_bots', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create(data) {
|
||||||
|
return axios.post(this.url, data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id, data) {
|
||||||
|
return axios.patch(`${this.url}/${id}`, data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAgentBotAvatar(botId) {
|
||||||
|
return axios.delete(`${this.url}/${botId}/avatar`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AgentBotsAPI();
|
export default new AgentBotsAPI();
|
||||||
|
|||||||
@@ -125,7 +125,10 @@ defineExpose({ open, close });
|
|||||||
<slot />
|
<slot />
|
||||||
<!-- Dialog content will be injected here -->
|
<!-- Dialog content will be injected here -->
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
<div class="flex items-center justify-between w-full gap-3">
|
<div
|
||||||
|
v-if="showCancelButton || showConfirmButton"
|
||||||
|
class="flex items-center justify-between w-full gap-3"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-if="showCancelButton"
|
v-if="showCancelButton"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ const settings = accountId => ({
|
|||||||
meta: {
|
meta: {
|
||||||
permissions: ['administrator'],
|
permissions: ['administrator'],
|
||||||
},
|
},
|
||||||
globalConfigFlag: 'csmlEditorHost',
|
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||||
toStateName: 'agent_bots',
|
toStateName: 'agent_bots',
|
||||||
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||||
|
|||||||
@@ -49,13 +49,6 @@ export default {
|
|||||||
return !!this.menuItem.children;
|
return !!this.menuItem.children;
|
||||||
},
|
},
|
||||||
isMenuItemVisible() {
|
isMenuItemVisible() {
|
||||||
if (this.menuItem.globalConfigFlag) {
|
|
||||||
// this checks for the `csmlEditorHost` flag in the global config
|
|
||||||
// if this is present, we toggle the CSML editor menu item
|
|
||||||
// TODO: This is very specific, and can be handled better, fix it
|
|
||||||
return !!this.globalConfig[this.menuItem.globalConfigFlag];
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFeatureEnabled = true;
|
let isFeatureEnabled = true;
|
||||||
if (this.menuItem.featureFlag) {
|
if (this.menuItem.featureFlag) {
|
||||||
isFeatureEnabled = this.isFeatureEnabledonAccount(
|
isFeatureEnabled = this.isFeatureEnabledonAccount(
|
||||||
|
|||||||
@@ -2,23 +2,13 @@
|
|||||||
"AGENT_BOTS": {
|
"AGENT_BOTS": {
|
||||||
"HEADER": "Bots",
|
"HEADER": "Bots",
|
||||||
"LOADING_EDITOR": "Loading editor...",
|
"LOADING_EDITOR": "Loading editor...",
|
||||||
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.",
|
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
|
||||||
"LEARN_MORE": "Learn about agent bots",
|
"LEARN_MORE": "Learn about agent bots",
|
||||||
"CSML_BOT_EDITOR": {
|
"GLOBAL_BOT": "System bot",
|
||||||
"NAME": {
|
"GLOBAL_BOT_BADGE": "System",
|
||||||
"LABEL": "Bot name",
|
"AVATAR": {
|
||||||
"PLACEHOLDER": "Name your bot.",
|
"SUCCESS_DELETE": "Bot avatar deleted successfully",
|
||||||
"ERROR": "Bot name is required."
|
"ERROR_DELETE": "Error deleting bot avatar, please try again"
|
||||||
},
|
|
||||||
"DESCRIPTION": {
|
|
||||||
"LABEL": "Bot description",
|
|
||||||
"PLACEHOLDER": "What does this bot do?"
|
|
||||||
},
|
|
||||||
"BOT_CONFIG": {
|
|
||||||
"ERROR": "Please enter your CSML bot configuration above.",
|
|
||||||
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
|
|
||||||
},
|
|
||||||
"SUBMIT": "Validate and save"
|
|
||||||
},
|
},
|
||||||
"BOT_CONFIGURATION": {
|
"BOT_CONFIGURATION": {
|
||||||
"TITLE": "Select an agent bot",
|
"TITLE": "Select an agent bot",
|
||||||
@@ -32,7 +22,7 @@
|
|||||||
"SELECT_PLACEHOLDER": "Select bot"
|
"SELECT_PLACEHOLDER": "Select bot"
|
||||||
},
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Configure new bot",
|
"TITLE": "Add Bot",
|
||||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Bot added successfully.",
|
"SUCCESS_MESSAGE": "Bot added successfully.",
|
||||||
@@ -40,16 +30,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
|
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
|
||||||
"LOADING": "Fetching bots...",
|
"LOADING": "Fetching bots...",
|
||||||
"TYPE": "Bot type"
|
"TABLE_HEADER": {
|
||||||
|
"DETAILS": "Bot Details",
|
||||||
|
"URL": "Webhook URL"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"BUTTON_TEXT": "Delete",
|
"BUTTON_TEXT": "Delete",
|
||||||
"TITLE": "Delete bot",
|
"TITLE": "Delete bot",
|
||||||
"SUBMIT": "Delete",
|
"CONFIRM": {
|
||||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
"TITLE": "Confirm Deletion",
|
||||||
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
|
"MESSAGE": "Are you sure you want to delete {name}?",
|
||||||
|
"YES": "Yes, Delete",
|
||||||
|
"NO": "No, Keep"
|
||||||
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Bot deleted successfully.",
|
"SUCCESS_MESSAGE": "Bot deleted successfully.",
|
||||||
"ERROR_MESSAGE": "Could not delete bot. Please try again."
|
"ERROR_MESSAGE": "Could not delete bot. Please try again."
|
||||||
@@ -57,17 +53,44 @@
|
|||||||
},
|
},
|
||||||
"EDIT": {
|
"EDIT": {
|
||||||
"BUTTON_TEXT": "Edit",
|
"BUTTON_TEXT": "Edit",
|
||||||
"LOADING": "Fetching bots...",
|
|
||||||
"TITLE": "Edit bot",
|
"TITLE": "Edit bot",
|
||||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Bot updated successfully.",
|
"SUCCESS_MESSAGE": "Bot updated successfully.",
|
||||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FORM": {
|
||||||
|
"AVATAR": {
|
||||||
|
"LABEL": "Bot avatar"
|
||||||
|
},
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Bot name",
|
||||||
|
"PLACEHOLDER": "Enter bot name",
|
||||||
|
"REQUIRED": "Bot name is required"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Description",
|
||||||
|
"PLACEHOLDER": "What does this bot do?"
|
||||||
|
},
|
||||||
|
"WEBHOOK_URL": {
|
||||||
|
"LABEL": "Webhook URL",
|
||||||
|
"PLACEHOLDER": "https://example.com/webhook",
|
||||||
|
"REQUIRED": "Webhook URL is required"
|
||||||
|
},
|
||||||
|
"ERRORS": {
|
||||||
|
"NAME": "Bot name is required",
|
||||||
|
"URL": "Webhook URL is required",
|
||||||
|
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
|
||||||
|
},
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"CREATE": "Create Bot",
|
||||||
|
"UPDATE": "Update Bot"
|
||||||
|
},
|
||||||
|
"WEBHOOK": {
|
||||||
|
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
|
||||||
|
},
|
||||||
"TYPES": {
|
"TYPES": {
|
||||||
"WEBHOOK": "Webhook bot",
|
"WEBHOOK": "Webhook bot"
|
||||||
"CSML": "CSML bot"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,191 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
|
||||||
|
|
||||||
import AgentBotRow from './components/AgentBotRow.vue';
|
|
||||||
|
|
||||||
import SettingsLayout from '../SettingsLayout.vue';
|
import SettingsLayout from '../SettingsLayout.vue';
|
||||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.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 store = useStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const accountId = useMapGetter('getCurrentAccountId');
|
|
||||||
const agentBots = useMapGetter('agentBots/getBots');
|
const agentBots = useMapGetter('agentBots/getBots');
|
||||||
const uiFlags = useMapGetter('agentBots/getUIFlags');
|
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 = () => {
|
const tableHeaders = computed(() => {
|
||||||
router.push({
|
return [
|
||||||
name: 'agent_bots_csml_new',
|
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(() => {
|
onMounted(() => {
|
||||||
store.dispatch('agentBots/get');
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SettingsLayout
|
<SettingsLayout
|
||||||
:is-loading="uiFlags.isFetching"
|
: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-found="!agentBots.length"
|
||||||
:no-records-message="$t('AGENT_BOTS.LIST.404')"
|
:no-records-message="t('AGENT_BOTS.LIST.404')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<BaseSettingsHeader
|
<BaseSettingsHeader
|
||||||
:title="$t('AGENT_BOTS.HEADER')"
|
:title="t('AGENT_BOTS.HEADER')"
|
||||||
:description="$t('AGENT_BOTS.DESCRIPTION')"
|
:description="t('AGENT_BOTS.DESCRIPTION')"
|
||||||
:link-text="$t('AGENT_BOTS.LEARN_MORE')"
|
:link-text="t('AGENT_BOTS.LEARN_MORE')"
|
||||||
feature-name="agent_bots"
|
feature-name="agent_bots"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<Button
|
<Button
|
||||||
icon="i-lucide-circle-plus"
|
icon="i-lucide-circle-plus"
|
||||||
:label="$t('AGENT_BOTS.ADD.TITLE')"
|
:label="$t('AGENT_BOTS.ADD.TITLE')"
|
||||||
@click="onConfigureNewBot"
|
@click="openAddModal"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</BaseSettingsHeader>
|
</BaseSettingsHeader>
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex-1 overflow-auto">
|
<table class="min-w-full overflow-x-auto divide-y divide-n-strong">
|
||||||
<table class="divide-y divide-slate-75 dark:divide-slate-700">
|
<thead>
|
||||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
<th
|
||||||
<AgentBotRow
|
v-for="thHeader in tableHeaders"
|
||||||
v-for="(agentBot, index) in agentBots"
|
:key="thHeader"
|
||||||
:key="agentBot.id"
|
class="py-4 font-semibold text-left ltr:pr-4 rtl:pl-4 text-n-slate-11"
|
||||||
:agent-bot="agentBot"
|
>
|
||||||
:index="index"
|
{{ thHeader }}
|
||||||
@delete="onDeleteAgentBot"
|
</th>
|
||||||
@edit="onEditAgentBot"
|
</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
|
||||||
/>
|
/>
|
||||||
</tbody>
|
<div>
|
||||||
</table>
|
<span class="block font-medium break-words">
|
||||||
<woot-confirm-modal
|
{{ bot.name }}
|
||||||
ref="confirmDialog"
|
<span
|
||||||
:title="$t('AGENT_BOTS.DELETE.TITLE')"
|
v-if="bot.system_bot"
|
||||||
:description="$t('AGENT_BOTS.DELETE.DESCRIPTION')"
|
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>
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</template>
|
</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>
|
</SettingsLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||||
import Bot from './Index.vue';
|
import Bot from './Index.vue';
|
||||||
import CsmlEditBot from './csml/Edit.vue';
|
|
||||||
import CsmlNewBot from './csml/New.vue';
|
|
||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
import SettingsContent from '../Wrapper.vue';
|
|
||||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||||
|
|
||||||
export default {
|
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'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 (
|
if (
|
||||||
this.isFeatureEnabledonAccount(
|
this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.AGENT_BOTS)
|
||||||
this.accountId,
|
|
||||||
FEATURE_FLAGS.AGENT_BOTS
|
|
||||||
) &&
|
|
||||||
!(this.isAnEmailChannel || this.isATwitterInbox)
|
|
||||||
) {
|
) {
|
||||||
visibleToAllChannelTabs = [
|
visibleToAllChannelTabs = [
|
||||||
...visibleToAllChannelTabs,
|
...visibleToAllChannelTabs,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const state = {
|
|||||||
isCreating: false,
|
isCreating: false,
|
||||||
isDeleting: false,
|
isDeleting: false,
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
|
isUpdatingAvatar: false,
|
||||||
isFetchingAgentBot: false,
|
isFetchingAgentBot: false,
|
||||||
isSettingAgentBot: false,
|
isSettingAgentBot: false,
|
||||||
isDisconnecting: false,
|
isDisconnecting: false,
|
||||||
@@ -48,10 +49,23 @@ export const actions = {
|
|||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetching: false });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetching: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
create: async ({ commit }, agentBotObj) => {
|
|
||||||
|
create: async ({ commit }, botData) => {
|
||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isCreating: true });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isCreating: true });
|
||||||
try {
|
try {
|
||||||
const response = await AgentBotsAPI.create(agentBotObj);
|
// Create FormData for file upload
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', botData.name);
|
||||||
|
formData.append('description', botData.description);
|
||||||
|
formData.append('bot_type', botData.bot_type || 'webhook');
|
||||||
|
formData.append('outgoing_url', botData.outgoing_url);
|
||||||
|
|
||||||
|
// Add avatar file if available
|
||||||
|
if (botData.avatar) {
|
||||||
|
formData.append('avatar', botData.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await AgentBotsAPI.create(formData);
|
||||||
commit(types.ADD_AGENT_BOT, response.data);
|
commit(types.ADD_AGENT_BOT, response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -61,10 +75,22 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
update: async ({ commit }, { id, ...agentBotObj }) => {
|
|
||||||
|
update: async ({ commit }, { id, data }) => {
|
||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true });
|
||||||
try {
|
try {
|
||||||
const response = await AgentBotsAPI.update(id, agentBotObj);
|
// Create FormData for file upload
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', data.name);
|
||||||
|
formData.append('description', data.description);
|
||||||
|
formData.append('bot_type', data.bot_type || 'webhook');
|
||||||
|
formData.append('outgoing_url', data.outgoing_url);
|
||||||
|
|
||||||
|
if (data.avatar) {
|
||||||
|
formData.append('avatar', data.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await AgentBotsAPI.update(id, formData);
|
||||||
commit(types.EDIT_AGENT_BOT, response.data);
|
commit(types.EDIT_AGENT_BOT, response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throwErrorMessage(error);
|
throwErrorMessage(error);
|
||||||
@@ -72,6 +98,7 @@ export const actions = {
|
|||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async ({ commit }, id) => {
|
delete: async ({ commit }, id) => {
|
||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: true });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: true });
|
||||||
try {
|
try {
|
||||||
@@ -83,6 +110,20 @@ export const actions = {
|
|||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: false });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteAgentBotAvatar: async ({ commit }, id) => {
|
||||||
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdatingAvatar: true });
|
||||||
|
try {
|
||||||
|
await AgentBotsAPI.deleteAgentBotAvatar(id);
|
||||||
|
// Update the thumbnail to empty string after deletion
|
||||||
|
commit(types.UPDATE_AGENT_BOT_AVATAR, { id, thumbnail: '' });
|
||||||
|
} catch (error) {
|
||||||
|
throwErrorMessage(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdatingAvatar: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
show: async ({ commit }, id) => {
|
show: async ({ commit }, id) => {
|
||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetchingItem: true });
|
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetchingItem: true });
|
||||||
try {
|
try {
|
||||||
@@ -150,6 +191,12 @@ export const mutations = {
|
|||||||
[inboxId]: agentBotId,
|
[inboxId]: agentBotId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[types.UPDATE_AGENT_BOT_AVATAR]($state, { id, thumbnail }) {
|
||||||
|
const botIndex = $state.records.findIndex(bot => bot.id === id);
|
||||||
|
if (botIndex !== -1) {
|
||||||
|
$state.records[botIndex].thumbnail = thumbnail || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { actions } from '../../agentBots';
|
import { actions } from '../../agentBots';
|
||||||
import types from '../../../mutation-types';
|
import types from '../../../mutation-types';
|
||||||
import { agentBotRecords } from './fixtures';
|
import { agentBotRecords, agentBotData } from './fixtures';
|
||||||
|
|
||||||
const commit = vi.fn();
|
const commit = vi.fn();
|
||||||
global.axios = axios;
|
global.axios = axios;
|
||||||
@@ -30,16 +30,22 @@ describe('#actions', () => {
|
|||||||
describe('#create', () => {
|
describe('#create', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.post.mockResolvedValue({ data: agentBotRecords[0] });
|
axios.post.mockResolvedValue({ data: agentBotRecords[0] });
|
||||||
await actions.create({ commit }, agentBotRecords[0]);
|
await actions.create({ commit }, agentBotData);
|
||||||
|
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }],
|
||||||
[types.ADD_AGENT_BOT, agentBotRecords[0]],
|
[types.ADD_AGENT_BOT, agentBotRecords[0]],
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
expect(axios.post.mock.calls.length).toBe(1);
|
||||||
|
const formDataArg = axios.post.mock.calls[0][1];
|
||||||
|
expect(formDataArg instanceof FormData).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
await expect(actions.create({ commit })).rejects.toThrow(Error);
|
await expect(actions.create({ commit }, {})).rejects.toThrow(Error);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }],
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }],
|
||||||
@@ -50,17 +56,29 @@ describe('#actions', () => {
|
|||||||
describe('#update', () => {
|
describe('#update', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.patch.mockResolvedValue({ data: agentBotRecords[0] });
|
axios.patch.mockResolvedValue({ data: agentBotRecords[0] });
|
||||||
await actions.update({ commit }, agentBotRecords[0]);
|
await actions.update(
|
||||||
|
{ commit },
|
||||||
|
{
|
||||||
|
id: agentBotRecords[0].id,
|
||||||
|
data: agentBotData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }],
|
||||||
[types.EDIT_AGENT_BOT, agentBotRecords[0]],
|
[types.EDIT_AGENT_BOT, agentBotRecords[0]],
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false }],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
expect(axios.patch.mock.calls.length).toBe(1);
|
||||||
|
const formDataArg = axios.patch.mock.calls[0][1];
|
||||||
|
expect(formDataArg instanceof FormData).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
await expect(
|
await expect(
|
||||||
actions.update({ commit }, agentBotRecords[0])
|
actions.update({ commit }, { id: 1, data: {} })
|
||||||
).rejects.toThrow(Error);
|
).rejects.toThrow(Error);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }],
|
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }],
|
||||||
@@ -68,7 +86,6 @@ describe('#actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#delete', () => {
|
describe('#delete', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.delete.mockResolvedValue({ data: agentBotRecords[0] });
|
axios.delete.mockResolvedValue({ data: agentBotRecords[0] });
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
export const agentBotRecords = [
|
export const agentBotRecords = [
|
||||||
{
|
{
|
||||||
|
account_id: 1,
|
||||||
id: 11,
|
id: 11,
|
||||||
name: 'Agent Bot 11',
|
name: 'Agent Bot 11',
|
||||||
description: 'Agent Bot Description',
|
description: 'Agent Bot Description',
|
||||||
type: 'csml',
|
bot_type: 'webhook',
|
||||||
|
thumbnail: 'https://example.com/thumbnail.jpg',
|
||||||
|
bot_config: {},
|
||||||
|
outgoing_url: 'https://example.com/outgoing',
|
||||||
|
access_token: 'hN8QwG769RqBXmme',
|
||||||
|
system_bot: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
account_id: 1,
|
||||||
id: 12,
|
id: 12,
|
||||||
name: 'Agent Bot 12',
|
name: 'Agent Bot 12',
|
||||||
description: 'Agent Bot Description 12',
|
description: 'Agent Bot Description 12',
|
||||||
type: 'csml',
|
bot_type: 'webhook',
|
||||||
|
thumbnail: 'https://example.com/thumbnail.jpg',
|
||||||
|
bot_config: {},
|
||||||
|
outgoing_url: 'https://example.com/outgoing',
|
||||||
|
access_token: 'hN8QwG769RqBXmme',
|
||||||
|
system_bot: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const agentBotData = {
|
||||||
|
name: 'Test Bot',
|
||||||
|
description: 'Test Description',
|
||||||
|
outgoing_url: 'https://test.com',
|
||||||
|
bot_type: 'webhook',
|
||||||
|
avatar: new File([''], 'filename'),
|
||||||
|
};
|
||||||
|
|||||||
@@ -51,4 +51,16 @@ describe('#mutations', () => {
|
|||||||
expect(state.agentBotInbox).toEqual({ 3: 2 });
|
expect(state.agentBotInbox).toEqual({ 3: 2 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('#UPDATE_AGENT_BOT_AVATAR', () => {
|
||||||
|
it('update agent bot avatar', () => {
|
||||||
|
const state = { records: [agentBotRecords[0]] };
|
||||||
|
mutations[types.UPDATE_AGENT_BOT_AVATAR](state, {
|
||||||
|
id: 11,
|
||||||
|
thumbnail: 'https://example.com/thumbnail.jpg',
|
||||||
|
});
|
||||||
|
expect(state.records[0].thumbnail).toEqual(
|
||||||
|
'https://example.com/thumbnail.jpg'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ export default {
|
|||||||
EDIT_AGENT_BOT: 'EDIT_AGENT_BOT',
|
EDIT_AGENT_BOT: 'EDIT_AGENT_BOT',
|
||||||
DELETE_AGENT_BOT: 'DELETE_AGENT_BOT',
|
DELETE_AGENT_BOT: 'DELETE_AGENT_BOT',
|
||||||
SET_AGENT_BOT_INBOX: 'SET_AGENT_BOT_INBOX',
|
SET_AGENT_BOT_INBOX: 'SET_AGENT_BOT_INBOX',
|
||||||
|
UPDATE_AGENT_BOT_AVATAR: 'UPDATE_AGENT_BOT_AVATAR',
|
||||||
|
|
||||||
// MACROS
|
// MACROS
|
||||||
SET_MACROS_UI_FLAG: 'SET_MACROS_UI_FLAG',
|
SET_MACROS_UI_FLAG: 'SET_MACROS_UI_FLAG',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const {
|
|||||||
AZURE_APP_ID: azureAppId,
|
AZURE_APP_ID: azureAppId,
|
||||||
BRAND_NAME: brandName,
|
BRAND_NAME: brandName,
|
||||||
CHATWOOT_INBOX_TOKEN: chatwootInboxToken,
|
CHATWOOT_INBOX_TOKEN: chatwootInboxToken,
|
||||||
CSML_EDITOR_HOST: csmlEditorHost,
|
|
||||||
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
||||||
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
||||||
DISPLAY_MANIFEST: displayManifest,
|
DISPLAY_MANIFEST: displayManifest,
|
||||||
@@ -29,7 +28,6 @@ const state = {
|
|||||||
azureAppId,
|
azureAppId,
|
||||||
brandName,
|
brandName,
|
||||||
chatwootInboxToken,
|
chatwootInboxToken,
|
||||||
csmlEditorHost,
|
|
||||||
deploymentEnv,
|
deploymentEnv,
|
||||||
createNewAccountFromDashboard,
|
createNewAccountFromDashboard,
|
||||||
directUploadsEnabled: directUploadsEnabled === 'true',
|
directUploadsEnabled: directUploadsEnabled === 'true',
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
class AgentBots::CsmlJob < ApplicationJob
|
|
||||||
queue_as :high
|
|
||||||
|
|
||||||
def perform(event, agent_bot, message)
|
|
||||||
event_data = { message: message }
|
|
||||||
Integrations::Csml::ProcessorService.new(
|
|
||||||
event_name: event, agent_bot: agent_bot, event_data: event_data
|
|
||||||
).perform
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -59,14 +59,10 @@ class AgentBotListener < BaseListener
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_message_event(method_name, agent_bot, message, event)
|
def process_message_event(method_name, agent_bot, message, _event)
|
||||||
case agent_bot.bot_type
|
# Only webhook bots are supported
|
||||||
when 'webhook'
|
|
||||||
payload = message.webhook_data.merge(event: method_name)
|
payload = message.webhook_data.merge(event: method_name)
|
||||||
process_webhook_bot_event(agent_bot, payload)
|
process_webhook_bot_event(agent_bot, payload)
|
||||||
when 'csml'
|
|
||||||
process_csml_bot_event(event.name, agent_bot, message)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_webhook_bot_event(agent_bot, payload)
|
def process_webhook_bot_event(agent_bot, payload)
|
||||||
@@ -74,8 +70,4 @@ class AgentBotListener < BaseListener
|
|||||||
|
|
||||||
AgentBots::WebhookJob.perform_later(agent_bot.outgoing_url, payload)
|
AgentBots::WebhookJob.perform_later(agent_bot.outgoing_url, payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_csml_bot_event(event, agent_bot, message)
|
|
||||||
AgentBots::CsmlJob.perform_later(event, agent_bot, message)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ class AgentBot < ApplicationRecord
|
|||||||
has_many :inboxes, through: :agent_bot_inboxes
|
has_many :inboxes, through: :agent_bot_inboxes
|
||||||
has_many :messages, as: :sender, dependent: :nullify
|
has_many :messages, as: :sender, dependent: :nullify
|
||||||
belongs_to :account, optional: true
|
belongs_to :account, optional: true
|
||||||
enum bot_type: { webhook: 0, csml: 1 }
|
enum bot_type: { webhook: 0 }
|
||||||
|
|
||||||
validate :validate_agent_bot_config
|
|
||||||
validates :outgoing_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
validates :outgoing_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
||||||
|
|
||||||
def available_name
|
def available_name
|
||||||
@@ -51,9 +50,7 @@ class AgentBot < ApplicationRecord
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def system_bot?
|
||||||
|
account.nil?
|
||||||
def validate_agent_bot_config
|
|
||||||
errors.add(:bot_config, 'Invalid Bot Configuration') unless AgentBots::ValidateBotService.new(agent_bot: self).perform
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
class AgentBots::ValidateBotService
|
|
||||||
pattr_initialize [:agent_bot]
|
|
||||||
def perform
|
|
||||||
return true unless agent_bot.bot_type == 'csml'
|
|
||||||
|
|
||||||
validate_csml_bot
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def csml_client
|
|
||||||
@csml_client ||= CsmlEngine.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def csml_bot_payload
|
|
||||||
{
|
|
||||||
id: agent_bot[:name],
|
|
||||||
name: agent_bot[:name],
|
|
||||||
default_flow: 'Default',
|
|
||||||
flows: [
|
|
||||||
{
|
|
||||||
id: SecureRandom.uuid,
|
|
||||||
name: 'Default',
|
|
||||||
content: agent_bot.bot_config['csml_content'],
|
|
||||||
commands: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_csml_bot
|
|
||||||
response = csml_client.validate(csml_bot_payload)
|
|
||||||
response.blank? || response['valid']
|
|
||||||
rescue StandardError => e
|
|
||||||
ChatwootExceptionTracker.new(e, account: agent_bot&.account).capture_exception
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
json.id resource.id
|
json.id resource.id
|
||||||
json.name resource.name
|
json.name resource.name
|
||||||
json.description resource.description
|
json.description resource.description
|
||||||
json.outgoing_url resource.outgoing_url
|
json.thumbnail resource.avatar_url
|
||||||
|
json.outgoing_url resource.outgoing_url unless resource.system_bot?
|
||||||
json.bot_type resource.bot_type
|
json.bot_type resource.bot_type
|
||||||
json.bot_config resource.bot_config
|
json.bot_config resource.bot_config
|
||||||
json.account_id resource.account_id
|
json.account_id resource.account_id
|
||||||
json.access_token resource.access_token if resource.access_token.present?
|
json.access_token resource.access_token if resource.access_token.present?
|
||||||
|
json.system_bot resource.system_bot?
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
help_url: https://chwt.app/hc/help-center
|
help_url: https://chwt.app/hc/help-center
|
||||||
- name: agent_bots
|
- name: agent_bots
|
||||||
display_name: Agent Bots
|
display_name: Agent Bots
|
||||||
enabled: false
|
enabled: true
|
||||||
help_url: https://chwt.app/hc/agent-bots
|
help_url: https://chwt.app/hc/agent-bots
|
||||||
- name: macros
|
- name: macros
|
||||||
display_name: Macros
|
display_name: Macros
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class ConvertCsmlBotsToWebhookBots < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
# Find all CSML bots (bot_type = 1) and convert them to webhook (bot_type = 0)
|
||||||
|
AgentBot.where(bot_type: 1).find_each do |bot|
|
||||||
|
bot.update(bot_type: 0, bot_config: {})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# This migration is not reversible - we've removed CSML support
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2025_04_02_233933) do
|
ActiveRecord::Schema[7.0].define(version: 2025_04_10_061725) do
|
||||||
# These extensions should be enabled to support this database
|
# These extensions should be enabled to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
class CsmlEngine
|
|
||||||
API_KEY_HEADER = 'X-Api-Key'.freeze
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@host_url = GlobalConfigService.load('CSML_BOT_HOST', '')
|
|
||||||
@api_key = GlobalConfigService.load('CSML_BOT_API_KEY', '')
|
|
||||||
|
|
||||||
raise ArgumentError, 'Missing Credentials' if @host_url.blank? || @api_key.blank?
|
|
||||||
end
|
|
||||||
|
|
||||||
def status
|
|
||||||
response = HTTParty.get("#{@host_url}/status")
|
|
||||||
process_response(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
def run(bot, params)
|
|
||||||
payload = {
|
|
||||||
bot: bot,
|
|
||||||
event: {
|
|
||||||
request_id: SecureRandom.uuid,
|
|
||||||
client: params[:client],
|
|
||||||
payload: params[:payload],
|
|
||||||
metadata: params[:metadata],
|
|
||||||
ttl_duration: 4000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response = post('run', payload)
|
|
||||||
process_response(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate(bot)
|
|
||||||
response = post('validate', bot)
|
|
||||||
process_response(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def process_response(response)
|
|
||||||
return response.parsed_response if response.success?
|
|
||||||
|
|
||||||
{ error: response.parsed_response, status: response.code }
|
|
||||||
end
|
|
||||||
|
|
||||||
def post(path, payload)
|
|
||||||
HTTParty.post(
|
|
||||||
"#{@host_url}/#{path}", {
|
|
||||||
headers: { API_KEY_HEADER => @api_key, 'Content-Type' => 'application/json' },
|
|
||||||
body: payload.to_json
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
class Integrations::BotProcessorService
|
class Integrations::BotProcessorService
|
||||||
# TODO: In CSML processor service, the argument is agent bot, update initializers accordingly.
|
|
||||||
pattr_initialize [:event_name!, :hook!, :event_data!]
|
pattr_initialize [:event_name!, :hook!, :event_data!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
class Integrations::Csml::ProcessorService < Integrations::BotProcessorService
|
|
||||||
pattr_initialize [:event_name!, :event_data!, :agent_bot!]
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def csml_client
|
|
||||||
@csml_client ||= CsmlEngine.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_response(session_id, content)
|
|
||||||
csml_client.run(
|
|
||||||
bot_payload,
|
|
||||||
{
|
|
||||||
client: client_params(session_id),
|
|
||||||
payload: message_payload(content),
|
|
||||||
metadata: metadata_params
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def client_params(session_id)
|
|
||||||
{
|
|
||||||
bot_id: "chatwoot-bot-#{conversation.inbox.id}",
|
|
||||||
channel_id: "chatwoot-bot-inbox-#{conversation.inbox.id}",
|
|
||||||
user_id: session_id
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def message_payload(content)
|
|
||||||
{
|
|
||||||
content_type: 'text',
|
|
||||||
content: { text: content }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def metadata_params
|
|
||||||
{
|
|
||||||
conversation: conversation,
|
|
||||||
contact: conversation.contact
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def bot_payload
|
|
||||||
{
|
|
||||||
id: "chatwoot-csml-bot-#{agent_bot.id}",
|
|
||||||
name: "chatwoot-csml-bot-#{agent_bot.id}",
|
|
||||||
default_flow: 'chatwoot_bot_flow',
|
|
||||||
flows: [
|
|
||||||
{
|
|
||||||
id: "chatwoot-csml-bot-flow-#{agent_bot.id}-inbox-#{conversation.inbox.id}",
|
|
||||||
name: 'chatwoot_bot_flow',
|
|
||||||
content: agent_bot.bot_config['csml_content'],
|
|
||||||
commands: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_response(message, response)
|
|
||||||
csml_messages = response['messages']
|
|
||||||
has_conversation_ended = response['conversation_end']
|
|
||||||
|
|
||||||
process_action(message, 'handoff') if has_conversation_ended.present?
|
|
||||||
|
|
||||||
return if csml_messages.blank?
|
|
||||||
|
|
||||||
# We do not support wait, typing now.
|
|
||||||
csml_messages.each do |csml_message|
|
|
||||||
create_messages(csml_message, conversation)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_messages(message, conversation)
|
|
||||||
message_payload = message['payload']
|
|
||||||
|
|
||||||
case message_payload['content_type']
|
|
||||||
when 'text'
|
|
||||||
process_text_messages(message_payload, conversation)
|
|
||||||
when 'question'
|
|
||||||
process_question_messages(message_payload, conversation)
|
|
||||||
when 'image'
|
|
||||||
process_image_messages(message_payload, conversation)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_text_messages(message_payload, conversation)
|
|
||||||
conversation.messages.create!(
|
|
||||||
{
|
|
||||||
message_type: :outgoing,
|
|
||||||
account_id: conversation.account_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
content: message_payload['content']['text'],
|
|
||||||
sender: agent_bot
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_question_messages(message_payload, conversation)
|
|
||||||
buttons = message_payload['content']['buttons'].map do |button|
|
|
||||||
{ title: button['content']['title'], value: button['content']['payload'] }
|
|
||||||
end
|
|
||||||
conversation.messages.create!(
|
|
||||||
{
|
|
||||||
message_type: :outgoing,
|
|
||||||
account_id: conversation.account_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
content: message_payload['content']['title'],
|
|
||||||
content_type: 'input_select',
|
|
||||||
content_attributes: { items: buttons },
|
|
||||||
sender: agent_bot
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def prepare_attachment(message_payload, message, account_id)
|
|
||||||
attachment_params = { file_type: :image, account_id: account_id }
|
|
||||||
attachment_url = message_payload['content']['url']
|
|
||||||
attachment = message.attachments.new(attachment_params)
|
|
||||||
attachment_file = Down.download(attachment_url)
|
|
||||||
attachment.file.attach(
|
|
||||||
io: attachment_file,
|
|
||||||
filename: attachment_file.original_filename,
|
|
||||||
content_type: attachment_file.content_type
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_image_messages(message_payload, conversation)
|
|
||||||
message = conversation.messages.new(
|
|
||||||
{
|
|
||||||
message_type: :outgoing,
|
|
||||||
account_id: conversation.account_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
content: '',
|
|
||||||
content_type: 'text',
|
|
||||||
sender: agent_bot
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
prepare_attachment(message_payload, message, conversation.account_id)
|
|
||||||
message.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -28,6 +28,31 @@ RSpec.describe 'Agent Bot API', type: :request do
|
|||||||
expect(response.body).to include(agent_bot.access_token.token)
|
expect(response.body).to include(agent_bot.access_token.token)
|
||||||
expect(response.body).not_to include(global_bot.access_token.token)
|
expect(response.body).not_to include(global_bot.access_token.token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'properly differentiates between system bots and account bots' do
|
||||||
|
global_bot = create(:agent_bot)
|
||||||
|
get "/api/v1/accounts/#{account.id}/agent_bots",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
response_data = response.parsed_body
|
||||||
|
# Find the global bot in the response
|
||||||
|
global_bot_response = response_data.find { |bot| bot['id'] == global_bot.id }
|
||||||
|
# Find the account bot in the response
|
||||||
|
account_bot_response = response_data.find { |bot| bot['id'] == agent_bot.id }
|
||||||
|
|
||||||
|
# Verify system_bot attribute and outgoing_url for global bot
|
||||||
|
expect(global_bot_response['system_bot']).to be(true)
|
||||||
|
expect(global_bot_response).not_to include('outgoing_url')
|
||||||
|
|
||||||
|
# Verify account bot has system_bot attribute false and includes outgoing_url
|
||||||
|
expect(account_bot_response['system_bot']).to be(false)
|
||||||
|
expect(account_bot_response).to include('outgoing_url')
|
||||||
|
|
||||||
|
# Verify both bots have thumbnail field
|
||||||
|
expect(global_bot_response).to include('thumbnail')
|
||||||
|
expect(account_bot_response).to include('thumbnail')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -60,6 +85,10 @@ RSpec.describe 'Agent Bot API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include(global_bot.name)
|
expect(response.body).to include(global_bot.name)
|
||||||
expect(response.body).not_to include(global_bot.access_token.token)
|
expect(response.body).not_to include(global_bot.access_token.token)
|
||||||
|
|
||||||
|
# Test for system_bot attribute and webhook URL not being exposed
|
||||||
|
expect(response.parsed_body['system_bot']).to be(true)
|
||||||
|
expect(response.parsed_body).not_to include('outgoing_url')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -142,7 +171,7 @@ RSpec.describe 'Agent Bot API', type: :request do
|
|||||||
expect(response.body).not_to include(global_bot.access_token.token)
|
expect(response.body).not_to include(global_bot.access_token.token)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates avatar' do
|
it 'updates avatar and includes thumbnail in response' do
|
||||||
# no avatar before upload
|
# no avatar before upload
|
||||||
expect(agent_bot.avatar.attached?).to be(false)
|
expect(agent_bot.avatar.attached?).to be(false)
|
||||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
@@ -153,6 +182,9 @@ RSpec.describe 'Agent Bot API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
agent_bot.reload
|
agent_bot.reload
|
||||||
expect(agent_bot.avatar.attached?).to be(true)
|
expect(agent_bot.avatar.attached?).to be(true)
|
||||||
|
|
||||||
|
# Verify thumbnail is included in the response
|
||||||
|
expect(response.parsed_body).to include('thumbnail')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updated avatar with avatar_url' do
|
it 'updated avatar with avatar_url' do
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe AgentBots::CsmlJob do
|
|
||||||
it 'runs csml processor service' do
|
|
||||||
event = 'message.created'
|
|
||||||
message = create(:message)
|
|
||||||
agent_bot = create(:agent_bot)
|
|
||||||
processor = double
|
|
||||||
|
|
||||||
allow(Integrations::Csml::ProcessorService).to receive(:new).and_return(processor)
|
|
||||||
allow(processor).to receive(:perform)
|
|
||||||
|
|
||||||
described_class.perform_now(event, agent_bot, message)
|
|
||||||
|
|
||||||
expect(Integrations::Csml::ProcessorService)
|
|
||||||
.to have_received(:new)
|
|
||||||
.with(event_name: event, agent_bot: agent_bot, event_data: { message: message })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe CsmlEngine do
|
|
||||||
it 'raises an exception if host and api is absent' do
|
|
||||||
expect { described_class.new }.to raise_error(StandardError)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when CSML_BOT_HOST & CSML_BOT_API_KEY is present' do
|
|
||||||
before do
|
|
||||||
create(:installation_config, { name: 'CSML_BOT_HOST', value: 'https://csml.chatwoot.dev' })
|
|
||||||
create(:installation_config, { name: 'CSML_BOT_API_KEY', value: 'random_api_key' })
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:csml_request) { double }
|
|
||||||
|
|
||||||
context 'when status is called' do
|
|
||||||
it 'returns api response if client response is valid' do
|
|
||||||
allow(HTTParty).to receive(:get).and_return(csml_request)
|
|
||||||
allow(csml_request).to receive(:success?).and_return(true)
|
|
||||||
allow(csml_request).to receive(:parsed_response).and_return({ 'engine_version': '1.11.1' })
|
|
||||||
|
|
||||||
response = described_class.new.status
|
|
||||||
|
|
||||||
expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status')
|
|
||||||
expect(csml_request).to have_received(:success?)
|
|
||||||
expect(csml_request).to have_received(:parsed_response)
|
|
||||||
expect(response).to eq({ 'engine_version': '1.11.1' })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns error if client response is invalid' do
|
|
||||||
allow(HTTParty).to receive(:get).and_return(csml_request)
|
|
||||||
allow(csml_request).to receive(:success?).and_return(false)
|
|
||||||
allow(csml_request).to receive(:code).and_return(401)
|
|
||||||
allow(csml_request).to receive(:parsed_response).and_return({ 'error': true })
|
|
||||||
|
|
||||||
response = described_class.new.status
|
|
||||||
|
|
||||||
expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status')
|
|
||||||
expect(csml_request).to have_received(:success?)
|
|
||||||
expect(response).to eq({ error: { 'error': true }, status: 401 })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when run is called' do
|
|
||||||
it 'returns api response if client response is valid' do
|
|
||||||
allow(HTTParty).to receive(:post).and_return(csml_request)
|
|
||||||
allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc')
|
|
||||||
allow(csml_request).to receive(:success?).and_return(true)
|
|
||||||
allow(csml_request).to receive(:parsed_response).and_return({ 'success': true })
|
|
||||||
|
|
||||||
response = described_class.new.run({ flow: 'default' }, { client: 'client', payload: { id: 1 }, metadata: {} })
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
bot: { flow: 'default' },
|
|
||||||
event: {
|
|
||||||
request_id: 'xxxx-yyyy-wwww-cccc',
|
|
||||||
client: 'client',
|
|
||||||
payload: { id: 1 },
|
|
||||||
metadata: {},
|
|
||||||
ttl_duration: 4000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(HTTParty).to have_received(:post)
|
|
||||||
.with(
|
|
||||||
'https://csml.chatwoot.dev/run', {
|
|
||||||
body: payload.to_json,
|
|
||||||
headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(csml_request).to have_received(:success?)
|
|
||||||
expect(csml_request).to have_received(:parsed_response)
|
|
||||||
expect(response).to eq({ 'success': true })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when validate is called' do
|
|
||||||
it 'returns api response if client response is valid' do
|
|
||||||
allow(HTTParty).to receive(:post).and_return(csml_request)
|
|
||||||
allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc')
|
|
||||||
allow(csml_request).to receive(:success?).and_return(true)
|
|
||||||
allow(csml_request).to receive(:parsed_response).and_return({ 'success': true })
|
|
||||||
|
|
||||||
payload = { flow: 'default' }
|
|
||||||
response = described_class.new.validate(payload)
|
|
||||||
|
|
||||||
expect(HTTParty).to have_received(:post)
|
|
||||||
.with(
|
|
||||||
'https://csml.chatwoot.dev/validate', {
|
|
||||||
body: payload.to_json,
|
|
||||||
headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(csml_request).to have_received(:success?)
|
|
||||||
expect(csml_request).to have_received(:parsed_response)
|
|
||||||
expect(response).to eq({ 'success': true })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe Integrations::Csml::ProcessorService do
|
|
||||||
let(:account) { create(:account) }
|
|
||||||
let(:inbox) { create(:inbox, account: account) }
|
|
||||||
let(:agent_bot) { create(:agent_bot, :skip_validate, bot_type: 'csml', account: account) }
|
|
||||||
let(:agent_bot_inbox) { create(:agent_bot_inbox, agent_bot: agent_bot, inbox: inbox, account: account) }
|
|
||||||
let(:conversation) { create(:conversation, account: account, status: :pending) }
|
|
||||||
let(:message) { create(:message, account: account, conversation: conversation) }
|
|
||||||
let(:event_name) { 'message.created' }
|
|
||||||
let(:event_data) { { message: message } }
|
|
||||||
|
|
||||||
describe '#perform' do
|
|
||||||
let(:csml_client) { double }
|
|
||||||
let(:processor) { described_class.new(event_name: event_name, agent_bot: agent_bot, event_data: event_data) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(CsmlEngine).to receive(:new).and_return(csml_client)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a conversation is completed from CSML' do
|
|
||||||
it 'open the conversation and handsoff it to an agent' do
|
|
||||||
csml_response = ActiveSupport::HashWithIndifferentAccess.new(conversation_end: true)
|
|
||||||
allow(csml_client).to receive(:run).and_return(csml_response)
|
|
||||||
|
|
||||||
processor.perform
|
|
||||||
expect(conversation.reload.status).to eql('open')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when a new message is returned from CSML' do
|
|
||||||
it 'creates a text message' do
|
|
||||||
csml_response = ActiveSupport::HashWithIndifferentAccess.new(
|
|
||||||
messages: [
|
|
||||||
{ payload: { content_type: 'text', content: { text: 'hello payload' } } }
|
|
||||||
]
|
|
||||||
)
|
|
||||||
allow(csml_client).to receive(:run).and_return(csml_response)
|
|
||||||
processor.perform
|
|
||||||
expect(conversation.messages.last.content).to eql('hello payload')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'creates a question message' do
|
|
||||||
csml_response = ActiveSupport::HashWithIndifferentAccess.new(
|
|
||||||
messages: [{
|
|
||||||
payload: {
|
|
||||||
content_type: 'question',
|
|
||||||
content: { title: 'Question Payload', buttons: [{ content: { title: 'Q1', payload: 'q1' } }] }
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
allow(csml_client).to receive(:run).and_return(csml_response)
|
|
||||||
processor.perform
|
|
||||||
expect(conversation.messages.last.content).to eql('Question Payload')
|
|
||||||
expect(conversation.messages.last.content_type).to eql('input_select')
|
|
||||||
expect(conversation.messages.last.content_attributes).to eql({ items: [{ title: 'Q1', value: 'q1' }] }.with_indifferent_access)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when conversation status is not pending' do
|
|
||||||
let(:conversation) { create(:conversation, account: account, status: :open) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(processor.perform).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when message is private' do
|
|
||||||
let(:message) { create(:message, account: account, conversation: conversation, private: true) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(processor.perform).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when message type is template (not outgoing or incoming)' do
|
|
||||||
let(:message) { create(:message, account: account, conversation: conversation, message_type: :template) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(processor.perform).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when message updated' do
|
|
||||||
let(:event_name) { 'message.updated' }
|
|
||||||
|
|
||||||
context 'when content_type is input_select' do
|
|
||||||
let(:message) do
|
|
||||||
create(:message, account: account, conversation: conversation, private: true,
|
|
||||||
submitted_values: [{ 'title' => 'Support', 'value' => 'selected_gas' }])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns submitted value for message content' do
|
|
||||||
expect(processor.send(:message_content, message)).to eql('selected_gas')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when content_type is not input_select' do
|
|
||||||
let(:message) { create(:message, account: account, conversation: conversation, message_type: :outgoing, content_type: :text) }
|
|
||||||
let(:event_name) { 'message.updated' }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(processor.perform).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -37,15 +37,6 @@ describe AgentBotListener do
|
|||||||
listener.message_created(event)
|
listener.message_created(event)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when agent bot csml type is configured' do
|
|
||||||
it 'sends message to agent bot' do
|
|
||||||
agent_bot_csml = create(:agent_bot, :skip_validate, bot_type: 'csml')
|
|
||||||
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot_csml)
|
|
||||||
expect(AgentBots::CsmlJob).to receive(:perform_later).with('message.created', agent_bot_csml, message).once
|
|
||||||
listener.message_created(event)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#webwidget_triggered' do
|
describe '#webwidget_triggered' do
|
||||||
|
|||||||
@@ -39,4 +39,23 @@ RSpec.describe AgentBot do
|
|||||||
expect(message.reload.sender).to be_nil
|
expect(message.reload.sender).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#system_bot?' do
|
||||||
|
context 'when account_id is nil' do
|
||||||
|
let(:agent_bot) { create(:agent_bot, account_id: nil) }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(agent_bot.system_bot?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account_id is present' do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:agent_bot) { create(:agent_bot, account: account) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(agent_bot.system_bot?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe AgentBots::ValidateBotService do
|
|
||||||
describe '#perform' do
|
|
||||||
it 'returns true if bot_type is not csml' do
|
|
||||||
agent_bot = create(:agent_bot)
|
|
||||||
valid = described_class.new(agent_bot: agent_bot).perform
|
|
||||||
expect(valid).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true if validate csml returns true' do
|
|
||||||
agent_bot = create(:agent_bot, :skip_validate, bot_type: 'csml', bot_config: {})
|
|
||||||
csml_client = double
|
|
||||||
csml_response = double
|
|
||||||
allow(CsmlEngine).to receive(:new).and_return(csml_client)
|
|
||||||
allow(csml_client).to receive(:validate).and_return(csml_response)
|
|
||||||
allow(csml_response).to receive(:blank?).and_return(false)
|
|
||||||
allow(csml_response).to receive(:[]).with('valid').and_return(true)
|
|
||||||
|
|
||||||
valid = described_class.new(agent_bot: agent_bot).perform
|
|
||||||
expect(valid).to be true
|
|
||||||
expect(CsmlEngine).to have_received(:new)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user