feat: New Assistants Edit Page (#11920)
# Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-4602/assistants-edit-page ### Screenshots <img width="1650" height="890" alt="image" src="https://github.com/user-attachments/assets/f9834cd3-9cf3-4173-8d33-1aad04d8991e" /> <img width="1650" height="890" alt="image" src="https://github.com/user-attachments/assets/a628328a-fdfd-4ee7-86b5-2a1945e0c114" /> --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -15,8 +15,8 @@ const emit = defineEmits(['click']);
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const onClick = event => {
|
const onClick = (item, index) => {
|
||||||
emit('click', event);
|
emit('click', item, index);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -24,22 +24,25 @@ const onClick = event => {
|
|||||||
<nav :aria-label="t('BREADCRUMB.ARIA_LABEL')" class="flex items-center h-8">
|
<nav :aria-label="t('BREADCRUMB.ARIA_LABEL')" class="flex items-center h-8">
|
||||||
<ol class="flex items-center mb-0">
|
<ol class="flex items-center mb-0">
|
||||||
<li v-for="(item, index) in items" :key="index" class="flex items-center">
|
<li v-for="(item, index) in items" :key="index" class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
v-if="index > 0"
|
||||||
|
icon="i-lucide-chevron-right"
|
||||||
|
class="flex-shrink-0 mx-2 size-4 text-n-slate-11 dark:text-n-slate-11"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Render as button for all except the last item -->
|
||||||
<button
|
<button
|
||||||
v-if="index === 0"
|
v-if="index !== items.length - 1"
|
||||||
class="inline-flex items-center justify-center min-w-0 gap-2 p-0 text-sm font-medium transition-all duration-200 ease-in-out border-0 rounded-lg text-n-slate-11 hover:text-n-slate-12 outline-transparent max-w-56"
|
class="inline-flex items-center justify-center min-w-0 gap-2 p-0 text-sm font-medium transition-all duration-200 ease-in-out border-0 rounded-lg text-n-slate-11 hover:text-n-slate-12 outline-transparent max-w-56"
|
||||||
@click="onClick"
|
@click="onClick(item, index)"
|
||||||
>
|
>
|
||||||
<span class="min-w-0 truncate">{{ item.label }}</span>
|
<span class="min-w-0 truncate">{{ item.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
<template v-else>
|
|
||||||
<Icon
|
<!-- The last breadcrumb item is plain text -->
|
||||||
icon="i-lucide-chevron-right"
|
<span v-else class="text-sm truncate text-n-slate-12 max-w-56">
|
||||||
class="flex-shrink-0 mx-2 size-4 text-n-slate-11 dark:text-n-slate-11"
|
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
||||||
/>
|
</span>
|
||||||
<span class="text-sm truncate text-n-slate-12 max-w-56">
|
|
||||||
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
isFetching: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isEmpty: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
currentPage: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
totalCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
default: 25,
|
||||||
|
},
|
||||||
|
showPaginationFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
breadcrumbItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:currentPage']);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handlePageChange = event => {
|
||||||
|
emit('update:currentPage', event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBreadcrumbClick = item => {
|
||||||
|
router.push({
|
||||||
|
name: item.routeName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="my-4 px-10 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
||||||
|
>
|
||||||
|
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full">
|
||||||
|
<header class="mb-7 sticky top-0 z-10 bg-n-background">
|
||||||
|
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||||
|
</header>
|
||||||
|
<main class="flex gap-16 w-full flex-1 pb-16">
|
||||||
|
<section
|
||||||
|
v-if="$slots.body || $slots.emptyState || isFetching"
|
||||||
|
class="flex flex-col w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isFetching"
|
||||||
|
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isEmpty">
|
||||||
|
<slot name="emptyState" />
|
||||||
|
</div>
|
||||||
|
<slot v-else name="body" />
|
||||||
|
</section>
|
||||||
|
<section v-if="$slots.controls" class="flex w-full">
|
||||||
|
<slot name="controls" />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 pb-4">
|
||||||
|
<PaginationFooter
|
||||||
|
:current-page="currentPage"
|
||||||
|
:total-items="totalCount"
|
||||||
|
:items-per-page="itemsPerPage"
|
||||||
|
@update:current-page="handlePageChange"
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, computed, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
|
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
assistant: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
productName: '',
|
||||||
|
features: {
|
||||||
|
conversationFaqs: false,
|
||||||
|
memories: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...initialState });
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
name: { required, minLength: minLength(1) },
|
||||||
|
description: { required, minLength: minLength(1) },
|
||||||
|
productName: { required, minLength: minLength(1) },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
|
const getErrorMessage = field => {
|
||||||
|
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formErrors = computed(() => ({
|
||||||
|
name: getErrorMessage('name'),
|
||||||
|
description: getErrorMessage('description'),
|
||||||
|
productName: getErrorMessage('productName'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateStateFromAssistant = assistant => {
|
||||||
|
const { config = {} } = assistant;
|
||||||
|
state.name = assistant.name;
|
||||||
|
state.description = assistant.description;
|
||||||
|
state.productName = config.product_name;
|
||||||
|
state.features = {
|
||||||
|
conversationFaqs: config.feature_faq || false,
|
||||||
|
memories: config.feature_memory || false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBasicInfoUpdate = async () => {
|
||||||
|
const result = await Promise.all([
|
||||||
|
v$.value.name.$validate(),
|
||||||
|
v$.value.description.$validate(),
|
||||||
|
v$.value.productName.$validate(),
|
||||||
|
]).then(results => results.every(Boolean));
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: state.name,
|
||||||
|
description: state.description,
|
||||||
|
config: {
|
||||||
|
...props.assistant.config,
|
||||||
|
product_name: state.productName,
|
||||||
|
feature_faq: state.features.conversationFaqs,
|
||||||
|
feature_memory: state.features.memories,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.assistant,
|
||||||
|
newAssistant => {
|
||||||
|
if (newAssistant) updateStateFromAssistant(newAssistant);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Input
|
||||||
|
v-model="state.name"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.NAME.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.NAME.PLACEHOLDER')"
|
||||||
|
:message="formErrors.name"
|
||||||
|
:message-type="formErrors.name ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="state.productName"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.PRODUCT_NAME.PLACEHOLDER')"
|
||||||
|
:message="formErrors.productName"
|
||||||
|
:message-type="formErrors.productName ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
v-model="state.description"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||||
|
:message="formErrors.description"
|
||||||
|
:message-type="formErrors.description ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.TITLE') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="state.features.conversationFaqs"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-checkbox"
|
||||||
|
/>
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS') }}
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-model="state.features.memories"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-checkbox"
|
||||||
|
/>
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||||
|
@click="handleBasicInfoUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
controlItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onClick = name => {
|
||||||
|
router.push({ name });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:key="controlItem.name"
|
||||||
|
class="pt-3 ltr:pl-4 rtl:pr-4 ltr:pr-2 rtl:pl-2 pb-5 gap-2 flex flex-col w-full shadow outline-1 outline outline-n-container rounded-2xl bg-n-solid-2 cursor-pointer"
|
||||||
|
@click="onClick(controlItem.routeName)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between w-full gap-1 h-8">
|
||||||
|
<span class="text-sm font-medium text-n-slate-12 line-clamp-1">
|
||||||
|
{{ controlItem.name }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-chevron-right"
|
||||||
|
slate
|
||||||
|
ghost
|
||||||
|
xs
|
||||||
|
@click="onClick(controlItem.routeName)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-n-slate-11 text-sm leading-[21px] line-clamp-5">
|
||||||
|
{{ controlItem.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactive, computed, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { minLength } from '@vuelidate/validators';
|
||||||
|
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
assistant: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
handoffMessage: '',
|
||||||
|
resolutionMessage: '',
|
||||||
|
temperature: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({ ...initialState });
|
||||||
|
|
||||||
|
const validationRules = {
|
||||||
|
handoffMessage: { minLength: minLength(1) },
|
||||||
|
resolutionMessage: { minLength: minLength(1) },
|
||||||
|
};
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
|
const getErrorMessage = field => {
|
||||||
|
return v$.value[field].$error ? v$.value[field].$errors[0].$message : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formErrors = computed(() => ({
|
||||||
|
handoffMessage: getErrorMessage('handoffMessage'),
|
||||||
|
resolutionMessage: getErrorMessage('resolutionMessage'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateStateFromAssistant = assistant => {
|
||||||
|
const { config = {} } = assistant;
|
||||||
|
state.handoffMessage = config.handoff_message;
|
||||||
|
state.resolutionMessage = config.resolution_message;
|
||||||
|
state.temperature = config.temperature || 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSystemMessagesUpdate = async () => {
|
||||||
|
const result = await Promise.all([
|
||||||
|
v$.value.handoffMessage.$validate(),
|
||||||
|
v$.value.resolutionMessage.$validate(),
|
||||||
|
]).then(results => results.every(Boolean));
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
config: {
|
||||||
|
...props.assistant.config,
|
||||||
|
handoff_message: state.handoffMessage,
|
||||||
|
resolution_message: state.resolutionMessage,
|
||||||
|
temperature: state.temperature || 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('submit', payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.assistant,
|
||||||
|
newAssistant => {
|
||||||
|
if (newAssistant) updateStateFromAssistant(newAssistant);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<Editor
|
||||||
|
v-model="state.handoffMessage"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.HANDOFF_MESSAGE.PLACEHOLDER')"
|
||||||
|
:message="formErrors.handoffMessage"
|
||||||
|
:message-type="formErrors.handoffMessage ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
v-model="state.resolutionMessage"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.RESOLUTION_MESSAGE.PLACEHOLDER')"
|
||||||
|
:message="formErrors.resolutionMessage"
|
||||||
|
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
v-model="state.temperature"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-n-slate-11 italic">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
||||||
|
@click="handleSystemMessagesUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -36,6 +36,7 @@ export const FEATURE_FLAGS = {
|
|||||||
REPORT_V4: 'report_v4',
|
REPORT_V4: 'report_v4',
|
||||||
CHANNEL_INSTAGRAM: 'channel_instagram',
|
CHANNEL_INSTAGRAM: 'channel_instagram',
|
||||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||||
|
CAPTAIN_V2: 'captain_integration_v2',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREMIUM_FEATURES = [
|
export const PREMIUM_FEATURES = [
|
||||||
@@ -44,4 +45,5 @@ export const PREMIUM_FEATURES = [
|
|||||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||||
FEATURE_FLAGS.AUDIT_LOGS,
|
FEATURE_FLAGS.AUDIT_LOGS,
|
||||||
FEATURE_FLAGS.HELP_CENTER,
|
FEATURE_FLAGS.HELP_CENTER,
|
||||||
|
FEATURE_FLAGS.CAPTAIN_V2,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -478,6 +478,37 @@
|
|||||||
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
"ERROR_MESSAGE": "There was an error updating the assistant, please try again.",
|
||||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||||
},
|
},
|
||||||
|
"SETTINGS": {
|
||||||
|
"BREADCRUMB": {
|
||||||
|
"ASSISTANT": "Assistant"
|
||||||
|
},
|
||||||
|
"BASIC_SETTINGS": {
|
||||||
|
"TITLE": "Basic settings",
|
||||||
|
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||||
|
},
|
||||||
|
"SYSTEM_SETTINGS": {
|
||||||
|
"TITLE": "System settings",
|
||||||
|
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||||
|
},
|
||||||
|
"CONTROL_ITEMS": {
|
||||||
|
"TITLE": "The Fun Stuff",
|
||||||
|
"DESCRIPTION": "Add more control to the assistant. (a bit more visual like a story : Query guardrail → scenarios → output) Nudges user to actually utilise these.",
|
||||||
|
"OPTIONS": {
|
||||||
|
"GUARDRAILS": {
|
||||||
|
"TITLE": "Guardrails",
|
||||||
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic."
|
||||||
|
},
|
||||||
|
"SCENARIOS": {
|
||||||
|
"TITLE": "Scenarios",
|
||||||
|
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”"
|
||||||
|
},
|
||||||
|
"RESPONSE_GUIDELINES": {
|
||||||
|
"TITLE": "Response guidelines",
|
||||||
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"EDIT_ASSISTANT": "Edit Assistant",
|
"EDIT_ASSISTANT": "Edit Assistant",
|
||||||
"DELETE_ASSISTANT": "Delete Assistant",
|
"DELETE_ASSISTANT": "Delete Assistant",
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import { useStore } from 'dashboard/composables/store';
|
|||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } 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 { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.vue';
|
import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.vue';
|
||||||
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
|
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
|
||||||
|
|
||||||
|
import AssistantSettings from 'dashboard/routes/dashboard/captain/assistants/settings/Settings.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -19,6 +22,16 @@ const assistant = computed(() =>
|
|||||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFeatureEnabledonAccount = useMapGetter(
|
||||||
|
'accounts/isFeatureEnabledonAccount'
|
||||||
|
);
|
||||||
|
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||||
|
|
||||||
|
const isCaptainV2Enabled = isFeatureEnabledonAccount.value(
|
||||||
|
currentAccountId.value,
|
||||||
|
FEATURE_FLAGS.CAPTAIN_V2
|
||||||
|
);
|
||||||
|
|
||||||
const isAssistantAvailable = computed(() => !!assistant.value?.id);
|
const isAssistantAvailable = computed(() => !!assistant.value?.id);
|
||||||
|
|
||||||
const handleSubmit = async updatedAssistant => {
|
const handleSubmit = async updatedAssistant => {
|
||||||
@@ -36,14 +49,16 @@ const handleSubmit = async updatedAssistant => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!isAssistantAvailable.value) {
|
if (!isAssistantAvailable.value || !isCaptainV2Enabled) {
|
||||||
store.dispatch('captainAssistants/show', assistantId);
|
store.dispatch('captainAssistants/show', assistantId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<AssistantSettings v-if="isCaptainV2Enabled" />
|
||||||
<PageLayout
|
<PageLayout
|
||||||
|
v-else
|
||||||
:header-title="assistant?.name"
|
:header-title="assistant?.name"
|
||||||
:show-pagination-footer="false"
|
:show-pagination-footer="false"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useStore } from 'dashboard/composables/store';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
||||||
|
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
||||||
|
import AssistantBasicSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue';
|
||||||
|
import AssistantSystemSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue';
|
||||||
|
import AssistantControlItems from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const store = useStore();
|
||||||
|
const assistantId = route.params.assistantId;
|
||||||
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
|
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||||
|
const assistant = computed(() =>
|
||||||
|
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAssistantAvailable = computed(() => !!assistant.value?.id);
|
||||||
|
|
||||||
|
const controlItems = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.GUARDRAILS.TITLE'
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.GUARDRAILS.DESCRIPTION'
|
||||||
|
),
|
||||||
|
// routeName: 'captain_assistants_guardrails_index',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.TITLE'
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
|
||||||
|
),
|
||||||
|
// routeName: 'captain_assistants_scenarios_index',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.TITLE'
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.DESCRIPTION'
|
||||||
|
),
|
||||||
|
// routeName: 'captain_assistants_guidelines_index',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const breadcrumbItems = computed(() => {
|
||||||
|
const activeControlItem = controlItems.value?.find(
|
||||||
|
item => item.routeName === route.name
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
|
||||||
|
routeName: 'captain_assistants_index',
|
||||||
|
},
|
||||||
|
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
|
||||||
|
...(activeControlItem
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: activeControlItem.name,
|
||||||
|
routeName: activeControlItem.routeName,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async updatedAssistant => {
|
||||||
|
try {
|
||||||
|
await store.dispatch('captainAssistants/update', {
|
||||||
|
id: assistantId,
|
||||||
|
...updatedAssistant,
|
||||||
|
});
|
||||||
|
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error?.message || t('CAPTAIN.ASSISTANTS.EDIT.ERROR_MESSAGE');
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isAssistantAvailable.value) {
|
||||||
|
store.dispatch('captainAssistants/show', assistantId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsPageLayout
|
||||||
|
:breadcrumb-items="breadcrumbItems"
|
||||||
|
:is-fetching="isFetching"
|
||||||
|
class="[&>div]:max-w-[80rem]"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<SettingsHeader
|
||||||
|
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
|
||||||
|
:description="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<AssistantBasicSettingsForm
|
||||||
|
:assistant="assistant"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="h-px w-full bg-n-weak mt-2" />
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<SettingsHeader
|
||||||
|
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.TITLE')"
|
||||||
|
:description="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.DESCRIPTION')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<AssistantSystemSettingsForm
|
||||||
|
:assistant="assistant"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #controls>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<SettingsHeader
|
||||||
|
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
|
||||||
|
:description="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<AssistantControlItems
|
||||||
|
v-for="item in controlItems"
|
||||||
|
:key="item.name"
|
||||||
|
:control-item="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SettingsPageLayout>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,7 @@ import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
|||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
import AssistantIndex from './assistants/Index.vue';
|
import AssistantIndex from './assistants/Index.vue';
|
||||||
import AssistantEdit from './assistants/Edit.vue';
|
import AssistantEdit from './assistants/Edit.vue';
|
||||||
|
// import AssistantSettings from './assistants/settings/Settings.vue';
|
||||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
||||||
import DocumentsIndex from './documents/Index.vue';
|
import DocumentsIndex from './documents/Index.vue';
|
||||||
import ResponsesIndex from './responses/Index.vue';
|
import ResponsesIndex from './responses/Index.vue';
|
||||||
|
|||||||
@@ -176,3 +176,7 @@
|
|||||||
- name: notion_integration
|
- name: notion_integration
|
||||||
display_name: Notion Integration
|
display_name: Notion Integration
|
||||||
enabled: false
|
enabled: false
|
||||||
|
- name: captain_integration_v2
|
||||||
|
display_name: Captain V2
|
||||||
|
enabled: false
|
||||||
|
premium: true
|
||||||
|
|||||||
Reference in New Issue
Block a user