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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user