feat: Allow agent-bots to be created from the UI (#4153)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Fayaz Ahmed
2022-11-18 11:45:58 +05:30
committed by GitHub
parent 9bfbd528ef
commit 47676c3cce
17 changed files with 1573 additions and 40 deletions

View File

@@ -1,7 +1,11 @@
<template>
<span>
{{ textToBeDisplayed }}
<button class="show-more--button" @click="toggleShowMore">
<button
v-if="text.length > limit"
class="show-more--button"
@click="toggleShowMore"
>
{{ buttonLabel }}
</button>
</span>
@@ -25,7 +29,7 @@ export default {
},
computed: {
textToBeDisplayed() {
if (this.showMore) {
if (this.showMore || this.text.length <= this.limit) {
return this.text;
}

View File

@@ -1,5 +1,62 @@
{
"AGENT_BOTS": {
"HEADER": "Bots"
"HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
},
"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"
},
"ADD": {
"TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully",
"ERROR_MESSAGE": "Could not add bot, Please try again later"
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
},
"DELETE": {
"BUTTON_TEXT": "Delete",
"TITLE": "Delete Bot",
"SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
}
},
"EDIT": {
"BUTTON_TEXT": "Edit",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "Cancel",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
}
},
"TYPES": {
"WEBHOOK": "Webhook Bot",
"CSML": "CSML Bot"
}
}
}

View File

@@ -1,18 +1,103 @@
<template>
<div>Agent Bot list</div>
<div class="column content-box">
<div class="row">
<div class="small-8 columns with-right-space">
<woot-loading-state
v-if="uiFlags.isFetching"
:message="$t('AGENT_BOTS.LIST.LOADING')"
/>
<table v-else-if="agentBots.length" class="woot-table">
<tbody>
<agent-bot-row
v-for="(agentBot, index) in agentBots"
:key="agentBot.id"
:agent-bot="agentBot"
:index="index"
@delete="onDeleteAgentBot"
@edit="onEditAgentBot"
/>
</tbody>
</table>
<p v-else class="no-items-error-message">
{{ $t('AGENT_BOTS.LIST.404') }}
</p>
</div>
<div class="small-4 columns content-box">
<p v-html="$t('AGENT_BOTS.SIDEBAR_TXT')" />
</div>
</div>
<woot-button
color-scheme="success"
class-names="button--fixed-right-top"
icon="add-circle"
>
<router-link :to="newAgentBotsURL" class="white-text">
{{ $t('AGENT_BOTS.ADD.TITLE') }}
</router-link>
</woot-button>
<woot-confirm-modal
ref="confirmDialog"
:title="$t('AGENT_BOTS.DELETE.TITLE')"
:description="$t('AGENT_BOTS.DELETE.DESCRIPTION')"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { frontendURL } from '../../../../helper/URLHelper';
import AgentBotRow from './components/AgentBotRow.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: { AgentBotRow },
mixins: [alertMixin],
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
agentBots: 'agentBots/getBots',
uiFlags: 'agentBots/getUIFlags',
}),
newAgentBotsURL() {
return frontendURL(
`accounts/${this.accountId}/settings/agent-bots/csml/new`
);
},
},
mounted() {
this.$store.dispatch('agentBots/get');
},
methods: {
async onDeleteAgentBot(bot) {
const ok = await this.$refs.confirmDialog.showConfirmation();
if (ok) {
try {
await this.$store.dispatch('agentBots/delete', bot.id);
this.showAlert(this.$t('AGENT_BOTS.DELETE.API.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(this.$t('AGENT_BOTS.DELETE.API.ERROR_MESSAGE'));
}
}
},
onEditAgentBot(bot) {
this.$router.push(
frontendURL(
`accounts/${this.accountId}/settings/agent-bots/csml/${bot.id}`
)
);
},
},
};
</script>
<style scoped>
.bots-list {
list-style: none;
}
.nowrap {
white-space: nowrap;
}
.white-text {
color: white;
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<tr>
<td class="agent-bot--details">
<div class="agent-bot--link">
{{ agentBot.name }}
(<agent-bot-type :bot-type="agentBot.bot_type" />)
</div>
<div class="agent-bot--description">
<show-more :text="agentBot.description" :limit="120" />
</div>
</td>
<td class="button-wrapper">
<woot-button
v-if="isACSMLTypeBot"
v-tooltip.top="$t('AGENT_BOTS.EDIT.BUTTON_TEXT')"
variant="smooth"
size="tiny"
color-scheme="secondary"
icon="edit"
@click="$emit('edit', agentBot)"
/>
<woot-button
v-tooltip.top="$t('AGENT_BOTS.DELETE.BUTTON_TEXT')"
variant="smooth"
color-scheme="alert"
size="tiny"
icon="dismiss-circle"
@click="$emit('delete', agentBot, index)"
/>
</td>
</tr>
</template>
<script>
import ShowMore from 'dashboard/components/widgets/ShowMore';
import AgentBotType from './AgentBotType.vue';
export default {
components: { ShowMore, AgentBotType },
props: {
agentBot: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
isACSMLTypeBot() {
const { bot_type: botType } = this.agentBot;
return botType === 'csml';
},
},
};
</script>
<style scoped lang="scss">
.agent-bot--link {
align-items: center;
color: var(--s-800);
display: flex;
font-weight: var(--font-weight-medium);
word-break: break-word;
}
.agent-bot--description {
color: var(--s-700);
font-size: var(--font-size-mini);
}
.agent-bot--type {
color: var(--s-600);
font-weight: var(--font-weight-medium);
margin-bottom: var(--space-small);
}
.agent-bot--details {
width: 90%;
}
.button-wrapper {
max-width: var(--space-mega);
min-width: auto;
button:nth-child(2) {
margin-left: var(--space-normal);
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<span>
<img
v-tooltip="botTypeConfig[botType].label"
class="agent-bot-type--thumbnail"
:src="botTypeConfig[botType].thumbnail"
:alt="botTypeConfig[botType].label"
/>
<span>{{ botTypeConfig[botType].label }}</span>
</span>
</template>
<script>
export default {
props: {
botType: {
type: String,
default: 'webhook',
},
},
data() {
return {
botTypeConfig: {
csml: {
label: this.$t('AGENT_BOTS.TYPES.CSML'),
thumbnail: '/dashboard/images/agent-bots/csml.png',
},
webhook: {
label: this.$t('AGENT_BOTS.TYPES.WEBHOOK'),
thumbnail: '/dashboard/images/agent-bots/webhook.svg',
},
},
};
},
};
</script>
<style scoped>
.agent-bot-type--thumbnail {
width: auto;
height: var(--space-slab);
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<div class="column content-box no-padding">
<div class="row">
<div class="small-8 columns">
<div class="full-height editor-wrapper">
<csml-monaco-editor v-model="bot.csmlContent" class="bot-editor" />
<div v-if="$v.bot.csmlContent.$error" class="editor-error-message">
<span>{{ $t('AGENT_BOTS.CSML_BOT_EDITOR.BOT_CONFIG.ERROR') }}</span>
</div>
</div>
</div>
<div class="small-4 columns content-box full-height">
<form class="details-editor" @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>
<woot-button>
{{ $t('AGENT_BOTS.CSML_BOT_EDITOR.SUBMIT') }}
</woot-button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import { required } from 'vuelidate/lib/validators';
import CsmlMonacoEditor from './CSMLMonacoEditor.vue';
export default {
components: { CsmlMonacoEditor },
props: {
agentBot: {
type: Object,
default: () => {},
},
},
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>
<style scoped>
.no-padding {
padding: 0 !important;
}
.full-height {
height: calc(100vh - 56px);
}
.bot-editor {
width: 100%;
height: 100%;
}
.details-editor {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.editor-wrapper {
position: relative;
}
.editor-error-message {
position: absolute;
bottom: 0;
width: 100%;
padding: 1rem;
background-color: #e0bbbb;
display: flex;
align-items: center;
font-size: 1.2rem;
justify-content: center;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="csml-editor--container">
<loading-state
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>
<script>
import LoadingState from 'dashboard/components/widgets/LoadingState';
import { mapGetters } from 'vuex';
export default {
components: { LoadingState },
props: {
value: {
type: String,
default: '',
},
},
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('input', csmlContent);
};
},
methods: {
onEditorLoad() {
const frameElement = document.getElementById(`csml-editor--frame`);
const eventData = {
event: 'editorContext',
data: this.value || '',
};
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
this.iframeLoading = false;
},
},
};
</script>
<style scoped>
#csml-editor--frame {
border: 0;
width: 100%;
height: 100%;
}
</style>

View File

@@ -1,6 +1,48 @@
<template>
<div>Component to edit CSML Bots</div>
<csml-bot-editor
v-if="agentBot.id"
:agent-bot="agentBot"
@submit="updateBot"
/>
<div v-else class="column content-box no-padding">
<spinner />
</div>
</template>
<script>
export default {};
import Spinner from 'shared/components/Spinner';
import CsmlBotEditor from '../components/CSMLBotEditor.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
export default {
components: { Spinner, CsmlBotEditor },
mixins: [alertMixin],
computed: {
...mapGetters({ uiFlags: 'agentBots/uiFlags' }),
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 },
});
this.showAlert(this.$t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(
this.$t('AGENT_BOTS.CSML_BOT_EDITOR.BOT_CONFIG.API_ERROR')
);
}
},
},
};
</script>

View File

@@ -1,6 +1,42 @@
<template>
<div>Component to create CSML Bots</div>
<csml-bot-editor :agent-bot="{ bot_config: {} }" @submit="saveBot" />
</template>
<script>
export default {};
import alertMixin from 'shared/mixins/alertMixin';
import CsmlBotEditor from '../components/CSMLBotEditor.vue';
import { frontendURL } from '../../../../../helper/URLHelper';
import { mapGetters } from 'vuex';
export default {
components: { CsmlBotEditor },
mixins: [alertMixin],
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}`
)
);
}
this.showAlert(this.$t('AGENT_BOTS.ADD.API.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(this.$t('AGENT_BOTS.ADD.API.ERROR_MESSAGE'));
}
},
},
};
</script>

View File

@@ -16,7 +16,7 @@ const state = {
export const getters = {
getAccount: $state => id => {
return $state.records.find(record => record.id === Number(id));
return $state.records.find(record => record.id === Number(id)) || {};
},
getUIFlags($state) {
return $state.uiFlags;

View File

@@ -44,11 +44,13 @@ export const actions = {
try {
const response = await AgentBotsAPI.create(agentBotObj);
commit(types.ADD_AGENT_BOT, response.data);
return response.data;
} catch (error) {
throwErrorMessage(error);
} finally {
commit(types.SET_AGENT_BOT_UI_FLAG, { isCreating: false });
}
return null;
},
update: async ({ commit }, { id, ...agentBotObj }) => {
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true });
@@ -76,7 +78,7 @@ export const actions = {
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetchingItem: true });
try {
const { data } = await AgentBotsAPI.show(id);
commit(types.DELETE_AGENT_BOT, data);
commit(types.ADD_AGENT_BOT, data);
} catch (error) {
throwErrorMessage(error);
} finally {
@@ -92,7 +94,7 @@ export const mutations = {
...data,
};
},
[types.ADD_AGENT_BOT]: MutationHelpers.create,
[types.ADD_AGENT_BOT]: MutationHelpers.setSingleRecord,
[types.SET_AGENT_BOTS]: MutationHelpers.set,
[types.EDIT_AGENT_BOT]: MutationHelpers.update,
[types.DELETE_AGENT_BOT]: MutationHelpers.destroy,

View File

@@ -4,6 +4,7 @@ const {
APP_VERSION: appVersion,
BRAND_NAME: brandName,
CHATWOOT_INBOX_TOKEN: chatwootInboxToken,
CSML_EDITOR_HOST: csmlEditorHost,
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
DISPLAY_MANIFEST: displayManifest,
@@ -24,6 +25,7 @@ const state = {
appVersion,
brandName,
chatwootInboxToken,
csmlEditorHost,
deploymentEnv,
createNewAccountFromDashboard,
directUploadsEnabled: directUploadsEnabled === 'true',