feat: Update Captain navigation structure (#12761)
# Pull Request Template ## Description This PR includes an update to the Captain navigation structure. ## Route Structure ```javascript 1. captain_assistants_responses_index → /captain/:assistantId/faqs 2. captain_assistants_documents_index → /captain/:assistantId/documents 3. captain_assistants_scenarios_index → /captain/:assistantId/scenarios 4. captain_assistants_playground_index → /captain/:assistantId/playground 5. captain_assistants_inboxes_index → /captain/:assistantId/inboxes 6. captain_tools_index → /captain/tools 7. captain_assistants_settings_index → /captain/:assistantId/settings 8. captain_assistants_guardrails_index → /captain/:assistantId/settings/guardrails 9. captain_assistants_guidelines_index → /captain/:assistantId/settings/guidelines 10. captain_assistants_index → /captain/:navigationPath ``` **How it works:** 1. User clicks sidebar item → Routes to `captain_assistants_index` with `navigationPath` 2. `AssistantsIndexPage` validates route and gets last active assistant, if not redirects to assistant create page. 3. Routes to actual page: `/captain/:assistantId/:page` 4. Page loads with correct assistant context Fixes https://linear.app/chatwoot/issue/CW-5832/updating-captain-navigation ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
90352b3a20
commit
5bf39d20e5
@ -1,11 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import { OnClickOutside } from '@vueuse/components';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import BackButton from 'dashboard/components/widgets/BackButton.vue';
|
import BackButton from 'dashboard/components/widgets/BackButton.vue';
|
||||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
import Policy from 'dashboard/components/policy.vue';
|
import Policy from 'dashboard/components/policy.vue';
|
||||||
|
import AssistantSwitcher from 'dashboard/components-next/captain/pageComponents/switcher/AssistantSwitcher.vue';
|
||||||
|
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
currentPage: {
|
currentPage: {
|
||||||
@ -56,11 +62,32 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showAssistantSwitcher: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['click', 'close', 'update:currentPage']);
|
const emit = defineEmits(['click', 'close', 'update:currentPage']);
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const { shouldShowPaywall } = usePolicy();
|
const { shouldShowPaywall } = usePolicy();
|
||||||
|
|
||||||
|
const showAssistantSwitcherDropdown = ref(false);
|
||||||
|
const createAssistantDialogRef = ref(null);
|
||||||
|
|
||||||
|
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||||
|
|
||||||
|
const currentAssistantId = computed(() => route.params.assistantId);
|
||||||
|
|
||||||
|
const activeAssistantName = computed(() => {
|
||||||
|
return (
|
||||||
|
assistants.value?.find(
|
||||||
|
assistant => assistant.id === Number(currentAssistantId.value)
|
||||||
|
)?.name || ''
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const showPaywall = computed(() => {
|
const showPaywall = computed(() => {
|
||||||
return shouldShowPaywall(props.featureFlag);
|
return shouldShowPaywall(props.featureFlag);
|
||||||
});
|
});
|
||||||
@ -72,6 +99,15 @@ const handleButtonClick = () => {
|
|||||||
const handlePageChange = event => {
|
const handlePageChange = event => {
|
||||||
emit('update:currentPage', event);
|
emit('update:currentPage', event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleAssistantSwitcher = () => {
|
||||||
|
showAssistantSwitcherDropdown.value = !showAssistantSwitcherDropdown.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateAssistant = () => {
|
||||||
|
showAssistantSwitcherDropdown.value = false;
|
||||||
|
createAssistantDialogRef.value.dialogRef.open();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -82,9 +118,48 @@ const handlePageChange = event => {
|
|||||||
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
<BackButton v-if="backUrl" :to="backUrl" />
|
<BackButton v-if="backUrl" :back-url="backUrl" />
|
||||||
<slot name="headerTitle">
|
<slot name="headerTitle">
|
||||||
<span class="text-xl font-medium text-n-slate-12">
|
<div v-if="showAssistantSwitcher" class="flex items-center gap-2">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
v-if="activeAssistantName"
|
||||||
|
class="text-xl font-medium truncate text-n-slate-12"
|
||||||
|
>
|
||||||
|
{{ activeAssistantName }}
|
||||||
|
</span>
|
||||||
|
<div v-if="activeAssistantName" class="relative group">
|
||||||
|
<OnClickOutside
|
||||||
|
@trigger="showAssistantSwitcherDropdown = false"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-chevron-down"
|
||||||
|
variant="ghost"
|
||||||
|
color="slate"
|
||||||
|
size="xs"
|
||||||
|
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3 [&>span]:size-4"
|
||||||
|
@click="toggleAssistantSwitcher"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AssistantSwitcher
|
||||||
|
v-if="showAssistantSwitcherDropdown"
|
||||||
|
class="absolute ltr:left-0 rtl:right-0 top-9"
|
||||||
|
@close="showAssistantSwitcherDropdown = false"
|
||||||
|
@create-assistant="handleCreateAssistant"
|
||||||
|
/>
|
||||||
|
</OnClickOutside>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
v-if="activeAssistantName"
|
||||||
|
icon="i-lucide-chevron-right"
|
||||||
|
class="size-6 text-n-slate-11"
|
||||||
|
/>
|
||||||
|
<span class="text-xl font-medium text-n-slate-11">
|
||||||
|
{{ headerTitle }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-xl font-medium text-n-slate-12">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
@ -97,21 +172,24 @@ const handlePageChange = event => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex gap-2">
|
||||||
v-if="!showPaywall && buttonLabel"
|
<slot name="search" />
|
||||||
v-on-clickaway="() => emit('close')"
|
<div
|
||||||
class="relative group/campaign-button"
|
v-if="!showPaywall && buttonLabel"
|
||||||
>
|
v-on-clickaway="() => emit('close')"
|
||||||
<Policy :permissions="buttonPolicy">
|
class="relative group/captain-button"
|
||||||
<Button
|
>
|
||||||
:label="buttonLabel"
|
<Policy :permissions="buttonPolicy">
|
||||||
icon="i-lucide-plus"
|
<Button
|
||||||
size="sm"
|
:label="buttonLabel"
|
||||||
class="group-hover/campaign-button:brightness-110"
|
icon="i-lucide-plus"
|
||||||
@click="handleButtonClick"
|
size="sm"
|
||||||
/>
|
class="group-hover/captain-button:brightness-110"
|
||||||
</Policy>
|
@click="handleButtonClick"
|
||||||
<slot name="action" />
|
/>
|
||||||
|
</Policy>
|
||||||
|
<slot name="action" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot name="subHeader" />
|
<slot name="subHeader" />
|
||||||
@ -144,5 +222,6 @@ const handlePageChange = event => {
|
|||||||
@update:current-page="handlePageChange"
|
@update:current-page="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
|
<CreateAssistantDialog ref="createAssistantDialogRef" type="create" />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
<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="px-6 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 mb-4">
|
|
||||||
<header class="mb-7 sticky top-0 bg-n-background pt-4 z-20">
|
|
||||||
<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>
|
|
||||||
@ -76,12 +76,11 @@ const handleAction = ({ action, value }) => {
|
|||||||
<template>
|
<template>
|
||||||
<CardLayout>
|
<CardLayout>
|
||||||
<div class="flex justify-between w-full gap-1">
|
<div class="flex justify-between w-full gap-1">
|
||||||
<router-link
|
<h6
|
||||||
:to="{ name: 'captain_assistants_edit', params: { assistantId: id } }"
|
class="text-base font-normal text-n-slate-12 line-clamp-1 hover:underline transition-colors"
|
||||||
class="text-base text-n-slate-12 line-clamp-1 hover:underline transition-colors"
|
|
||||||
>
|
>
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</router-link>
|
</h6>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
v-on-clickaway="() => toggleDropdown(false)"
|
v-on-clickaway="() => toggleDropdown(false)"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
import MessageList from './MessageList.vue';
|
import MessageList from './MessageList.vue';
|
||||||
@ -29,6 +29,16 @@ const resetConversation = () => {
|
|||||||
newMessage.value = '';
|
newMessage.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watch for assistant ID changes and reset conversation
|
||||||
|
watch(
|
||||||
|
() => assistantId,
|
||||||
|
(newId, oldId) => {
|
||||||
|
if (oldId && newId !== oldId) {
|
||||||
|
resetConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!newMessage.value.trim() || isLoading.value) return;
|
if (!newMessage.value.trim() || isLoading.value) return;
|
||||||
|
|
||||||
@ -74,7 +84,7 @@ const sendMessage = async () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<NextButton
|
<NextButton
|
||||||
ghost
|
ghost
|
||||||
size="small"
|
sm
|
||||||
icon="i-lucide-rotate-ccw"
|
icon="i-lucide-rotate-ccw"
|
||||||
@click="resetConversation"
|
@click="resetConversation"
|
||||||
/>
|
/>
|
||||||
@ -97,7 +107,7 @@ const sendMessage = async () => {
|
|||||||
/>
|
/>
|
||||||
<NextButton
|
<NextButton
|
||||||
ghost
|
ghost
|
||||||
size="small"
|
sm
|
||||||
:disabled="!newMessage.trim()"
|
:disabled="!newMessage.trim()"
|
||||||
icon="i-lucide-send"
|
icon="i-lucide-send"
|
||||||
@click="sendMessage"
|
@click="sendMessage"
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { OnClickOutside } from '@vueuse/components';
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
|
||||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
assistantId: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['update']);
|
|
||||||
const { t } = useI18n();
|
|
||||||
const isFilterOpen = ref(false);
|
|
||||||
|
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
|
||||||
const assistantOptions = computed(() => [
|
|
||||||
{
|
|
||||||
label: t(`CAPTAIN.RESPONSES.FILTER.ALL_ASSISTANTS`),
|
|
||||||
value: 'all',
|
|
||||||
action: 'filter',
|
|
||||||
},
|
|
||||||
...assistants.value.map(assistant => ({
|
|
||||||
value: assistant.id,
|
|
||||||
label: assistant.name,
|
|
||||||
action: 'filter',
|
|
||||||
})),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const selectedAssistantLabel = computed(() => {
|
|
||||||
const assistant = assistantOptions.value.find(
|
|
||||||
option => option.value === props.assistantId
|
|
||||||
);
|
|
||||||
return t('CAPTAIN.RESPONSES.FILTER.ASSISTANT', {
|
|
||||||
selected: assistant ? assistant.label : '',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleAssistantFilterChange = ({ value }) => {
|
|
||||||
isFilterOpen.value = false;
|
|
||||||
emit('update', value);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<OnClickOutside @trigger="isFilterOpen = false">
|
|
||||||
<Button
|
|
||||||
:label="selectedAssistantLabel"
|
|
||||||
icon="i-lucide-chevron-down"
|
|
||||||
size="sm"
|
|
||||||
color="slate"
|
|
||||||
trailing-icon
|
|
||||||
class="max-w-48"
|
|
||||||
@click="isFilterOpen = !isFilterOpen"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenu
|
|
||||||
v-if="isFilterOpen"
|
|
||||||
:menu-items="assistantOptions"
|
|
||||||
class="mt-2"
|
|
||||||
@action="handleAssistantFilterChange"
|
|
||||||
/>
|
|
||||||
</OnClickOutside>
|
|
||||||
</template>
|
|
||||||
@ -18,7 +18,7 @@ const props = defineProps({
|
|||||||
validator: value => ['create', 'edit'].includes(value),
|
validator: value => ['create', 'edit'].includes(value),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close', 'created']);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
@ -35,8 +35,18 @@ const i18nKey = computed(
|
|||||||
() => `CAPTAIN.ASSISTANTS.${props.type.toUpperCase()}`
|
() => `CAPTAIN.ASSISTANTS.${props.type.toUpperCase()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const createAssistant = assistantDetails =>
|
const createAssistant = async assistantDetails => {
|
||||||
store.dispatch('captainAssistants/create', assistantDetails);
|
try {
|
||||||
|
const newAssistant = await store.dispatch(
|
||||||
|
'captainAssistants/create',
|
||||||
|
assistantDetails
|
||||||
|
);
|
||||||
|
emit('created', newAssistant);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error?.message || t(`${i18nKey.value}.ERROR_MESSAGE`);
|
||||||
|
useAlert(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async updatedAssistant => {
|
const handleSubmit = async updatedAssistant => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,333 +0,0 @@
|
|||||||
<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 { useMapGetter } from 'dashboard/composables/store';
|
|
||||||
|
|
||||||
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';
|
|
||||||
import Accordion from 'dashboard/components-next/Accordion/Accordion.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
validator: value => ['edit', 'create'].includes(value),
|
|
||||||
},
|
|
||||||
assistant: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['submit']);
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const formState = {
|
|
||||||
uiFlags: useMapGetter('captainAssistants/getUIFlags'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
productName: '',
|
|
||||||
welcomeMessage: '',
|
|
||||||
handoffMessage: '',
|
|
||||||
resolutionMessage: '',
|
|
||||||
instructions: '',
|
|
||||||
features: {
|
|
||||||
conversationFaqs: false,
|
|
||||||
memories: false,
|
|
||||||
citations: false,
|
|
||||||
},
|
|
||||||
temperature: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({ ...initialState });
|
|
||||||
|
|
||||||
const validationRules = {
|
|
||||||
name: { required, minLength: minLength(1) },
|
|
||||||
description: { required, minLength: minLength(1) },
|
|
||||||
productName: { required, minLength: minLength(1) },
|
|
||||||
welcomeMessage: { minLength: minLength(1) },
|
|
||||||
handoffMessage: { minLength: minLength(1) },
|
|
||||||
resolutionMessage: { minLength: minLength(1) },
|
|
||||||
instructions: { minLength: minLength(1) },
|
|
||||||
};
|
|
||||||
|
|
||||||
const v$ = useVuelidate(validationRules, state);
|
|
||||||
|
|
||||||
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
|
||||||
|
|
||||||
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'),
|
|
||||||
welcomeMessage: getErrorMessage('welcomeMessage'),
|
|
||||||
handoffMessage: getErrorMessage('handoffMessage'),
|
|
||||||
resolutionMessage: getErrorMessage('resolutionMessage'),
|
|
||||||
instructions: getErrorMessage('instructions'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const updateStateFromAssistant = assistant => {
|
|
||||||
const { config = {} } = assistant;
|
|
||||||
state.name = assistant.name;
|
|
||||||
state.description = assistant.description;
|
|
||||||
state.productName = config.product_name;
|
|
||||||
state.welcomeMessage = config.welcome_message;
|
|
||||||
state.handoffMessage = config.handoff_message;
|
|
||||||
state.resolutionMessage = config.resolution_message;
|
|
||||||
state.instructions = config.instructions;
|
|
||||||
state.features = {
|
|
||||||
conversationFaqs: config.feature_faq || false,
|
|
||||||
memories: config.feature_memory || false,
|
|
||||||
citations: config.feature_citation || false,
|
|
||||||
};
|
|
||||||
state.temperature = config.temperature || 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('submit', payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSystemMessagesUpdate = async () => {
|
|
||||||
const result = await Promise.all([
|
|
||||||
v$.value.welcomeMessage.$validate(),
|
|
||||||
v$.value.handoffMessage.$validate(),
|
|
||||||
v$.value.resolutionMessage.$validate(),
|
|
||||||
]).then(results => results.every(Boolean));
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
config: {
|
|
||||||
...props.assistant.config,
|
|
||||||
welcome_message: state.welcomeMessage,
|
|
||||||
handoff_message: state.handoffMessage,
|
|
||||||
resolution_message: state.resolutionMessage,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('submit', payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInstructionsUpdate = async () => {
|
|
||||||
const result = await v$.value.instructions.$validate();
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
config: {
|
|
||||||
...props.assistant.config,
|
|
||||||
temperature: state.temperature || 1,
|
|
||||||
instructions: state.instructions,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('submit', payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFeaturesUpdate = () => {
|
|
||||||
const payload = {
|
|
||||||
config: {
|
|
||||||
...props.assistant.config,
|
|
||||||
feature_faq: state.features.conversationFaqs,
|
|
||||||
feature_memory: state.features.memories,
|
|
||||||
feature_citation: state.features.citations,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('submit', payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.assistant,
|
|
||||||
newAssistant => {
|
|
||||||
if (props.mode === 'edit' && newAssistant) {
|
|
||||||
updateStateFromAssistant(newAssistant);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
|
||||||
<!-- Basic Information Section -->
|
|
||||||
<Accordion
|
|
||||||
:title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.BASIC_INFO')"
|
|
||||||
is-open
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-4 pt-4">
|
|
||||||
<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'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
:loading="isLoading"
|
|
||||||
@click="handleBasicInfoUpdate"
|
|
||||||
>
|
|
||||||
{{ t('CAPTAIN.ASSISTANTS.FORM.UPDATE') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<!-- Instructions Section -->
|
|
||||||
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<Editor
|
|
||||||
v-model="state.instructions"
|
|
||||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
|
|
||||||
:message="formErrors.instructions"
|
|
||||||
:max-length="20000"
|
|
||||||
:message-type="formErrors.instructions ? 'error' : 'info'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 mt-4">
|
|
||||||
<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 class="flex justify-end">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
:loading="isLoading"
|
|
||||||
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
|
||||||
@click="handleInstructionsUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<!-- Greeting Messages Section -->
|
|
||||||
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.SYSTEM_MESSAGES')">
|
|
||||||
<div class="flex flex-col gap-4 pt-4">
|
|
||||||
<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 justify-end">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
:loading="isLoading"
|
|
||||||
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
|
||||||
@click="handleSystemMessagesUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<!-- Features Section -->
|
|
||||||
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.FEATURES')">
|
|
||||||
<div class="flex flex-col gap-4 pt-4">
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
{{
|
|
||||||
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
|
|
||||||
}}
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input v-model="state.features.memories" type="checkbox" />
|
|
||||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input v-model="state.features.citations" type="checkbox" />
|
|
||||||
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
:loading="isLoading"
|
|
||||||
:label="t('CAPTAIN.ASSISTANTS.FORM.UPDATE')"
|
|
||||||
@click="handleFeaturesUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Accordion>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
@ -3,6 +3,8 @@ import { reactive, computed, watch } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { minLength } from '@vuelidate/validators';
|
import { minLength } from '@vuelidate/validators';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
@ -17,10 +19,16 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['submit']);
|
const emit = defineEmits(['submit']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { isCloudFeatureEnabled } = useAccount();
|
||||||
|
|
||||||
|
const isCaptainV2Enabled = computed(() =>
|
||||||
|
isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_V2)
|
||||||
|
);
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
handoffMessage: '',
|
handoffMessage: '',
|
||||||
resolutionMessage: '',
|
resolutionMessage: '',
|
||||||
|
instructions: '',
|
||||||
temperature: 1,
|
temperature: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,6 +37,7 @@ const state = reactive({ ...initialState });
|
|||||||
const validationRules = {
|
const validationRules = {
|
||||||
handoffMessage: { minLength: minLength(1) },
|
handoffMessage: { minLength: minLength(1) },
|
||||||
resolutionMessage: { minLength: minLength(1) },
|
resolutionMessage: { minLength: minLength(1) },
|
||||||
|
instructions: { minLength: minLength(1) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const v$ = useVuelidate(validationRules, state);
|
const v$ = useVuelidate(validationRules, state);
|
||||||
@ -40,20 +49,30 @@ const getErrorMessage = field => {
|
|||||||
const formErrors = computed(() => ({
|
const formErrors = computed(() => ({
|
||||||
handoffMessage: getErrorMessage('handoffMessage'),
|
handoffMessage: getErrorMessage('handoffMessage'),
|
||||||
resolutionMessage: getErrorMessage('resolutionMessage'),
|
resolutionMessage: getErrorMessage('resolutionMessage'),
|
||||||
|
instructions: getErrorMessage('instructions'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const updateStateFromAssistant = assistant => {
|
const updateStateFromAssistant = assistant => {
|
||||||
const { config = {} } = assistant;
|
const { config = {} } = assistant;
|
||||||
state.handoffMessage = config.handoff_message;
|
state.handoffMessage = config.handoff_message;
|
||||||
state.resolutionMessage = config.resolution_message;
|
state.resolutionMessage = config.resolution_message;
|
||||||
|
state.instructions = config.instructions;
|
||||||
state.temperature = config.temperature || 1;
|
state.temperature = config.temperature || 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSystemMessagesUpdate = async () => {
|
const handleSystemMessagesUpdate = async () => {
|
||||||
const result = await Promise.all([
|
const validations = [
|
||||||
v$.value.handoffMessage.$validate(),
|
v$.value.handoffMessage.$validate(),
|
||||||
v$.value.resolutionMessage.$validate(),
|
v$.value.resolutionMessage.$validate(),
|
||||||
]).then(results => results.every(Boolean));
|
];
|
||||||
|
|
||||||
|
if (!isCaptainV2Enabled.value) {
|
||||||
|
validations.push(v$.value.instructions.$validate());
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Promise.all(validations).then(results =>
|
||||||
|
results.every(Boolean)
|
||||||
|
);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -65,6 +84,10 @@ const handleSystemMessagesUpdate = async () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isCaptainV2Enabled.value) {
|
||||||
|
payload.config.instructions = state.instructions;
|
||||||
|
}
|
||||||
|
|
||||||
emit('submit', payload);
|
emit('submit', payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,6 +118,16 @@ watch(
|
|||||||
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
|
:message-type="formErrors.resolutionMessage ? 'error' : 'info'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
v-if="!isCaptainV2Enabled"
|
||||||
|
v-model="state.instructions"
|
||||||
|
:label="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.LABEL')"
|
||||||
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
|
||||||
|
:message="formErrors.instructions"
|
||||||
|
:max-length="20000"
|
||||||
|
:message-type="formErrors.instructions ? 'error' : 'info'"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-sm font-medium text-n-slate-12">
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
|
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
|
||||||
|
|||||||
@ -8,6 +8,13 @@ import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
|||||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
import DocumentForm from './DocumentForm.vue';
|
import DocumentForm from './DocumentForm.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
assistantId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@ -48,7 +55,11 @@ defineExpose({ dialogRef });
|
|||||||
:show-confirm-button="false"
|
:show-confirm-button="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<DocumentForm @submit="handleSubmit" @cancel="handleCancel" />
|
<DocumentForm
|
||||||
|
:assistant-id="assistantId"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
/>
|
||||||
<template #footer />
|
<template #footer />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { reactive, computed, ref, nextTick } from 'vue';
|
import { reactive, computed, ref, nextTick } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required, minLength, requiredIf, url } from '@vuelidate/validators';
|
import { minLength, requiredIf, url } from '@vuelidate/validators';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
@ -10,6 +10,13 @@ import Input from 'dashboard/components-next/input/Input.vue';
|
|||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
assistantId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['submit', 'cancel']);
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
@ -18,13 +25,11 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const formState = {
|
const formState = {
|
||||||
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
|
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
|
||||||
assistants: useMapGetter('captainAssistants/getRecords'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
assistantId: null,
|
|
||||||
documentType: 'url',
|
documentType: 'url',
|
||||||
pdfFile: null,
|
pdfFile: null,
|
||||||
};
|
};
|
||||||
@ -38,19 +43,11 @@ const validationRules = {
|
|||||||
url: requiredIf(() => state.documentType === 'url' && url),
|
url: requiredIf(() => state.documentType === 'url' && url),
|
||||||
minLength: requiredIf(() => state.documentType === 'url' && minLength(1)),
|
minLength: requiredIf(() => state.documentType === 'url' && minLength(1)),
|
||||||
},
|
},
|
||||||
assistantId: { required },
|
|
||||||
pdfFile: {
|
pdfFile: {
|
||||||
required: requiredIf(() => state.documentType === 'pdf'),
|
required: requiredIf(() => state.documentType === 'pdf'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const assistantList = computed(() =>
|
|
||||||
formState.assistants.value.map(assistant => ({
|
|
||||||
value: assistant.id,
|
|
||||||
label: assistant.name,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const documentTypeOptions = [
|
const documentTypeOptions = [
|
||||||
{ value: 'url', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') },
|
{ value: 'url', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') },
|
||||||
{ value: 'pdf', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') },
|
{ value: 'pdf', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') },
|
||||||
@ -70,7 +67,6 @@ const getErrorMessage = (field, errorKey) => {
|
|||||||
|
|
||||||
const formErrors = computed(() => ({
|
const formErrors = computed(() => ({
|
||||||
url: getErrorMessage('url', 'URL'),
|
url: getErrorMessage('url', 'URL'),
|
||||||
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
|
|
||||||
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
|
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -106,7 +102,7 @@ const openFileDialog = () => {
|
|||||||
|
|
||||||
const prepareDocumentDetails = () => {
|
const prepareDocumentDetails = () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('document[assistant_id]', state.assistantId);
|
formData.append('document[assistant_id]', props.assistantId);
|
||||||
|
|
||||||
if (state.documentType === 'url') {
|
if (state.documentType === 'url') {
|
||||||
formData.append('document[external_link]', state.url);
|
formData.append('document[external_link]', state.url);
|
||||||
@ -218,21 +214,6 @@ const handleSubmit = async () => {
|
|||||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.NAME.PLACEHOLDER')"
|
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.NAME.PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
|
||||||
{{ t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<ComboBox
|
|
||||||
id="assistant"
|
|
||||||
v-model="state.assistantId"
|
|
||||||
:options="assistantList"
|
|
||||||
:has-error="!!formErrors.assistantId"
|
|
||||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.PLACEHOLDER')"
|
|
||||||
class="[&>div>button]:bg-n-alpha-black2"
|
|
||||||
:message="formErrors.assistantId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 justify-between items-center w-full">
|
<div class="flex gap-3 justify-between items-center w-full">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
|||||||
import { useStore } from 'dashboard/composables/store';
|
import { 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 { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
import ResponseForm from './ResponseForm.vue';
|
import ResponseForm from './ResponseForm.vue';
|
||||||
@ -21,6 +22,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const dialogRef = ref(null);
|
const dialogRef = ref(null);
|
||||||
const responseForm = ref(null);
|
const responseForm = ref(null);
|
||||||
@ -39,9 +41,15 @@ const createResponse = responseDetails =>
|
|||||||
const handleSubmit = async updatedResponse => {
|
const handleSubmit = async updatedResponse => {
|
||||||
try {
|
try {
|
||||||
if (props.type === 'edit') {
|
if (props.type === 'edit') {
|
||||||
await updateResponse(updatedResponse);
|
await updateResponse({
|
||||||
|
...updatedResponse,
|
||||||
|
assistant_id: route.params.assistantId,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await createResponse(updatedResponse);
|
await createResponse({
|
||||||
|
...updatedResponse,
|
||||||
|
assistant_id: route.params.assistantId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
|
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
|
||||||
dialogRef.value.close();
|
dialogRef.value.close();
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { useMapGetter } from 'dashboard/composables/store';
|
|||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
@ -21,18 +20,17 @@ const props = defineProps({
|
|||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['submit', 'cancel']);
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const formState = {
|
const formState = {
|
||||||
uiFlags: useMapGetter('captainResponses/getUIFlags'),
|
uiFlags: useMapGetter('captainResponses/getUIFlags'),
|
||||||
assistants: useMapGetter('captainAssistants/getRecords'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
question: '',
|
question: '',
|
||||||
answer: '',
|
answer: '',
|
||||||
assistantId: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = reactive({ ...initialState });
|
const state = reactive({ ...initialState });
|
||||||
@ -40,16 +38,8 @@ const state = reactive({ ...initialState });
|
|||||||
const validationRules = {
|
const validationRules = {
|
||||||
question: { required, minLength: minLength(1) },
|
question: { required, minLength: minLength(1) },
|
||||||
answer: { required, minLength: minLength(1) },
|
answer: { required, minLength: minLength(1) },
|
||||||
assistantId: { required },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const assistantList = computed(() =>
|
|
||||||
formState.assistants.value.map(assistant => ({
|
|
||||||
value: assistant.id,
|
|
||||||
label: assistant.name,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const v$ = useVuelidate(validationRules, state);
|
const v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
||||||
@ -63,7 +53,6 @@ const getErrorMessage = (field, errorKey) => {
|
|||||||
const formErrors = computed(() => ({
|
const formErrors = computed(() => ({
|
||||||
question: getErrorMessage('question', 'QUESTION'),
|
question: getErrorMessage('question', 'QUESTION'),
|
||||||
answer: getErrorMessage('answer', 'ANSWER'),
|
answer: getErrorMessage('answer', 'ANSWER'),
|
||||||
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleCancel = () => emit('cancel');
|
const handleCancel = () => emit('cancel');
|
||||||
@ -71,7 +60,6 @@ const handleCancel = () => emit('cancel');
|
|||||||
const prepareDocumentDetails = () => ({
|
const prepareDocumentDetails = () => ({
|
||||||
question: state.question,
|
question: state.question,
|
||||||
answer: state.answer,
|
answer: state.answer,
|
||||||
assistant_id: state.assistantId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@ -86,12 +74,11 @@ const handleSubmit = async () => {
|
|||||||
const updateStateFromResponse = response => {
|
const updateStateFromResponse = response => {
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
|
|
||||||
const { question, answer, assistant } = response;
|
const { question, answer } = response;
|
||||||
|
|
||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
question,
|
question,
|
||||||
answer,
|
answer,
|
||||||
assistantId: assistant.id,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,7 +102,6 @@ watch(
|
|||||||
:message="formErrors.question"
|
:message="formErrors.question"
|
||||||
:message-type="formErrors.question ? 'error' : 'info'"
|
:message-type="formErrors.question ? 'error' : 'info'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Editor
|
<Editor
|
||||||
v-model="state.answer"
|
v-model="state.answer"
|
||||||
:label="t('CAPTAIN.RESPONSES.FORM.ANSWER.LABEL')"
|
:label="t('CAPTAIN.RESPONSES.FORM.ANSWER.LABEL')"
|
||||||
@ -124,22 +110,6 @@ watch(
|
|||||||
:max-length="10000"
|
:max-length="10000"
|
||||||
:message-type="formErrors.answer ? 'error' : 'info'"
|
:message-type="formErrors.answer ? 'error' : 'info'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
|
||||||
{{ t('CAPTAIN.RESPONSES.FORM.ASSISTANT.LABEL') }}
|
|
||||||
</label>
|
|
||||||
<ComboBox
|
|
||||||
id="assistant"
|
|
||||||
v-model="state.assistantId"
|
|
||||||
:options="assistantList"
|
|
||||||
:has-error="!!formErrors.assistantId"
|
|
||||||
:placeholder="t('CAPTAIN.RESPONSES.FORM.ASSISTANT.PLACEHOLDER')"
|
|
||||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
|
||||||
:message="formErrors.assistantId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between w-full gap-3">
|
<div class="flex items-center justify-between w-full gap-3">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -0,0 +1,143 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||||
|
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'createAssistant']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||||
|
|
||||||
|
const currentAssistantId = computed(() => route.params.assistantId);
|
||||||
|
|
||||||
|
const isAssistantActive = assistant => {
|
||||||
|
return assistant.id === Number(currentAssistantId.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataForRoute = async (routeName, assistantId) => {
|
||||||
|
const dataFetchMap = {
|
||||||
|
captain_assistants_responses_index: async () => {
|
||||||
|
await store.dispatch('captainResponses/get', { assistantId });
|
||||||
|
await store.dispatch('captainResponses/fetchPendingCount', assistantId);
|
||||||
|
},
|
||||||
|
captain_assistants_responses_pending: async () => {
|
||||||
|
await store.dispatch('captainResponses/get', {
|
||||||
|
assistantId,
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
captain_assistants_documents_index: async () => {
|
||||||
|
await store.dispatch('captainDocuments/get', { assistantId });
|
||||||
|
},
|
||||||
|
captain_assistants_scenarios_index: async () => {
|
||||||
|
await store.dispatch('captainScenarios/get', { assistantId });
|
||||||
|
},
|
||||||
|
captain_assistants_playground_index: () => {
|
||||||
|
// Playground doesn't need pre-fetching, it loads on interaction
|
||||||
|
},
|
||||||
|
captain_assistants_inboxes_index: async () => {
|
||||||
|
await store.dispatch('captainInboxes/get', { assistantId });
|
||||||
|
},
|
||||||
|
captain_tools_index: async () => {
|
||||||
|
await store.dispatch('captainCustomTools/get', { page: 1 });
|
||||||
|
},
|
||||||
|
captain_assistants_settings_index: async () => {
|
||||||
|
await store.dispatch('captainAssistants/show', assistantId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFn = dataFetchMap[routeName];
|
||||||
|
if (fetchFn) {
|
||||||
|
await fetchFn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssistantChange = async assistant => {
|
||||||
|
if (isAssistantActive(assistant)) return;
|
||||||
|
|
||||||
|
const currentRouteName = route.name;
|
||||||
|
const targetRouteName =
|
||||||
|
currentRouteName || 'captain_assistants_responses_index';
|
||||||
|
|
||||||
|
await fetchDataForRoute(targetRouteName, assistant.id);
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
name: targetRouteName,
|
||||||
|
params: {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
assistantId: assistant.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateAssistantDialog = () => {
|
||||||
|
emit('createAssistant');
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="pt-5 pb-3 bg-n-alpha-3 backdrop-blur-[100px] outline outline-n-container outline-1 z-50 absolute w-[27.5rem] rounded-xl shadow-md flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-4 px-6 pb-3 border-b border-n-alpha-2"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h2
|
||||||
|
class="text-base font-medium cursor-pointer text-n-slate-12 w-fit hover:underline"
|
||||||
|
>
|
||||||
|
{{ t('CAPTAIN.ASSISTANT_SWITCHER.ASSISTANTS') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-n-slate-11">
|
||||||
|
{{ t('CAPTAIN.ASSISTANT_SWITCHER.SWITCH_ASSISTANT') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
:label="t('CAPTAIN.ASSISTANT_SWITCHER.NEW_ASSISTANT')"
|
||||||
|
color="slate"
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
size="sm"
|
||||||
|
class="!bg-n-alpha-2 hover:!bg-n-alpha-3"
|
||||||
|
@click="openCreateAssistantDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="assistants.length > 0" class="flex flex-col gap-2 px-4">
|
||||||
|
<Button
|
||||||
|
v-for="assistant in assistants"
|
||||||
|
:key="assistant.id"
|
||||||
|
:label="assistant.name"
|
||||||
|
variant="ghost"
|
||||||
|
color="slate"
|
||||||
|
trailing-icon
|
||||||
|
:icon="isAssistantActive(assistant) ? 'i-lucide-check' : ''"
|
||||||
|
class="!justify-end !px-2 !py-2 hover:!bg-n-alpha-2 [&>.i-lucide-check]:text-n-teal-10 h-9"
|
||||||
|
size="sm"
|
||||||
|
@click="handleAssistantChange(assistant)"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium truncate text-n-slate-12">
|
||||||
|
{{ assistant.name || '' }}
|
||||||
|
</span>
|
||||||
|
<Avatar
|
||||||
|
v-if="assistant"
|
||||||
|
:name="assistant.name"
|
||||||
|
:size="20"
|
||||||
|
icon-name="i-lucide-bot"
|
||||||
|
rounded-full
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -78,8 +78,10 @@ const handleSuggestion = opt => {
|
|||||||
</p>
|
</p>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'captain_assistants_index',
|
name: 'captain_assistants_create_index',
|
||||||
params: { accountId: route.params.accountId },
|
params: {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
},
|
||||||
}"
|
}"
|
||||||
class="text-n-slate-11 underline hover:text-n-slate-12"
|
class="text-n-slate-11 underline hover:text-n-slate-12"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -221,26 +221,70 @@ const menuItems = computed(() => {
|
|||||||
name: 'Captain',
|
name: 'Captain',
|
||||||
icon: 'i-woot-captain',
|
icon: 'i-woot-captain',
|
||||||
label: t('SIDEBAR.CAPTAIN'),
|
label: t('SIDEBAR.CAPTAIN'),
|
||||||
|
activeOn: ['captain_assistants_create_index'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'Assistants',
|
name: 'FAQs',
|
||||||
label: t('SIDEBAR.CAPTAIN_ASSISTANTS'),
|
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
|
||||||
to: accountScopedRoute('captain_assistants_index'),
|
activeOn: [
|
||||||
|
'captain_assistants_responses_index',
|
||||||
|
'captain_assistants_responses_pending',
|
||||||
|
],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_assistants_responses_index',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Documents',
|
name: 'Documents',
|
||||||
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
|
label: t('SIDEBAR.CAPTAIN_DOCUMENTS'),
|
||||||
to: accountScopedRoute('captain_documents_index'),
|
activeOn: ['captain_assistants_documents_index'],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_assistants_documents_index',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Responses',
|
name: 'Scenarios',
|
||||||
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
|
label: t('SIDEBAR.CAPTAIN_SCENARIOS'),
|
||||||
to: accountScopedRoute('captain_responses_index'),
|
activeOn: ['captain_assistants_scenarios_index'],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_assistants_scenarios_index',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Playground',
|
||||||
|
label: t('SIDEBAR.CAPTAIN_PLAYGROUND'),
|
||||||
|
activeOn: ['captain_assistants_playground_index'],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_assistants_playground_index',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Inboxes',
|
||||||
|
label: t('SIDEBAR.CAPTAIN_INBOXES'),
|
||||||
|
activeOn: ['captain_assistants_inboxes_index'],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_assistants_inboxes_index',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Tools',
|
name: 'Tools',
|
||||||
label: t('SIDEBAR.CAPTAIN_TOOLS'),
|
label: t('SIDEBAR.CAPTAIN_TOOLS'),
|
||||||
to: accountScopedRoute('captain_tools_index'),
|
activeOn: ['captain_tools_index'],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_tools_index',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Settings',
|
||||||
|
label: t('SIDEBAR.CAPTAIN_SETTINGS'),
|
||||||
|
activeOn: [
|
||||||
|
'captain_assistants_settings_index',
|
||||||
|
'captain_assistants_guidelines_index',
|
||||||
|
'captain_assistants_guardrails_index',
|
||||||
|
],
|
||||||
|
to: accountScopedRoute('captain_assistants_index', {
|
||||||
|
navigationPath: 'captain_assistants_settings_index',
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,19 +19,46 @@ export function useSidebarContext() {
|
|||||||
return '/';
|
return '/';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to find route definition by name without resolving
|
||||||
|
const findRouteByName = name => {
|
||||||
|
const routes = router.getRoutes();
|
||||||
|
return routes.find(route => route.name === name);
|
||||||
|
};
|
||||||
|
|
||||||
const resolvePermissions = to => {
|
const resolvePermissions = to => {
|
||||||
if (to) return router.resolve(to)?.meta?.permissions ?? [];
|
if (!to) return [];
|
||||||
return [];
|
|
||||||
|
// If navigationPath param exists, get the target route definition
|
||||||
|
if (to.params?.navigationPath) {
|
||||||
|
const targetRoute = findRouteByName(to.params.navigationPath);
|
||||||
|
return targetRoute?.meta?.permissions ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.resolve(to)?.meta?.permissions ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveFeatureFlag = to => {
|
const resolveFeatureFlag = to => {
|
||||||
if (to) return router.resolve(to)?.meta?.featureFlag || '';
|
if (!to) return '';
|
||||||
return '';
|
|
||||||
|
// If navigationPath param exists, get the target route definition
|
||||||
|
if (to.params?.navigationPath) {
|
||||||
|
const targetRoute = findRouteByName(to.params.navigationPath);
|
||||||
|
return targetRoute?.meta?.featureFlag || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.resolve(to)?.meta?.featureFlag || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveInstallationType = to => {
|
const resolveInstallationType = to => {
|
||||||
if (to) return router.resolve(to)?.meta?.installationTypes || [];
|
if (!to) return [];
|
||||||
return [];
|
|
||||||
|
// If navigationPath param exists, get the target route definition
|
||||||
|
if (to.params?.navigationPath) {
|
||||||
|
const targetRoute = findRouteByName(to.params.navigationPath);
|
||||||
|
return targetRoute?.meta?.installationTypes || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.resolve(to)?.meta?.installationTypes || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAllowed = to => {
|
const isAllowed = to => {
|
||||||
|
|||||||
@ -341,6 +341,11 @@
|
|||||||
"CAPTAIN": {
|
"CAPTAIN": {
|
||||||
"NAME": "Captain",
|
"NAME": "Captain",
|
||||||
"HEADER_KNOW_MORE": "Know more",
|
"HEADER_KNOW_MORE": "Know more",
|
||||||
|
"ASSISTANT_SWITCHER": {
|
||||||
|
"ASSISTANTS": "Assistants",
|
||||||
|
"SWITCH_ASSISTANT": "Switch between assistants",
|
||||||
|
"NEW_ASSISTANT": "Create Assistant"
|
||||||
|
},
|
||||||
"COPILOT": {
|
"COPILOT": {
|
||||||
"TITLE": "Copilot",
|
"TITLE": "Copilot",
|
||||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
"TRY_THESE_PROMPTS": "Try these prompts",
|
||||||
@ -480,9 +485,7 @@
|
|||||||
"NOT_FOUND": "Could not find the assistant. Please try again."
|
"NOT_FOUND": "Could not find the assistant. Please try again."
|
||||||
},
|
},
|
||||||
"SETTINGS": {
|
"SETTINGS": {
|
||||||
"BREADCRUMB": {
|
"HEADER": "Settings",
|
||||||
"ASSISTANT": "Assistant"
|
|
||||||
},
|
|
||||||
"BASIC_SETTINGS": {
|
"BASIC_SETTINGS": {
|
||||||
"TITLE": "Basic settings",
|
"TITLE": "Basic settings",
|
||||||
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
"DESCRIPTION": "Customize what the assistant says when ending a conversation or transferring to a human."
|
||||||
@ -499,15 +502,16 @@
|
|||||||
"TITLE": "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."
|
"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": {
|
"RESPONSE_GUIDELINES": {
|
||||||
"TITLE": "Response guidelines",
|
"TITLE": "Response guidelines",
|
||||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"TITLE": "Delete Assistant",
|
||||||
|
"DESCRIPTION": "This action is permanent. Deleting this assistant will remove it from all connected inboxes and permanently erase all generated knowledge.",
|
||||||
|
"BUTTON_TEXT": "Delete {assistantName}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
@ -526,9 +530,6 @@
|
|||||||
"GUARDRAILS": {
|
"GUARDRAILS": {
|
||||||
"TITLE": "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.",
|
"DESCRIPTION": "Keeps things on track—only the kinds of questions you want your assistant to answer, nothing off-limits or off-topic.",
|
||||||
"BREADCRUMB": {
|
|
||||||
"TITLE": "Guardrails"
|
|
||||||
},
|
|
||||||
"BULK_ACTION": {
|
"BULK_ACTION": {
|
||||||
"SELECTED": "{count} item selected | {count} items selected",
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
"SELECT_ALL": "Select all ({count})",
|
"SELECT_ALL": "Select all ({count})",
|
||||||
@ -574,9 +575,6 @@
|
|||||||
"RESPONSE_GUIDELINES": {
|
"RESPONSE_GUIDELINES": {
|
||||||
"TITLE": "Response Guidelines",
|
"TITLE": "Response Guidelines",
|
||||||
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
"DESCRIPTION": "The vibe and structure of your assistant’s replies—clear and friendly? Short and snappy? Detailed and formal?",
|
||||||
"BREADCRUMB": {
|
|
||||||
"TITLE": "Response Guidelines"
|
|
||||||
},
|
|
||||||
"BULK_ACTION": {
|
"BULK_ACTION": {
|
||||||
"SELECTED": "{count} item selected | {count} items selected",
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
"SELECT_ALL": "Select all ({count})",
|
"SELECT_ALL": "Select all ({count})",
|
||||||
@ -622,9 +620,6 @@
|
|||||||
"SCENARIOS": {
|
"SCENARIOS": {
|
||||||
"TITLE": "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.”",
|
"DESCRIPTION": "Give your assistant some context—like “what to do when a user is stuck,” or “how to act during a refund request.”",
|
||||||
"BREADCRUMB": {
|
|
||||||
"TITLE": "Scenarios"
|
|
||||||
},
|
|
||||||
"BULK_ACTION": {
|
"BULK_ACTION": {
|
||||||
"SELECTED": "{count} item selected | {count} items selected",
|
"SELECTED": "{count} item selected | {count} items selected",
|
||||||
"SELECT_ALL": "Select all ({count})",
|
"SELECT_ALL": "Select all ({count})",
|
||||||
@ -722,11 +717,6 @@
|
|||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "Document Name (Optional)",
|
"LABEL": "Document Name (Optional)",
|
||||||
"PLACEHOLDER": "Enter a name for the document"
|
"PLACEHOLDER": "Enter a name for the document"
|
||||||
},
|
|
||||||
"ASSISTANT": {
|
|
||||||
"LABEL": "Assistant",
|
|
||||||
"PLACEHOLDER": "Select the assistant",
|
|
||||||
"ERROR": "The assistant field is required"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
@ -921,12 +911,6 @@
|
|||||||
"LABEL": "Answer",
|
"LABEL": "Answer",
|
||||||
"PLACEHOLDER": "Enter the answer here",
|
"PLACEHOLDER": "Enter the answer here",
|
||||||
"ERROR": "Please provide a valid answer."
|
"ERROR": "Please provide a valid answer."
|
||||||
},
|
|
||||||
|
|
||||||
"ASSISTANT": {
|
|
||||||
"LABEL": "Assistant",
|
|
||||||
"PLACEHOLDER": "Select an assistant",
|
|
||||||
"ERROR": "Please select an assistant."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT": {
|
"EDIT": {
|
||||||
|
|||||||
@ -305,6 +305,10 @@
|
|||||||
"CAPTAIN_DOCUMENTS": "Documents",
|
"CAPTAIN_DOCUMENTS": "Documents",
|
||||||
"CAPTAIN_RESPONSES": "FAQs",
|
"CAPTAIN_RESPONSES": "FAQs",
|
||||||
"CAPTAIN_TOOLS": "Tools",
|
"CAPTAIN_TOOLS": "Tools",
|
||||||
|
"CAPTAIN_SCENARIOS": "Scenarios",
|
||||||
|
"CAPTAIN_PLAYGROUND": "Playground",
|
||||||
|
"CAPTAIN_INBOXES": "Inboxes",
|
||||||
|
"CAPTAIN_SETTINGS": "Settings",
|
||||||
"HOME": "Home",
|
"HOME": "Home",
|
||||||
"AGENTS": "Agents",
|
"AGENTS": "Agents",
|
||||||
"AGENT_BOTS": "Bots",
|
"AGENT_BOTS": "Bots",
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, onMounted } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useStore } from 'dashboard/composables/store';
|
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
|
||||||
import EditAssistantForm from '../../../../components-next/captain/pageComponents/assistant/EditAssistantForm.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 store = useStore();
|
|
||||||
const { t } = useI18n();
|
|
||||||
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 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 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 || !isCaptainV2Enabled) {
|
|
||||||
store.dispatch('captainAssistants/show', assistantId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AssistantSettings v-if="isCaptainV2Enabled" />
|
|
||||||
<PageLayout
|
|
||||||
v-else
|
|
||||||
:header-title="assistant?.name"
|
|
||||||
:show-pagination-footer="false"
|
|
||||||
:is-fetching="isFetching"
|
|
||||||
:show-know-more="false"
|
|
||||||
:back-url="{ name: 'captain_assistants_index' }"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<div v-if="!isAssistantAvailable">
|
|
||||||
{{ t('CAPTAIN.ASSISTANTS.EDIT.NOT_FOUND') }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex gap-4 h-full">
|
|
||||||
<div class="flex-1 lg:overflow-auto pr-4 h-full md:h-auto">
|
|
||||||
<EditAssistantForm
|
|
||||||
:assistant="assistant"
|
|
||||||
mode="edit"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="w-[400px] hidden lg:block h-full">
|
|
||||||
<AssistantPlayground :assistant-id="Number(assistantId)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</PageLayout>
|
|
||||||
</template>
|
|
||||||
@ -1,91 +1,57 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
import { computed, ref, nextTick } from 'vue';
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
|
||||||
import AssistantCard from 'dashboard/components-next/captain/assistant/AssistantCard.vue';
|
|
||||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
||||||
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
|
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
|
||||||
import AssistantPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue';
|
import AssistantPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.vue';
|
||||||
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
|
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
|
||||||
import LimitBanner from 'dashboard/components-next/captain/pageComponents/response/LimitBanner.vue';
|
|
||||||
|
|
||||||
const { isOnChatwootCloud } = useAccount();
|
const { isOnChatwootCloud } = useAccount();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
const dialogType = ref('');
|
const dialogType = ref('');
|
||||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||||
|
|
||||||
const selectedAssistant = ref(null);
|
const selectedAssistant = ref(null);
|
||||||
const deleteAssistantDialog = ref(null);
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
deleteAssistantDialog.value.dialogRef.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createAssistantDialog = ref(null);
|
const createAssistantDialog = ref(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
dialogType.value = 'create';
|
dialogType.value = 'create';
|
||||||
nextTick(() => createAssistantDialog.value.dialogRef.open());
|
nextTick(() => createAssistantDialog.value.dialogRef.open());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
router.push({
|
|
||||||
name: 'captain_assistants_edit',
|
|
||||||
params: { assistantId: selectedAssistant.value.id },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewConnectedInboxes = () => {
|
|
||||||
router.push({
|
|
||||||
name: 'captain_assistants_inboxes_index',
|
|
||||||
params: { assistantId: selectedAssistant.value.id },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAction = ({ action, id }) => {
|
|
||||||
selectedAssistant.value = assistants.value.find(
|
|
||||||
assistant => id === assistant.id
|
|
||||||
);
|
|
||||||
nextTick(() => {
|
|
||||||
if (action === 'delete') {
|
|
||||||
handleDelete();
|
|
||||||
}
|
|
||||||
if (action === 'edit') {
|
|
||||||
handleEdit();
|
|
||||||
}
|
|
||||||
if (action === 'viewConnectedInboxes') {
|
|
||||||
handleViewConnectedInboxes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateClose = () => {
|
const handleCreateClose = () => {
|
||||||
dialogType.value = '';
|
dialogType.value = '';
|
||||||
selectedAssistant.value = null;
|
selectedAssistant.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => store.dispatch('captainAssistants/get'));
|
const handleAfterCreate = newAssistant => {
|
||||||
|
// Navigate directly to documents page with the new assistant ID
|
||||||
|
if (newAssistant?.id) {
|
||||||
|
router.push({
|
||||||
|
name: 'captain_assistants_responses_index',
|
||||||
|
params: {
|
||||||
|
accountId: router.currentRoute.value.params.accountId,
|
||||||
|
assistantId: newAssistant.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageLayout
|
<PageLayout
|
||||||
:header-title="$t('CAPTAIN.ASSISTANTS.HEADER')"
|
:header-title="$t('CAPTAIN.ASSISTANTS.HEADER')"
|
||||||
:button-label="$t('CAPTAIN.ASSISTANTS.ADD_NEW')"
|
|
||||||
:button-policy="['administrator']"
|
|
||||||
:show-pagination-footer="false"
|
:show-pagination-footer="false"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
:is-empty="!assistants.length"
|
|
||||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||||
|
is-empty
|
||||||
@click="handleCreate"
|
@click="handleCreate"
|
||||||
>
|
>
|
||||||
<template #knowMore>
|
<template #knowMore>
|
||||||
@ -107,36 +73,13 @@ onMounted(() => store.dispatch('captainAssistants/get'));
|
|||||||
<CaptainPaywall />
|
<CaptainPaywall />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LimitBanner class="mb-5" />
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<AssistantCard
|
|
||||||
v-for="assistant in assistants"
|
|
||||||
:id="assistant.id"
|
|
||||||
:key="assistant.id"
|
|
||||||
:name="assistant.name"
|
|
||||||
:description="assistant.description"
|
|
||||||
:updated-at="assistant.updated_at || assistant.created_at"
|
|
||||||
:created-at="assistant.created_at"
|
|
||||||
@action="handleAction"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<DeleteDialog
|
|
||||||
v-if="selectedAssistant"
|
|
||||||
ref="deleteAssistantDialog"
|
|
||||||
:entity="selectedAssistant"
|
|
||||||
type="Assistants"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateAssistantDialog
|
<CreateAssistantDialog
|
||||||
v-if="dialogType"
|
v-if="dialogType"
|
||||||
ref="createAssistantDialog"
|
ref="createAssistantDialog"
|
||||||
:type="dialogType"
|
:type="dialogType"
|
||||||
:selected-assistant="selectedAssistant"
|
:selected-assistant="selectedAssistant"
|
||||||
@close="handleCreateClose"
|
@close="handleCreateClose"
|
||||||
|
@created="handleAfterCreate"
|
||||||
/>
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
|||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
||||||
import SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
import SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||||
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
|
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
|
||||||
@ -23,30 +23,27 @@ const route = useRoute();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
const assistantId = route.params.assistantId;
|
|
||||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
|
const assistantId = computed(() => Number(route.params.assistantId));
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||||
const assistant = computed(() =>
|
const assistant = computed(() =>
|
||||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
store.getters['captainAssistants/getRecord'](assistantId.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const newInlineRule = ref('');
|
const newInlineRule = ref('');
|
||||||
const newDialogRule = ref('');
|
const newDialogRule = ref('');
|
||||||
|
|
||||||
const breadcrumbItems = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
|
|
||||||
routeName: 'captain_assistants_index',
|
|
||||||
},
|
|
||||||
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
|
|
||||||
{ label: t('CAPTAIN.ASSISTANTS.GUARDRAILS.BREADCRUMB.TITLE') },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const guardrailsContent = computed(() => assistant.value?.guardrails || []);
|
const guardrailsContent = computed(() => assistant.value?.guardrails || []);
|
||||||
|
|
||||||
|
const backUrl = computed(() => ({
|
||||||
|
name: 'captain_assistants_settings_index',
|
||||||
|
params: {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
assistantId: assistantId.value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const displayGuardrails = computed(() =>
|
const displayGuardrails = computed(() =>
|
||||||
guardrailsContent.value.map((c, idx) => ({ id: idx, content: c }))
|
guardrailsContent.value.map((c, idx) => ({ id: idx, content: c }))
|
||||||
);
|
);
|
||||||
@ -113,7 +110,7 @@ const selectedCountLabel = computed(() => {
|
|||||||
|
|
||||||
const saveGuardrails = async list => {
|
const saveGuardrails = async list => {
|
||||||
await store.dispatch('captainAssistants/update', {
|
await store.dispatch('captainAssistants/update', {
|
||||||
id: assistantId,
|
id: assistantId.value,
|
||||||
assistant: { guardrails: list },
|
assistant: { guardrails: list },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -176,9 +173,13 @@ const addAllExample = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SettingsPageLayout
|
<PageLayout
|
||||||
:breadcrumb-items="breadcrumbItems"
|
:header-title="$t('CAPTAIN.ASSISTANTS.GUARDRAILS.TITLE')"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
|
:back-url="backUrl"
|
||||||
|
:show-know-more="false"
|
||||||
|
:show-pagination-footer="false"
|
||||||
|
:show-assistant-switcher="false"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
@ -297,5 +298,5 @@ const addAllExample = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsPageLayout>
|
</PageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
|||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
||||||
import SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
import SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||||
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
|
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
|
||||||
@ -23,32 +23,29 @@ const route = useRoute();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
const assistantId = route.params.assistantId;
|
|
||||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
|
const assistantId = computed(() => Number(route.params.assistantId));
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||||
const assistant = computed(() =>
|
const assistant = computed(() =>
|
||||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
store.getters['captainAssistants/getRecord'](assistantId.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const newInlineRule = ref('');
|
const newInlineRule = ref('');
|
||||||
const newDialogRule = ref('');
|
const newDialogRule = ref('');
|
||||||
|
|
||||||
const breadcrumbItems = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
|
|
||||||
routeName: 'captain_assistants_index',
|
|
||||||
},
|
|
||||||
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
|
|
||||||
{ label: t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.TITLE') },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const guidelinesContent = computed(
|
const guidelinesContent = computed(
|
||||||
() => assistant.value?.response_guidelines || []
|
() => assistant.value?.response_guidelines || []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const backUrl = computed(() => ({
|
||||||
|
name: 'captain_assistants_settings_index',
|
||||||
|
params: {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
assistantId: assistantId.value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const displayGuidelines = computed(() =>
|
const displayGuidelines = computed(() =>
|
||||||
guidelinesContent.value.map((c, idx) => ({ id: idx, content: c }))
|
guidelinesContent.value.map((c, idx) => ({ id: idx, content: c }))
|
||||||
);
|
);
|
||||||
@ -119,7 +116,7 @@ const selectedCountLabel = computed(() => {
|
|||||||
|
|
||||||
const saveGuidelines = async list => {
|
const saveGuidelines = async list => {
|
||||||
await store.dispatch('captainAssistants/update', {
|
await store.dispatch('captainAssistants/update', {
|
||||||
id: assistantId,
|
id: assistantId.value,
|
||||||
assistant: { response_guidelines: list },
|
assistant: { response_guidelines: list },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -183,9 +180,13 @@ const addAllExample = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SettingsPageLayout
|
<PageLayout
|
||||||
:breadcrumb-items="breadcrumbItems"
|
:header-title="$t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.TITLE')"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
|
:back-url="backUrl"
|
||||||
|
:show-know-more="false"
|
||||||
|
:show-pagination-footer="false"
|
||||||
|
:show-assistant-switcher="false"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
@ -321,5 +322,5 @@ const addAllExample = async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsPageLayout>
|
</PageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeMount, onMounted, ref, nextTick } from 'vue';
|
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||||
import {
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
useMapGetter,
|
|
||||||
useStore,
|
|
||||||
useStoreGetters,
|
|
||||||
} from 'dashboard/composables/store';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
import BackButton from 'dashboard/components/widgets/BackButton.vue';
|
|
||||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import ConnectInboxDialog from 'dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue';
|
import ConnectInboxDialog from 'dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue';
|
||||||
@ -52,13 +47,7 @@ const handleCreateClose = () => {
|
|||||||
selectedInbox.value = null;
|
selectedInbox.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getters = useStoreGetters();
|
|
||||||
const assistantId = Number(route.params.assistantId);
|
const assistantId = Number(route.params.assistantId);
|
||||||
const assistant = computed(() =>
|
|
||||||
getters['captainAssistants/getRecord'].value(assistantId)
|
|
||||||
);
|
|
||||||
onBeforeMount(() => store.dispatch('captainAssistants/show', assistantId));
|
|
||||||
|
|
||||||
onMounted(() =>
|
onMounted(() =>
|
||||||
store.dispatch('captainInboxes/get', {
|
store.dispatch('captainInboxes/get', {
|
||||||
assistantId: assistantId,
|
assistantId: assistantId,
|
||||||
@ -68,27 +57,16 @@ onMounted(() =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageLayout
|
<PageLayout
|
||||||
|
:header-title="$t('CAPTAIN.INBOXES.HEADER')"
|
||||||
:button-label="$t('CAPTAIN.INBOXES.ADD_NEW')"
|
:button-label="$t('CAPTAIN.INBOXES.ADD_NEW')"
|
||||||
:button-policy="['administrator']"
|
:button-policy="['administrator']"
|
||||||
:is-fetching="isFetchingAssistant || isFetching"
|
:is-fetching="isFetchingAssistant || isFetching"
|
||||||
:is-empty="!captainInboxes.length"
|
:is-empty="!captainInboxes.length"
|
||||||
:show-pagination-footer="false"
|
:show-pagination-footer="false"
|
||||||
|
:show-know-more="false"
|
||||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||||
@click="handleCreate"
|
@click="handleCreate"
|
||||||
>
|
>
|
||||||
<template v-if="!isFetchingAssistant" #headerTitle>
|
|
||||||
<div class="flex flex-row items-center gap-4">
|
|
||||||
<BackButton compact />
|
|
||||||
<span
|
|
||||||
class="flex items-center gap-1 text-lg font-medium text-n-slate-12"
|
|
||||||
>
|
|
||||||
{{ assistant.name }}
|
|
||||||
<span class="i-lucide-chevron-right text-xl text-n-slate-10" />
|
|
||||||
{{ $t('CAPTAIN.INBOXES.HEADER') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #emptyState>
|
<template #emptyState>
|
||||||
<InboxPageEmptyState @click="handleCreate" />
|
<InboxPageEmptyState @click="handleCreate" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
|
import AssistantPlayground from 'dashboard/components-next/captain/assistant/AssistantPlayground.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const assistantId = computed(() => Number(route.params.assistantId));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageLayout
|
||||||
|
:header-title="$t('CAPTAIN.PLAYGROUND.HEADER')"
|
||||||
|
show-assistant-switcher
|
||||||
|
:show-pagination-footer="false"
|
||||||
|
class="h-full"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<AssistantPlayground :assistant-id="assistantId" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageLayout>
|
||||||
|
</template>
|
||||||
@ -10,7 +10,7 @@ import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
|||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
|
||||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.vue';
|
||||||
import SuggestedScenarios from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
import SuggestedScenarios from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||||
import ScenariosCard from 'dashboard/components-next/captain/assistant/ScenariosCard.vue';
|
import ScenariosCard from 'dashboard/components-next/captain/assistant/ScenariosCard.vue';
|
||||||
@ -22,28 +22,14 @@ const route = useRoute();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
const { formatMessage } = useMessageFormatter();
|
const { formatMessage } = useMessageFormatter();
|
||||||
const assistantId = route.params.assistantId;
|
const assistantId = computed(() => Number(route.params.assistantId));
|
||||||
|
|
||||||
const uiFlags = useMapGetter('captainScenarios/getUIFlags');
|
const uiFlags = useMapGetter('captainScenarios/getUIFlags');
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||||
const assistant = computed(() =>
|
|
||||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
|
||||||
);
|
|
||||||
const scenarios = useMapGetter('captainScenarios/getRecords');
|
const scenarios = useMapGetter('captainScenarios/getRecords');
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
|
|
||||||
const breadcrumbItems = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: t('CAPTAIN.ASSISTANTS.SETTINGS.BREADCRUMB.ASSISTANT'),
|
|
||||||
routeName: 'captain_assistants_index',
|
|
||||||
},
|
|
||||||
{ label: assistant.value?.name, routeName: 'captain_assistants_edit' },
|
|
||||||
{ label: t('CAPTAIN.ASSISTANTS.SCENARIOS.BREADCRUMB.TITLE') },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const LINK_INSTRUCTION_CLASS =
|
const LINK_INSTRUCTION_CLASS =
|
||||||
'[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
|
'[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
|
||||||
|
|
||||||
@ -119,7 +105,7 @@ const updateScenario = async scenario => {
|
|||||||
try {
|
try {
|
||||||
await store.dispatch('captainScenarios/update', {
|
await store.dispatch('captainScenarios/update', {
|
||||||
id: scenario.id,
|
id: scenario.id,
|
||||||
assistantId: route.params.assistantId,
|
assistantId: assistantId.value,
|
||||||
...scenario,
|
...scenario,
|
||||||
tools: getToolsFromInstruction(scenario.instruction),
|
tools: getToolsFromInstruction(scenario.instruction),
|
||||||
});
|
});
|
||||||
@ -136,7 +122,7 @@ const deleteScenario = async id => {
|
|||||||
try {
|
try {
|
||||||
await store.dispatch('captainScenarios/delete', {
|
await store.dispatch('captainScenarios/delete', {
|
||||||
id,
|
id,
|
||||||
assistantId: route.params.assistantId,
|
assistantId: assistantId.value,
|
||||||
});
|
});
|
||||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
|
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -154,7 +140,7 @@ const bulkDeleteScenarios = async ids => {
|
|||||||
idsArray.map(id =>
|
idsArray.map(id =>
|
||||||
store.dispatch('captainScenarios/delete', {
|
store.dispatch('captainScenarios/delete', {
|
||||||
id,
|
id,
|
||||||
assistantId: route.params.assistantId,
|
assistantId: assistantId.value,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -165,7 +151,7 @@ const bulkDeleteScenarios = async ids => {
|
|||||||
const addScenario = async scenario => {
|
const addScenario = async scenario => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainScenarios/create', {
|
await store.dispatch('captainScenarios/create', {
|
||||||
assistantId: route.params.assistantId,
|
assistantId: assistantId.value,
|
||||||
...scenario,
|
...scenario,
|
||||||
tools: getToolsFromInstruction(scenario.instruction),
|
tools: getToolsFromInstruction(scenario.instruction),
|
||||||
});
|
});
|
||||||
@ -182,7 +168,7 @@ const addAllExampleScenarios = async () => {
|
|||||||
try {
|
try {
|
||||||
scenariosExample.forEach(async scenario => {
|
scenariosExample.forEach(async scenario => {
|
||||||
await store.dispatch('captainScenarios/create', {
|
await store.dispatch('captainScenarios/create', {
|
||||||
assistantId: route.params.assistantId,
|
assistantId: assistantId.value,
|
||||||
...scenario,
|
...scenario,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -197,16 +183,18 @@ const addAllExampleScenarios = async () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch('captainScenarios/get', {
|
store.dispatch('captainScenarios/get', {
|
||||||
assistantId: assistantId,
|
assistantId: assistantId.value,
|
||||||
});
|
});
|
||||||
store.dispatch('captainTools/getTools');
|
store.dispatch('captainTools/getTools');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SettingsPageLayout
|
<PageLayout
|
||||||
:breadcrumb-items="breadcrumbItems"
|
:header-title="$t('CAPTAIN.DOCUMENTS.HEADER')"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
|
:show-know-more="false"
|
||||||
|
:show-pagination-footer="false"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
@ -310,5 +298,5 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SettingsPageLayout>
|
</PageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,27 +1,39 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useStore } from 'dashboard/composables/store';
|
import { useStore } from 'dashboard/composables/store';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import SettingsHeader from 'dashboard/components-next/captain/pageComponents/settings/SettingsHeader.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 AssistantBasicSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue';
|
||||||
import AssistantSystemSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.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';
|
import AssistantControlItems from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue';
|
||||||
|
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const { isCloudFeatureEnabled } = useAccount();
|
||||||
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 isCaptainV2Enabled = computed(() =>
|
||||||
|
isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_V2)
|
||||||
|
);
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const deleteAssistantDialog = ref(null);
|
||||||
|
|
||||||
|
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||||
|
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||||
|
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||||
|
const assistantId = computed(() => Number(route.params.assistantId));
|
||||||
|
const assistant = computed(() =>
|
||||||
|
store.getters['captainAssistants/getRecord'](assistantId.value)
|
||||||
|
);
|
||||||
|
|
||||||
const controlItems = computed(() => {
|
const controlItems = computed(() => {
|
||||||
return [
|
return [
|
||||||
@ -34,15 +46,6 @@ const controlItems = computed(() => {
|
|||||||
),
|
),
|
||||||
routeName: 'captain_assistants_guardrails_index',
|
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(
|
name: t(
|
||||||
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.TITLE'
|
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.TITLE'
|
||||||
@ -55,32 +58,10 @@ const controlItems = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
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 => {
|
const handleSubmit = async updatedAssistant => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainAssistants/update', {
|
await store.dispatch('captainAssistants/update', {
|
||||||
id: assistantId,
|
id: assistantId.value,
|
||||||
...updatedAssistant,
|
...updatedAssistant,
|
||||||
});
|
});
|
||||||
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
|
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
|
||||||
@ -91,64 +72,126 @@ const handleSubmit = async updatedAssistant => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
const handleDelete = () => {
|
||||||
if (!isAssistantAvailable.value) {
|
deleteAssistantDialog.value.dialogRef.open();
|
||||||
store.dispatch('captainAssistants/show', assistantId);
|
};
|
||||||
|
|
||||||
|
const handleDeleteSuccess = () => {
|
||||||
|
// Get remaining assistants after deletion
|
||||||
|
const remainingAssistants = assistants.value.filter(
|
||||||
|
a => a.id !== assistantId.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remainingAssistants.length > 0) {
|
||||||
|
// Navigate to the first available assistant's settings
|
||||||
|
const nextAssistant = remainingAssistants[0];
|
||||||
|
router.push({
|
||||||
|
name: 'captain_assistants_settings_index',
|
||||||
|
params: {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
assistantId: nextAssistant.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No assistants left, redirect to create assistant page
|
||||||
|
router.push({
|
||||||
|
name: 'captain_assistants_create_index',
|
||||||
|
params: { accountId: route.params.accountId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SettingsPageLayout
|
<PageLayout
|
||||||
:breadcrumb-items="breadcrumbItems"
|
:header-title="$t('CAPTAIN.ASSISTANTS.SETTINGS.HEADER')"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
class="[&>div]:max-w-[80rem]"
|
:show-pagination-footer="false"
|
||||||
|
:show-know-more="false"
|
||||||
|
:class="{
|
||||||
|
'[&>header>div]:max-w-[80rem] [&>main>div]:max-w-[80rem]':
|
||||||
|
isCaptainV2Enabled,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col gap-6">
|
<div
|
||||||
|
class="gap-6 lg:gap-16 pb-8"
|
||||||
|
:class="{ 'grid grid-cols-2': isCaptainV2Enabled }"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<SettingsHeader
|
<div class="flex flex-col gap-6">
|
||||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
|
<SettingsHeader
|
||||||
:description="
|
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
|
||||||
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
|
:description="
|
||||||
"
|
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
|
||||||
/>
|
"
|
||||||
<AssistantBasicSettingsForm
|
/>
|
||||||
:assistant="assistant"
|
<AssistantBasicSettingsForm
|
||||||
@submit="handleSubmit"
|
: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>
|
||||||
|
<span class="h-px w-full bg-n-weak mt-2" />
|
||||||
|
<div class="flex items-end justify-between w-full gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h6 class="text-n-slate-12 text-base font-medium">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.SETTINGS.DELETE.TITLE') }}
|
||||||
|
</h6>
|
||||||
|
<span class="text-n-slate-11 text-sm">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.SETTINGS.DELETE.DESCRIPTION') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
:label="
|
||||||
|
t('CAPTAIN.ASSISTANTS.SETTINGS.DELETE.BUTTON_TEXT', {
|
||||||
|
assistantName: assistant.name,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
color="ruby"
|
||||||
|
class="max-w-56 !w-fit"
|
||||||
|
@click="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="h-px w-full bg-n-weak mt-2" />
|
<div v-if="isCaptainV2Enabled" class="flex flex-col gap-6">
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
<SettingsHeader
|
<SettingsHeader
|
||||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.TITLE')"
|
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
|
||||||
:description="
|
:description="
|
||||||
t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.DESCRIPTION')
|
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<AssistantSystemSettingsForm
|
<div class="flex flex-col gap-6">
|
||||||
:assistant="assistant"
|
<AssistantControlItems
|
||||||
@submit="handleSubmit"
|
v-for="item in controlItems"
|
||||||
/>
|
:key="item.name"
|
||||||
|
:control-item="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #controls>
|
<DeleteDialog
|
||||||
<div class="flex flex-col gap-6">
|
v-if="assistant"
|
||||||
<SettingsHeader
|
ref="deleteAssistantDialog"
|
||||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
|
:entity="assistant"
|
||||||
:description="
|
type="Assistants"
|
||||||
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
|
translation-key="ASSISTANTS"
|
||||||
"
|
@delete-success="handleDeleteSuccess"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-6">
|
</PageLayout>
|
||||||
<AssistantControlItems
|
|
||||||
v-for="item in controlItems"
|
|
||||||
:key="item.name"
|
|
||||||
:control-item="item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</SettingsPageLayout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
import AssistantIndex from './assistants/Index.vue';
|
|
||||||
import AssistantEdit from './assistants/Edit.vue';
|
import CaptainPageRouteView from './pages/CaptainPageRouteView.vue';
|
||||||
// import AssistantSettings from './assistants/settings/Settings.vue';
|
import AssistantsIndexPage from './pages/AssistantsIndexPage.vue';
|
||||||
|
import AssistantEmptyStateIndex from './assistants/Index.vue';
|
||||||
|
|
||||||
|
import AssistantSettingsIndex from './assistants/settings/Settings.vue';
|
||||||
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
import AssistantInboxesIndex from './assistants/inboxes/Index.vue';
|
||||||
|
import AssistantPlaygroundIndex from './assistants/playground/Index.vue';
|
||||||
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
||||||
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
|
import AssistantGuidelinesIndex from './assistants/guidelines/Index.vue';
|
||||||
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
|
import AssistantScenariosIndex from './assistants/scenarios/Index.vue';
|
||||||
@ -13,87 +17,90 @@ import ResponsesIndex from './responses/Index.vue';
|
|||||||
import ResponsesPendingIndex from './responses/Pending.vue';
|
import ResponsesPendingIndex from './responses/Pending.vue';
|
||||||
import CustomToolsIndex from './tools/Index.vue';
|
import CustomToolsIndex from './tools/Index.vue';
|
||||||
|
|
||||||
export const routes = [
|
const meta = {
|
||||||
|
permissions: ['administrator', 'agent'],
|
||||||
|
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||||
|
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||||
|
};
|
||||||
|
|
||||||
|
const metaV2 = {
|
||||||
|
permissions: ['administrator', 'agent'],
|
||||||
|
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
|
||||||
|
installationTypes: [INSTALLATION_TYPES.CLOUD, INSTALLATION_TYPES.ENTERPRISE],
|
||||||
|
};
|
||||||
|
|
||||||
|
const assistantRoutes = [
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/captain/assistants'),
|
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs'),
|
||||||
component: AssistantIndex,
|
component: ResponsesIndex,
|
||||||
name: 'captain_assistants_index',
|
name: 'captain_assistants_responses_index',
|
||||||
meta: {
|
meta,
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/captain/assistants/:assistantId'),
|
path: frontendURL('accounts/:accountId/captain/:assistantId/documents'),
|
||||||
component: AssistantEdit,
|
component: DocumentsIndex,
|
||||||
name: 'captain_assistants_edit',
|
name: 'captain_assistants_documents_index',
|
||||||
meta: {
|
meta,
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL(
|
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
|
||||||
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
|
component: CustomToolsIndex,
|
||||||
),
|
name: 'captain_tools_index',
|
||||||
|
meta: metaV2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/captain/:assistantId/scenarios'),
|
||||||
|
component: AssistantScenariosIndex,
|
||||||
|
name: 'captain_assistants_scenarios_index',
|
||||||
|
meta: metaV2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/captain/:assistantId/playground'),
|
||||||
|
component: AssistantPlaygroundIndex,
|
||||||
|
name: 'captain_assistants_playground_index',
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/captain/:assistantId/inboxes'),
|
||||||
component: AssistantInboxesIndex,
|
component: AssistantInboxesIndex,
|
||||||
name: 'captain_assistants_inboxes_index',
|
name: 'captain_assistants_inboxes_index',
|
||||||
meta: {
|
meta,
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs/pending'),
|
||||||
|
component: ResponsesPendingIndex,
|
||||||
|
name: 'captain_assistants_responses_pending',
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/captain/:assistantId/settings'),
|
||||||
|
component: AssistantSettingsIndex,
|
||||||
|
name: 'captain_assistants_settings_index',
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
// Settings sub-pages (guardrails and guidelines)
|
||||||
{
|
{
|
||||||
path: frontendURL(
|
path: frontendURL(
|
||||||
'accounts/:accountId/captain/assistants/:assistantId/guardrails'
|
'accounts/:accountId/captain/:assistantId/settings/guardrails'
|
||||||
),
|
),
|
||||||
component: AssistantGuardrailsIndex,
|
component: AssistantGuardrailsIndex,
|
||||||
name: 'captain_assistants_guardrails_index',
|
name: 'captain_assistants_guardrails_index',
|
||||||
meta: {
|
meta: metaV2,
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL(
|
path: frontendURL(
|
||||||
'accounts/:accountId/captain/assistants/:assistantId/scenarios'
|
'accounts/:accountId/captain/:assistantId/settings/guidelines'
|
||||||
),
|
|
||||||
component: AssistantScenariosIndex,
|
|
||||||
name: 'captain_assistants_scenarios_index',
|
|
||||||
meta: {
|
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: frontendURL(
|
|
||||||
'accounts/:accountId/captain/assistants/:assistantId/guidelines'
|
|
||||||
),
|
),
|
||||||
component: AssistantGuidelinesIndex,
|
component: AssistantGuidelinesIndex,
|
||||||
name: 'captain_assistants_guidelines_index',
|
name: 'captain_assistants_guidelines_index',
|
||||||
|
meta: metaV2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/captain/assistants'),
|
||||||
|
component: AssistantEmptyStateIndex,
|
||||||
|
name: 'captain_assistants_create_index',
|
||||||
meta: {
|
meta: {
|
||||||
permissions: ['administrator', 'agent'],
|
permissions: ['administrator', 'agent'],
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
installationTypes: [
|
||||||
INSTALLATION_TYPES.CLOUD,
|
INSTALLATION_TYPES.CLOUD,
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
INSTALLATION_TYPES.ENTERPRISE,
|
||||||
@ -101,55 +108,26 @@ export const routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/captain/documents'),
|
path: frontendURL('accounts/:accountId/captain/:navigationPath'),
|
||||||
component: DocumentsIndex,
|
component: AssistantsIndexPage,
|
||||||
name: 'captain_documents_index',
|
name: 'captain_assistants_index',
|
||||||
meta: {
|
meta,
|
||||||
permissions: ['administrator', 'agent'],
|
},
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
];
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
export const routes = [
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
{
|
||||||
],
|
path: frontendURL('accounts/:accountId/captain'),
|
||||||
},
|
component: CaptainPageRouteView,
|
||||||
},
|
redirect: to => {
|
||||||
{
|
return {
|
||||||
path: frontendURL('accounts/:accountId/captain/responses'),
|
name: 'captain_assistants_index',
|
||||||
component: ResponsesIndex,
|
params: {
|
||||||
name: 'captain_responses_index',
|
navigationPath: 'captain_assistants_responses_index',
|
||||||
meta: {
|
...to.params,
|
||||||
permissions: ['administrator', 'agent'],
|
},
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
};
|
||||||
installationTypes: [
|
},
|
||||||
INSTALLATION_TYPES.CLOUD,
|
children: [...assistantRoutes],
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: frontendURL('accounts/:accountId/captain/responses/pending'),
|
|
||||||
component: ResponsesPendingIndex,
|
|
||||||
name: 'captain_responses_pending',
|
|
||||||
meta: {
|
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: frontendURL('accounts/:accountId/captain/tools'),
|
|
||||||
component: CustomToolsIndex,
|
|
||||||
name: 'captain_tools_index',
|
|
||||||
meta: {
|
|
||||||
permissions: ['administrator', 'agent'],
|
|
||||||
featureFlag: FEATURE_FLAGS.CAPTAIN_V2,
|
|
||||||
installationTypes: [
|
|
||||||
INSTALLATION_TYPES.CLOUD,
|
|
||||||
INSTALLATION_TYPES.ENTERPRISE,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
|
||||||
@ -10,20 +11,20 @@ import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
|||||||
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
||||||
import RelatedResponses from 'dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue';
|
import RelatedResponses from 'dashboard/components-next/captain/pageComponents/document/RelatedResponses.vue';
|
||||||
import CreateDocumentDialog from 'dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue';
|
import CreateDocumentDialog from 'dashboard/components-next/captain/pageComponents/document/CreateDocumentDialog.vue';
|
||||||
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
|
|
||||||
import DocumentPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue';
|
import DocumentPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/DocumentPageEmptyState.vue';
|
||||||
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
|
import FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
|
||||||
import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
|
import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const { isOnChatwootCloud } = useAccount();
|
const { isOnChatwootCloud } = useAccount();
|
||||||
const uiFlags = useMapGetter('captainDocuments/getUIFlags');
|
const uiFlags = useMapGetter('captainDocuments/getUIFlags');
|
||||||
const documents = useMapGetter('captainDocuments/getRecords');
|
const documents = useMapGetter('captainDocuments/getRecords');
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||||
const documentsMeta = useMapGetter('captainDocuments/getMeta');
|
const documentsMeta = useMapGetter('captainDocuments/getMeta');
|
||||||
const selectedAssistant = ref('all');
|
|
||||||
|
const selectedAssistantId = computed(() => Number(route.params.assistantId));
|
||||||
|
|
||||||
const selectedDocument = ref(null);
|
const selectedDocument = ref(null);
|
||||||
const deleteDocumentDialog = ref(null);
|
const deleteDocumentDialog = ref(null);
|
||||||
@ -37,12 +38,6 @@ const showCreateDialog = ref(false);
|
|||||||
const createDocumentDialog = ref(null);
|
const createDocumentDialog = ref(null);
|
||||||
const relationQuestionDialog = ref(null);
|
const relationQuestionDialog = ref(null);
|
||||||
|
|
||||||
const shouldShowAssistantSelector = computed(() => {
|
|
||||||
if (assistants.value.length === 0) return false;
|
|
||||||
|
|
||||||
return !isFetching.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleShowRelatedDocument = () => {
|
const handleShowRelatedDocument = () => {
|
||||||
showRelatedResponses.value = true;
|
showRelatedResponses.value = true;
|
||||||
nextTick(() => relationQuestionDialog.value.dialogRef.open());
|
nextTick(() => relationQuestionDialog.value.dialogRef.open());
|
||||||
@ -77,17 +72,12 @@ const handleAction = ({ action, id }) => {
|
|||||||
const fetchDocuments = (page = 1) => {
|
const fetchDocuments = (page = 1) => {
|
||||||
const filterParams = { page };
|
const filterParams = { page };
|
||||||
|
|
||||||
if (selectedAssistant.value !== 'all') {
|
if (selectedAssistantId.value) {
|
||||||
filterParams.assistantId = selectedAssistant.value;
|
filterParams.assistantId = selectedAssistantId.value;
|
||||||
}
|
}
|
||||||
store.dispatch('captainDocuments/get', filterParams);
|
store.dispatch('captainDocuments/get', filterParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAssistantFilterChange = assistant => {
|
|
||||||
selectedAssistant.value = assistant;
|
|
||||||
fetchDocuments();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPageChange = page => fetchDocuments(page);
|
const onPageChange = page => fetchDocuments(page);
|
||||||
|
|
||||||
const onDeleteSuccess = () => {
|
const onDeleteSuccess = () => {
|
||||||
@ -97,9 +87,6 @@ const onDeleteSuccess = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!assistants.value.length) {
|
|
||||||
store.dispatch('captainAssistants/get');
|
|
||||||
}
|
|
||||||
fetchDocuments();
|
fetchDocuments();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -114,6 +101,7 @@ onMounted(() => {
|
|||||||
:show-pagination-footer="!isFetching && !!documents.length"
|
:show-pagination-footer="!isFetching && !!documents.length"
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
:is-empty="!documents.length"
|
:is-empty="!documents.length"
|
||||||
|
:show-know-more="false"
|
||||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||||
@update:current-page="onPageChange"
|
@update:current-page="onPageChange"
|
||||||
@click="handleCreateDocument"
|
@click="handleCreateDocument"
|
||||||
@ -138,15 +126,6 @@ onMounted(() => {
|
|||||||
<CaptainPaywall />
|
<CaptainPaywall />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #controls>
|
|
||||||
<div v-if="shouldShowAssistantSelector" class="mb-4 -mt-3 flex gap-3">
|
|
||||||
<AssistantSelector
|
|
||||||
:assistant-id="selectedAssistant"
|
|
||||||
@update="handleAssistantFilterChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<LimitBanner class="mb-5" />
|
<LimitBanner class="mb-5" />
|
||||||
|
|
||||||
@ -173,6 +152,7 @@ onMounted(() => {
|
|||||||
<CreateDocumentDialog
|
<CreateDocumentDialog
|
||||||
v-if="showCreateDialog"
|
v-if="showCreateDialog"
|
||||||
ref="createDocumentDialog"
|
ref="createDocumentDialog"
|
||||||
|
:assistant-id="selectedAssistantId"
|
||||||
@close="handleCreateDialogClose"
|
@close="handleCreateDialogClose"
|
||||||
/>
|
/>
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const { uiSettings } = useUISettings();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const assistants = computed(
|
||||||
|
() => store.getters['captainAssistants/getRecords']
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAssistantPresent = assistantId => {
|
||||||
|
return !!assistants.value.find(a => a.id === Number(assistantId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeToView = (name, params) => {
|
||||||
|
router.replace({ name, params, replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRouterParams = () => {
|
||||||
|
const { last_active_assistant_id: lastActiveAssistantId } =
|
||||||
|
uiSettings.value || {};
|
||||||
|
|
||||||
|
if (isAssistantPresent(lastActiveAssistantId)) {
|
||||||
|
return {
|
||||||
|
assistantId: lastActiveAssistantId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assistants.value.length > 0) {
|
||||||
|
const { id: assistantId } = assistants.value[0];
|
||||||
|
return { assistantId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeToLastActiveAssistant = () => {
|
||||||
|
const params = generateRouterParams();
|
||||||
|
|
||||||
|
// No assistants found, redirect to create page
|
||||||
|
if (!params) {
|
||||||
|
return routeToView('captain_assistants_create_index', {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { navigationPath } = route.params;
|
||||||
|
const isAValidRoute = [
|
||||||
|
'captain_assistants_responses_index', // Faq page
|
||||||
|
'captain_assistants_documents_index', // Document page
|
||||||
|
'captain_assistants_scenarios_index', // Scenario page
|
||||||
|
'captain_assistants_playground_index', // Playground page
|
||||||
|
'captain_assistants_inboxes_index', // Inboxes page
|
||||||
|
'captain_tools_index', // Tools page
|
||||||
|
'captain_assistants_settings_index', // Settings page
|
||||||
|
].includes(navigationPath);
|
||||||
|
|
||||||
|
const navigateTo = isAValidRoute
|
||||||
|
? navigationPath
|
||||||
|
: 'captain_assistants_responses_index';
|
||||||
|
|
||||||
|
return routeToView(navigateTo, {
|
||||||
|
accountId: route.params.accountId,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const performRouting = async () => {
|
||||||
|
await store.dispatch('captainAssistants/get');
|
||||||
|
nextTick(() => routeToLastActiveAssistant());
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => performRouting());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-full bg-n-background text-n-slate-11"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.assistantId,
|
||||||
|
newAssistantId => {
|
||||||
|
if (
|
||||||
|
newAssistantId &&
|
||||||
|
newAssistantId !== String(uiSettings.value.last_active_assistant_id)
|
||||||
|
) {
|
||||||
|
updateUISettings({
|
||||||
|
last_active_assistant_id: Number(newAssistantId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex w-full h-full min-h-0">
|
||||||
|
<section class="flex flex-1 h-full px-0 overflow-hidden bg-n-background">
|
||||||
|
<router-view />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -8,14 +8,12 @@ import { debounce } from '@chatwoot/utils';
|
|||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
|
|
||||||
import Banner from 'dashboard/components-next/banner/Banner.vue';
|
import Banner from 'dashboard/components-next/banner/Banner.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
|
||||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||||
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
||||||
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
|
|
||||||
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||||
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
||||||
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
|
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
|
||||||
@ -27,7 +25,6 @@ const route = useRoute();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { isOnChatwootCloud } = useAccount();
|
const { isOnChatwootCloud } = useAccount();
|
||||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
|
||||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||||
const responses = useMapGetter('captainResponses/getRecords');
|
const responses = useMapGetter('captainResponses/getRecords');
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||||
@ -36,18 +33,13 @@ const selectedResponse = ref(null);
|
|||||||
const deleteDialog = ref(null);
|
const deleteDialog = ref(null);
|
||||||
const bulkDeleteDialog = ref(null);
|
const bulkDeleteDialog = ref(null);
|
||||||
|
|
||||||
const selectedAssistant = ref('all');
|
|
||||||
const dialogType = ref('');
|
const dialogType = ref('');
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const createDialog = ref(null);
|
const createDialog = ref(null);
|
||||||
|
|
||||||
const shouldShowDropdown = computed(() => {
|
const selectedAssistantId = computed(() => Number(route.params.assistantId));
|
||||||
if (assistants.value.length === 0) return false;
|
|
||||||
|
|
||||||
return !isFetching.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pendingCount = useMapGetter('captainResponses/getPendingCount');
|
const pendingCount = useMapGetter('captainResponses/getPendingCount');
|
||||||
|
|
||||||
@ -106,8 +98,8 @@ const updateURLWithFilters = (page, search) => {
|
|||||||
const fetchResponses = (page = 1) => {
|
const fetchResponses = (page = 1) => {
|
||||||
const filterParams = { page, status: 'approved' };
|
const filterParams = { page, status: 'approved' };
|
||||||
|
|
||||||
if (selectedAssistant.value !== 'all') {
|
if (selectedAssistantId.value) {
|
||||||
filterParams.assistantId = selectedAssistant.value;
|
filterParams.assistantId = selectedAssistantId.value;
|
||||||
}
|
}
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
filterParams.search = searchQuery.value;
|
filterParams.search = searchQuery.value;
|
||||||
@ -123,33 +115,20 @@ const fetchResponses = (page = 1) => {
|
|||||||
const bulkSelectedIds = ref(new Set());
|
const bulkSelectedIds = ref(new Set());
|
||||||
const hoveredCard = ref(null);
|
const hoveredCard = ref(null);
|
||||||
|
|
||||||
const bulkSelectionState = computed(() => {
|
|
||||||
const selectedCount = bulkSelectedIds.value.size;
|
|
||||||
const totalCount = responses.value?.length || 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasSelected: selectedCount > 0,
|
|
||||||
isIndeterminate: selectedCount > 0 && selectedCount < totalCount,
|
|
||||||
allSelected: totalCount > 0 && selectedCount === totalCount,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const bulkCheckbox = computed({
|
|
||||||
get: () => bulkSelectionState.value.allSelected,
|
|
||||||
set: value => {
|
|
||||||
bulkSelectedIds.value = value
|
|
||||||
? new Set(responses.value.map(r => r.id))
|
|
||||||
: new Set();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildSelectedCountLabel = computed(() => {
|
const buildSelectedCountLabel = computed(() => {
|
||||||
const count = responses.value?.length || 0;
|
const count = responses.value?.length || 0;
|
||||||
return bulkSelectionState.value.allSelected
|
const isAllSelected = bulkSelectedIds.value.size === count && count > 0;
|
||||||
|
return isAllSelected
|
||||||
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
|
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
|
||||||
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
|
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedCountLabel = computed(() => {
|
||||||
|
return t('CAPTAIN.RESPONSES.SELECTED', {
|
||||||
|
count: bulkSelectedIds.value.size,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleCardHover = (isHovered, id) => {
|
const handleCardHover = (isHovered, id) => {
|
||||||
hoveredCard.value = isHovered ? id : null;
|
hoveredCard.value = isHovered ? id : null;
|
||||||
};
|
};
|
||||||
@ -179,14 +158,11 @@ const fetchResponseAfterBulkAction = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPageChange = page => {
|
const onPageChange = page => {
|
||||||
// Store current selection state before fetching new page
|
const hadSelection = bulkSelectedIds.value.size > 0;
|
||||||
const wasAllPageSelected = bulkSelectionState.value.allSelected;
|
|
||||||
const hadPartialSelection = bulkSelectedIds.value.size > 0;
|
|
||||||
|
|
||||||
fetchResponses(page);
|
fetchResponses(page);
|
||||||
|
|
||||||
// Reset selection if we had any selections on page change
|
if (hadSelection) {
|
||||||
if (wasAllPageSelected || hadPartialSelection) {
|
|
||||||
bulkSelectedIds.value = new Set();
|
bulkSelectedIds.value = new Set();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -201,11 +177,6 @@ const onBulkDeleteSuccess = () => {
|
|||||||
fetchResponseAfterBulkAction();
|
fetchResponseAfterBulkAction();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAssistantFilterChange = assistant => {
|
|
||||||
selectedAssistant.value = assistant;
|
|
||||||
fetchResponses(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedSearch = debounce(async () => {
|
const debouncedSearch = debounce(async () => {
|
||||||
fetchResponses(1);
|
fetchResponses(1);
|
||||||
}, 500);
|
}, 500);
|
||||||
@ -219,13 +190,12 @@ const initializeFromURL = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateToPendingFAQs = () => {
|
const navigateToPendingFAQs = () => {
|
||||||
router.push({ name: 'captain_responses_pending' });
|
router.push({ name: 'captain_assistants_responses_pending' });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch('captainAssistants/get');
|
|
||||||
initializeFromURL();
|
initializeFromURL();
|
||||||
store.dispatch('captainResponses/fetchPendingCount');
|
store.dispatch('captainResponses/fetchPendingCount', selectedAssistantId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -255,82 +225,38 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #subHeader>
|
<template #search>
|
||||||
<div
|
<div
|
||||||
v-if="shouldShowDropdown"
|
v-if="bulkSelectedIds.size === 0"
|
||||||
class="mb-2 flex justify-between items-center py-1"
|
class="flex gap-3 justify-between w-full items-center"
|
||||||
:class="{
|
|
||||||
'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 w-fit':
|
|
||||||
bulkSelectionState.hasSelected,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div
|
<Input
|
||||||
v-if="!bulkSelectionState.hasSelected"
|
v-model="searchQuery"
|
||||||
class="flex gap-3 justify-between w-full items-center"
|
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
||||||
>
|
class="w-64"
|
||||||
<div class="flex items-center gap-3">
|
size="sm"
|
||||||
<AssistantSelector
|
type="search"
|
||||||
:assistant-id="selectedAssistant"
|
autofocus
|
||||||
@update="handleAssistantFilterChange"
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
v-model="searchQuery"
|
|
||||||
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
|
||||||
class="w-64"
|
|
||||||
size="sm"
|
|
||||||
type="search"
|
|
||||||
autofocus
|
|
||||||
@input="debouncedSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition
|
|
||||||
name="slide-fade"
|
|
||||||
enter-active-class="transition-all duration-300 ease-out"
|
|
||||||
enter-from-class="opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4"
|
|
||||||
enter-to-class="opacity-100 transform translate-x-0"
|
|
||||||
leave-active-class="hidden opacity-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="bulkSelectionState.hasSelected"
|
|
||||||
class="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<Checkbox
|
|
||||||
v-model="bulkCheckbox"
|
|
||||||
:indeterminate="bulkSelectionState.isIndeterminate"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-n-slate-12 font-medium tabular-nums">
|
|
||||||
{{ buildSelectedCountLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-n-slate-10 tabular-nums">
|
|
||||||
{{
|
|
||||||
$t('CAPTAIN.RESPONSES.SELECTED', {
|
|
||||||
count: bulkSelectedIds.size,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-4 w-px bg-n-strong" />
|
|
||||||
<div class="flex gap-3 items-center">
|
|
||||||
<Button
|
|
||||||
:label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
|
|
||||||
sm
|
|
||||||
ruby
|
|
||||||
ghost
|
|
||||||
class="!px-1.5"
|
|
||||||
icon="i-lucide-trash"
|
|
||||||
@click="bulkDeleteDialog.dialogRef.open()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #subHeader>
|
||||||
|
<BulkSelectBar
|
||||||
|
v-model="bulkSelectedIds"
|
||||||
|
:all-items="responses"
|
||||||
|
:select-all-label="buildSelectedCountLabel"
|
||||||
|
:selected-count-label="selectedCountLabel"
|
||||||
|
:delete-label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
|
||||||
|
class="w-fit"
|
||||||
|
:class="{
|
||||||
|
'mb-2': bulkSelectedIds.size > 0,
|
||||||
|
}"
|
||||||
|
@bulk-delete="bulkDeleteDialog.dialogRef.open()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #emptyState>
|
<template #emptyState>
|
||||||
<ResponsePageEmptyState @click="handleCreate" />
|
<ResponsePageEmptyState @click="handleCreate" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -7,16 +7,14 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import { debounce } from '@chatwoot/utils';
|
import { debounce } from '@chatwoot/utils';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||||
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
||||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||||
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
import CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
||||||
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
|
|
||||||
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||||
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
||||||
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
|
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
|
||||||
@ -28,7 +26,6 @@ const route = useRoute();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { isOnChatwootCloud } = useAccount();
|
const { isOnChatwootCloud } = useAccount();
|
||||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
|
||||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||||
const responses = useMapGetter('captainResponses/getRecords');
|
const responses = useMapGetter('captainResponses/getRecords');
|
||||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||||
@ -37,21 +34,20 @@ const selectedResponse = ref(null);
|
|||||||
const deleteDialog = ref(null);
|
const deleteDialog = ref(null);
|
||||||
const bulkDeleteDialog = ref(null);
|
const bulkDeleteDialog = ref(null);
|
||||||
|
|
||||||
const selectedAssistant = ref('all');
|
const selectedAssistantId = Number(route.params.assistantId);
|
||||||
const dialogType = ref('');
|
const dialogType = ref('');
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const createDialog = ref(null);
|
const createDialog = ref(null);
|
||||||
|
|
||||||
const shouldShowDropdown = computed(() => {
|
const backUrl = computed(() => ({
|
||||||
if (assistants.value.length === 0) return false;
|
name: 'captain_assistants_responses_index',
|
||||||
return !isFetching.value;
|
params: {
|
||||||
});
|
accountId: route.params.accountId,
|
||||||
|
assistantId: selectedAssistantId,
|
||||||
const backUrl = computed(() =>
|
},
|
||||||
frontendURL(`accounts/${route.params.accountId}/captain/responses`)
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
// Filter out approved responses in pending view
|
// Filter out approved responses in pending view
|
||||||
const filteredResponses = computed(() =>
|
const filteredResponses = computed(() =>
|
||||||
@ -78,11 +74,6 @@ const handleAccept = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
|
||||||
dialogType.value = 'create';
|
|
||||||
nextTick(() => createDialog.value.dialogRef.open());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
dialogType.value = 'edit';
|
dialogType.value = 'edit';
|
||||||
nextTick(() => createDialog.value.dialogRef.open());
|
nextTick(() => createDialog.value.dialogRef.open());
|
||||||
@ -134,8 +125,8 @@ const updateURLWithFilters = (page, search) => {
|
|||||||
const fetchResponses = (page = 1) => {
|
const fetchResponses = (page = 1) => {
|
||||||
const filterParams = { page, status: 'pending' };
|
const filterParams = { page, status: 'pending' };
|
||||||
|
|
||||||
if (selectedAssistant.value !== 'all') {
|
if (selectedAssistantId) {
|
||||||
filterParams.assistantId = selectedAssistant.value;
|
filterParams.assistantId = selectedAssistantId;
|
||||||
}
|
}
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
filterParams.search = searchQuery.value;
|
filterParams.search = searchQuery.value;
|
||||||
@ -151,33 +142,20 @@ const fetchResponses = (page = 1) => {
|
|||||||
const bulkSelectedIds = ref(new Set());
|
const bulkSelectedIds = ref(new Set());
|
||||||
const hoveredCard = ref(null);
|
const hoveredCard = ref(null);
|
||||||
|
|
||||||
const bulkSelectionState = computed(() => {
|
|
||||||
const selectedCount = bulkSelectedIds.value.size;
|
|
||||||
const totalCount = filteredResponses.value?.length || 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasSelected: selectedCount > 0,
|
|
||||||
isIndeterminate: selectedCount > 0 && selectedCount < totalCount,
|
|
||||||
allSelected: totalCount > 0 && selectedCount === totalCount,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const bulkCheckbox = computed({
|
|
||||||
get: () => bulkSelectionState.value.allSelected,
|
|
||||||
set: value => {
|
|
||||||
bulkSelectedIds.value = value
|
|
||||||
? new Set(filteredResponses.value.map(r => r.id))
|
|
||||||
: new Set();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildSelectedCountLabel = computed(() => {
|
const buildSelectedCountLabel = computed(() => {
|
||||||
const count = filteredResponses.value?.length || 0;
|
const count = filteredResponses.value?.length || 0;
|
||||||
return bulkSelectionState.value.allSelected
|
const isAllSelected = bulkSelectedIds.value.size === count && count > 0;
|
||||||
|
return isAllSelected
|
||||||
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
|
? t('CAPTAIN.RESPONSES.UNSELECT_ALL', { count })
|
||||||
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
|
: t('CAPTAIN.RESPONSES.SELECT_ALL', { count });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedCountLabel = computed(() => {
|
||||||
|
return t('CAPTAIN.RESPONSES.SELECTED', {
|
||||||
|
count: bulkSelectedIds.value.size,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handleCardHover = (isHovered, id) => {
|
const handleCardHover = (isHovered, id) => {
|
||||||
hoveredCard.value = isHovered ? id : null;
|
hoveredCard.value = isHovered ? id : null;
|
||||||
};
|
};
|
||||||
@ -219,12 +197,11 @@ const handleBulkApprove = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPageChange = page => {
|
const onPageChange = page => {
|
||||||
const wasAllPageSelected = bulkSelectionState.value.allSelected;
|
const hadSelection = bulkSelectedIds.value.size > 0;
|
||||||
const hadPartialSelection = bulkSelectedIds.value.size > 0;
|
|
||||||
|
|
||||||
fetchResponses(page);
|
fetchResponses(page);
|
||||||
|
|
||||||
if (wasAllPageSelected || hadPartialSelection) {
|
if (hadSelection) {
|
||||||
bulkSelectedIds.value = new Set();
|
bulkSelectedIds.value = new Set();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -239,22 +216,16 @@ const onBulkDeleteSuccess = () => {
|
|||||||
fetchResponseAfterBulkAction();
|
fetchResponseAfterBulkAction();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAssistantFilterChange = assistant => {
|
|
||||||
selectedAssistant.value = assistant;
|
|
||||||
fetchResponses(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedSearch = debounce(async () => {
|
const debouncedSearch = debounce(async () => {
|
||||||
fetchResponses(1);
|
fetchResponses(1);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const hasActiveFilters = computed(() => {
|
const hasActiveFilters = computed(() => {
|
||||||
return Boolean(searchQuery.value || selectedAssistant.value !== 'all');
|
return Boolean(searchQuery.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
selectedAssistant.value = 'all';
|
|
||||||
fetchResponses(1);
|
fetchResponses(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -267,7 +238,6 @@ const initializeFromURL = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.dispatch('captainAssistants/get');
|
|
||||||
initializeFromURL();
|
initializeFromURL();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -276,16 +246,14 @@ onMounted(() => {
|
|||||||
<PageLayout
|
<PageLayout
|
||||||
:total-count="responseMeta.totalCount"
|
:total-count="responseMeta.totalCount"
|
||||||
:current-page="responseMeta.page"
|
:current-page="responseMeta.page"
|
||||||
:button-policy="['administrator']"
|
|
||||||
:header-title="$t('CAPTAIN.RESPONSES.PENDING_FAQS')"
|
:header-title="$t('CAPTAIN.RESPONSES.PENDING_FAQS')"
|
||||||
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
|
|
||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
:is-empty="!filteredResponses.length"
|
:is-empty="!filteredResponses.length"
|
||||||
:show-pagination-footer="!isFetching && !!filteredResponses.length"
|
:show-pagination-footer="!isFetching && !!filteredResponses.length"
|
||||||
|
:show-know-more="false"
|
||||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||||
:back-url="backUrl"
|
:back-url="backUrl"
|
||||||
@update:current-page="onPageChange"
|
@update:current-page="onPageChange"
|
||||||
@click="handleCreate"
|
|
||||||
>
|
>
|
||||||
<template #knowMore>
|
<template #knowMore>
|
||||||
<FeatureSpotlightPopover
|
<FeatureSpotlightPopover
|
||||||
@ -299,96 +267,53 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #subHeader>
|
<template #search>
|
||||||
<div
|
<div
|
||||||
v-if="shouldShowDropdown"
|
v-if="bulkSelectedIds.size === 0"
|
||||||
class="mb-2 flex justify-between items-center py-1"
|
class="flex gap-3 justify-between w-full items-center"
|
||||||
:class="{
|
|
||||||
'ltr:pl-3 rtl:pr-3 ltr:pr-1 rtl:pl-1 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 w-fit':
|
|
||||||
bulkSelectionState.hasSelected,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div
|
<Input
|
||||||
v-if="!bulkSelectionState.hasSelected"
|
v-model="searchQuery"
|
||||||
class="flex gap-3 justify-between w-full items-center"
|
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
||||||
>
|
class="w-64"
|
||||||
<div class="flex items-center gap-3">
|
size="sm"
|
||||||
<AssistantSelector
|
type="search"
|
||||||
:assistant-id="selectedAssistant"
|
autofocus
|
||||||
@update="handleAssistantFilterChange"
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
v-model="searchQuery"
|
|
||||||
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
|
||||||
class="w-64"
|
|
||||||
size="sm"
|
|
||||||
type="search"
|
|
||||||
autofocus
|
|
||||||
@input="debouncedSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition
|
|
||||||
name="slide-fade"
|
|
||||||
enter-active-class="transition-all duration-300 ease-out"
|
|
||||||
enter-from-class="opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4"
|
|
||||||
enter-to-class="opacity-100 transform translate-x-0"
|
|
||||||
leave-active-class="hidden opacity-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="bulkSelectionState.hasSelected"
|
|
||||||
class="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<Checkbox
|
|
||||||
v-model="bulkCheckbox"
|
|
||||||
:indeterminate="bulkSelectionState.isIndeterminate"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-n-slate-12 font-medium tabular-nums">
|
|
||||||
{{ buildSelectedCountLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-n-slate-10 tabular-nums">
|
|
||||||
{{
|
|
||||||
$t('CAPTAIN.RESPONSES.SELECTED', {
|
|
||||||
count: bulkSelectedIds.size,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="h-4 w-px bg-n-strong" />
|
|
||||||
<div class="flex gap-3 items-center">
|
|
||||||
<Button
|
|
||||||
:label="$t('CAPTAIN.RESPONSES.BULK_APPROVE_BUTTON')"
|
|
||||||
sm
|
|
||||||
ghost
|
|
||||||
icon="i-lucide-check"
|
|
||||||
class="!px-1.5"
|
|
||||||
@click="handleBulkApprove"
|
|
||||||
/>
|
|
||||||
<div class="h-4 w-px bg-n-strong" />
|
|
||||||
<Button
|
|
||||||
:label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
|
|
||||||
sm
|
|
||||||
ruby
|
|
||||||
ghost
|
|
||||||
class="!px-1.5"
|
|
||||||
icon="i-lucide-trash"
|
|
||||||
@click="bulkDeleteDialog.dialogRef.open()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #subHeader>
|
||||||
|
<BulkSelectBar
|
||||||
|
v-model="bulkSelectedIds"
|
||||||
|
:all-items="filteredResponses"
|
||||||
|
:select-all-label="buildSelectedCountLabel"
|
||||||
|
:selected-count-label="selectedCountLabel"
|
||||||
|
:delete-label="$t('CAPTAIN.RESPONSES.BULK_DELETE_BUTTON')"
|
||||||
|
class="w-fit"
|
||||||
|
:class="{
|
||||||
|
'mb-2': bulkSelectedIds.size > 0,
|
||||||
|
}"
|
||||||
|
@bulk-delete="bulkDeleteDialog.dialogRef.open()"
|
||||||
|
>
|
||||||
|
<template #secondary-actions>
|
||||||
|
<Button
|
||||||
|
:label="$t('CAPTAIN.RESPONSES.BULK_APPROVE_BUTTON')"
|
||||||
|
sm
|
||||||
|
ghost
|
||||||
|
icon="i-lucide-check"
|
||||||
|
class="!px-1.5"
|
||||||
|
@click="handleBulkApprove"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BulkSelectBar>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #emptyState>
|
<template #emptyState>
|
||||||
<ResponsePageEmptyState
|
<ResponsePageEmptyState
|
||||||
variant="pending"
|
variant="pending"
|
||||||
:has-active-filters="hasActiveFilters"
|
:has-active-filters="hasActiveFilters"
|
||||||
@click="handleCreate"
|
|
||||||
@clear-filters="clearFilters"
|
@clear-filters="clearFilters"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -87,6 +87,7 @@ onMounted(() => {
|
|||||||
:is-fetching="isFetching"
|
:is-fetching="isFetching"
|
||||||
:is-empty="!customTools.length"
|
:is-empty="!customTools.length"
|
||||||
:feature-flag="FEATURE_FLAGS.CAPTAIN_V2"
|
:feature-flag="FEATURE_FLAGS.CAPTAIN_V2"
|
||||||
|
:show-know-more="false"
|
||||||
@update:current-page="onPageChange"
|
@update:current-page="onPageChange"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -175,7 +175,7 @@ describe('storeFactory', () => {
|
|||||||
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
|
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
|
||||||
fetchingItem: true,
|
fetchingItem: true,
|
||||||
});
|
});
|
||||||
expect(commit).toHaveBeenCalledWith(mutationTypes.ADD, data);
|
expect(commit).toHaveBeenCalledWith(mutationTypes.UPSERT, data);
|
||||||
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
|
expect(commit).toHaveBeenCalledWith(mutationTypes.SET_UI_FLAG, {
|
||||||
fetchingItem: false,
|
fetchingItem: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const showRecord =
|
|||||||
commit(mutationTypes.SET_UI_FLAG, { fetchingItem: true });
|
commit(mutationTypes.SET_UI_FLAG, { fetchingItem: true });
|
||||||
try {
|
try {
|
||||||
const response = await API.show(id);
|
const response = await API.show(id);
|
||||||
commit(mutationTypes.ADD, response.data);
|
commit(mutationTypes.UPSERT, response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return throwErrorMessage(error);
|
return throwErrorMessage(error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user