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:
@@ -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>
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { computed, ref, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
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 CaptainPaywall from 'dashboard/components-next/captain/pageComponents/Paywall.vue';
|
||||
import CreateAssistantDialog from 'dashboard/components-next/captain/pageComponents/assistant/CreateAssistantDialog.vue';
|
||||
import AssistantPageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/AssistantPageEmptyState.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 router = useRouter();
|
||||
|
||||
const store = useStore();
|
||||
const dialogType = ref('');
|
||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
|
||||
const selectedAssistant = ref(null);
|
||||
const deleteAssistantDialog = ref(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteAssistantDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const createAssistantDialog = ref(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create';
|
||||
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 = () => {
|
||||
dialogType.value = '';
|
||||
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>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.ASSISTANTS.HEADER')"
|
||||
:button-label="$t('CAPTAIN.ASSISTANTS.ADD_NEW')"
|
||||
:button-policy="['administrator']"
|
||||
:show-pagination-footer="false"
|
||||
:is-fetching="isFetching"
|
||||
:is-empty="!assistants.length"
|
||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||
is-empty
|
||||
@click="handleCreate"
|
||||
>
|
||||
<template #knowMore>
|
||||
@@ -107,36 +73,13 @@ onMounted(() => store.dispatch('captainAssistants/get'));
|
||||
<CaptainPaywall />
|
||||
</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
|
||||
v-if="dialogType"
|
||||
ref="createAssistantDialog"
|
||||
:type="dialogType"
|
||||
:selected-assistant="selectedAssistant"
|
||||
@close="handleCreateClose"
|
||||
@created="handleAfterCreate"
|
||||
/>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import Input from 'dashboard/components-next/input/Input.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 SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
|
||||
@@ -23,30 +23,27 @@ const route = useRoute();
|
||||
const store = useStore();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const assistantId = route.params.assistantId;
|
||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||
const assistantId = computed(() => Number(route.params.assistantId));
|
||||
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||
const assistant = computed(() =>
|
||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||
store.getters['captainAssistants/getRecord'](assistantId.value)
|
||||
);
|
||||
|
||||
const searchQuery = ref('');
|
||||
const newInlineRule = 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 backUrl = computed(() => ({
|
||||
name: 'captain_assistants_settings_index',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
assistantId: assistantId.value,
|
||||
},
|
||||
}));
|
||||
|
||||
const displayGuardrails = computed(() =>
|
||||
guardrailsContent.value.map((c, idx) => ({ id: idx, content: c }))
|
||||
);
|
||||
@@ -113,7 +110,7 @@ const selectedCountLabel = computed(() => {
|
||||
|
||||
const saveGuardrails = async list => {
|
||||
await store.dispatch('captainAssistants/update', {
|
||||
id: assistantId,
|
||||
id: assistantId.value,
|
||||
assistant: { guardrails: list },
|
||||
});
|
||||
};
|
||||
@@ -176,9 +173,13 @@ const addAllExample = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsPageLayout
|
||||
:breadcrumb-items="breadcrumbItems"
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.ASSISTANTS.GUARDRAILS.TITLE')"
|
||||
:is-fetching="isFetching"
|
||||
:back-url="backUrl"
|
||||
:show-know-more="false"
|
||||
:show-pagination-footer="false"
|
||||
:show-assistant-switcher="false"
|
||||
>
|
||||
<template #body>
|
||||
<SettingsHeader
|
||||
@@ -297,5 +298,5 @@ const addAllExample = () => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsPageLayout>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import Input from 'dashboard/components-next/input/Input.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 SuggestedRules from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||
import AddNewRulesInput from 'dashboard/components-next/captain/assistant/AddNewRulesInput.vue';
|
||||
@@ -23,32 +23,29 @@ const route = useRoute();
|
||||
const store = useStore();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const assistantId = route.params.assistantId;
|
||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||
const assistantId = computed(() => Number(route.params.assistantId));
|
||||
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||
const assistant = computed(() =>
|
||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||
store.getters['captainAssistants/getRecord'](assistantId.value)
|
||||
);
|
||||
|
||||
const searchQuery = ref('');
|
||||
const newInlineRule = 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(
|
||||
() => assistant.value?.response_guidelines || []
|
||||
);
|
||||
|
||||
const backUrl = computed(() => ({
|
||||
name: 'captain_assistants_settings_index',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
assistantId: assistantId.value,
|
||||
},
|
||||
}));
|
||||
|
||||
const displayGuidelines = computed(() =>
|
||||
guidelinesContent.value.map((c, idx) => ({ id: idx, content: c }))
|
||||
);
|
||||
@@ -119,7 +116,7 @@ const selectedCountLabel = computed(() => {
|
||||
|
||||
const saveGuidelines = async list => {
|
||||
await store.dispatch('captainAssistants/update', {
|
||||
id: assistantId,
|
||||
id: assistantId.value,
|
||||
assistant: { response_guidelines: list },
|
||||
});
|
||||
};
|
||||
@@ -183,9 +180,13 @@ const addAllExample = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsPageLayout
|
||||
:breadcrumb-items="breadcrumbItems"
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.ASSISTANTS.RESPONSE_GUIDELINES.TITLE')"
|
||||
:is-fetching="isFetching"
|
||||
:back-url="backUrl"
|
||||
:show-know-more="false"
|
||||
:show-pagination-footer="false"
|
||||
:show-assistant-switcher="false"
|
||||
>
|
||||
<template #body>
|
||||
<SettingsHeader
|
||||
@@ -321,5 +322,5 @@ const addAllExample = async () => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsPageLayout>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeMount, onMounted, ref, nextTick } from 'vue';
|
||||
import {
|
||||
useMapGetter,
|
||||
useStore,
|
||||
useStoreGetters,
|
||||
} from 'dashboard/composables/store';
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
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 PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
|
||||
import ConnectInboxDialog from 'dashboard/components-next/captain/pageComponents/inbox/ConnectInboxDialog.vue';
|
||||
@@ -52,13 +47,7 @@ const handleCreateClose = () => {
|
||||
selectedInbox.value = null;
|
||||
};
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const assistantId = Number(route.params.assistantId);
|
||||
const assistant = computed(() =>
|
||||
getters['captainAssistants/getRecord'].value(assistantId)
|
||||
);
|
||||
onBeforeMount(() => store.dispatch('captainAssistants/show', assistantId));
|
||||
|
||||
onMounted(() =>
|
||||
store.dispatch('captainInboxes/get', {
|
||||
assistantId: assistantId,
|
||||
@@ -68,27 +57,16 @@ onMounted(() =>
|
||||
|
||||
<template>
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.INBOXES.HEADER')"
|
||||
:button-label="$t('CAPTAIN.INBOXES.ADD_NEW')"
|
||||
:button-policy="['administrator']"
|
||||
:is-fetching="isFetchingAssistant || isFetching"
|
||||
:is-empty="!captainInboxes.length"
|
||||
:show-pagination-footer="false"
|
||||
:show-know-more="false"
|
||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||
@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>
|
||||
<InboxPageEmptyState @click="handleCreate" />
|
||||
</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 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 SuggestedScenarios from 'dashboard/components-next/captain/assistant/SuggestedRules.vue';
|
||||
import ScenariosCard from 'dashboard/components-next/captain/assistant/ScenariosCard.vue';
|
||||
@@ -22,28 +22,14 @@ const route = useRoute();
|
||||
const store = useStore();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const assistantId = route.params.assistantId;
|
||||
const assistantId = computed(() => Number(route.params.assistantId));
|
||||
|
||||
const uiFlags = useMapGetter('captainScenarios/getUIFlags');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const assistant = computed(() =>
|
||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||
);
|
||||
const scenarios = useMapGetter('captainScenarios/getRecords');
|
||||
|
||||
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 =
|
||||
'[&_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 {
|
||||
await store.dispatch('captainScenarios/update', {
|
||||
id: scenario.id,
|
||||
assistantId: route.params.assistantId,
|
||||
assistantId: assistantId.value,
|
||||
...scenario,
|
||||
tools: getToolsFromInstruction(scenario.instruction),
|
||||
});
|
||||
@@ -136,7 +122,7 @@ const deleteScenario = async id => {
|
||||
try {
|
||||
await store.dispatch('captainScenarios/delete', {
|
||||
id,
|
||||
assistantId: route.params.assistantId,
|
||||
assistantId: assistantId.value,
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.SCENARIOS.API.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
@@ -154,7 +140,7 @@ const bulkDeleteScenarios = async ids => {
|
||||
idsArray.map(id =>
|
||||
store.dispatch('captainScenarios/delete', {
|
||||
id,
|
||||
assistantId: route.params.assistantId,
|
||||
assistantId: assistantId.value,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -165,7 +151,7 @@ const bulkDeleteScenarios = async ids => {
|
||||
const addScenario = async scenario => {
|
||||
try {
|
||||
await store.dispatch('captainScenarios/create', {
|
||||
assistantId: route.params.assistantId,
|
||||
assistantId: assistantId.value,
|
||||
...scenario,
|
||||
tools: getToolsFromInstruction(scenario.instruction),
|
||||
});
|
||||
@@ -182,7 +168,7 @@ const addAllExampleScenarios = async () => {
|
||||
try {
|
||||
scenariosExample.forEach(async scenario => {
|
||||
await store.dispatch('captainScenarios/create', {
|
||||
assistantId: route.params.assistantId,
|
||||
assistantId: assistantId.value,
|
||||
...scenario,
|
||||
});
|
||||
});
|
||||
@@ -197,16 +183,18 @@ const addAllExampleScenarios = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainScenarios/get', {
|
||||
assistantId: assistantId,
|
||||
assistantId: assistantId.value,
|
||||
});
|
||||
store.dispatch('captainTools/getTools');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsPageLayout
|
||||
:breadcrumb-items="breadcrumbItems"
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.DOCUMENTS.HEADER')"
|
||||
:is-fetching="isFetching"
|
||||
:show-know-more="false"
|
||||
:show-pagination-footer="false"
|
||||
>
|
||||
<template #body>
|
||||
<SettingsHeader
|
||||
@@ -310,5 +298,5 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsPageLayout>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import SettingsPageLayout from 'dashboard/components-next/captain/SettingsPageLayout.vue';
|
||||
import { 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 AssistantBasicSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue';
|
||||
import AssistantSystemSettingsForm from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantSystemSettingsForm.vue';
|
||||
import AssistantControlItems from 'dashboard/components-next/captain/pageComponents/assistant/settings/AssistantControlItems.vue';
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const assistantId = route.params.assistantId;
|
||||
const uiFlags = useMapGetter('captainAssistants/getUIFlags');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingItem);
|
||||
const assistant = computed(() =>
|
||||
store.getters['captainAssistants/getRecord'](Number(assistantId))
|
||||
);
|
||||
const { isCloudFeatureEnabled } = useAccount();
|
||||
|
||||
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(() => {
|
||||
return [
|
||||
@@ -34,15 +46,6 @@ const controlItems = computed(() => {
|
||||
),
|
||||
routeName: 'captain_assistants_guardrails_index',
|
||||
},
|
||||
{
|
||||
name: t(
|
||||
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.TITLE'
|
||||
),
|
||||
description: t(
|
||||
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.SCENARIOS.DESCRIPTION'
|
||||
),
|
||||
routeName: 'captain_assistants_scenarios_index',
|
||||
},
|
||||
{
|
||||
name: t(
|
||||
'CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.OPTIONS.RESPONSE_GUIDELINES.TITLE'
|
||||
@@ -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 => {
|
||||
try {
|
||||
await store.dispatch('captainAssistants/update', {
|
||||
id: assistantId,
|
||||
id: assistantId.value,
|
||||
...updatedAssistant,
|
||||
});
|
||||
useAlert(t('CAPTAIN.ASSISTANTS.EDIT.SUCCESS_MESSAGE'));
|
||||
@@ -91,64 +72,126 @@ const handleSubmit = async updatedAssistant => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!isAssistantAvailable.value) {
|
||||
store.dispatch('captainAssistants/show', assistantId);
|
||||
const handleDelete = () => {
|
||||
deleteAssistantDialog.value.dialogRef.open();
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<SettingsPageLayout
|
||||
:breadcrumb-items="breadcrumbItems"
|
||||
<PageLayout
|
||||
:header-title="$t('CAPTAIN.ASSISTANTS.SETTINGS.HEADER')"
|
||||
: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>
|
||||
<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">
|
||||
<SettingsHeader
|
||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
|
||||
:description="
|
||||
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
|
||||
"
|
||||
/>
|
||||
<AssistantBasicSettingsForm
|
||||
:assistant="assistant"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
<div class="flex flex-col gap-6">
|
||||
<SettingsHeader
|
||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.TITLE')"
|
||||
:description="
|
||||
t('CAPTAIN.ASSISTANTS.SETTINGS.BASIC_SETTINGS.DESCRIPTION')
|
||||
"
|
||||
/>
|
||||
<AssistantBasicSettingsForm
|
||||
:assistant="assistant"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
<span class="h-px w-full bg-n-weak mt-2" />
|
||||
<div class="flex flex-col gap-6">
|
||||
<SettingsHeader
|
||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.TITLE')"
|
||||
:description="
|
||||
t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.DESCRIPTION')
|
||||
"
|
||||
/>
|
||||
<AssistantSystemSettingsForm
|
||||
:assistant="assistant"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<span class="h-px w-full bg-n-weak mt-2" />
|
||||
<div class="flex flex-col gap-6">
|
||||
<div v-if="isCaptainV2Enabled" class="flex flex-col gap-6">
|
||||
<SettingsHeader
|
||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.TITLE')"
|
||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
|
||||
:description="
|
||||
t('CAPTAIN.ASSISTANTS.SETTINGS.SYSTEM_SETTINGS.DESCRIPTION')
|
||||
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
|
||||
"
|
||||
/>
|
||||
<AssistantSystemSettingsForm
|
||||
:assistant="assistant"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
<div class="flex flex-col gap-6">
|
||||
<AssistantControlItems
|
||||
v-for="item in controlItems"
|
||||
:key="item.name"
|
||||
:control-item="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #controls>
|
||||
<div class="flex flex-col gap-6">
|
||||
<SettingsHeader
|
||||
:heading="t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.TITLE')"
|
||||
:description="
|
||||
t('CAPTAIN.ASSISTANTS.SETTINGS.CONTROL_ITEMS.DESCRIPTION')
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col gap-6">
|
||||
<AssistantControlItems
|
||||
v-for="item in controlItems"
|
||||
:key="item.name"
|
||||
:control-item="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsPageLayout>
|
||||
<DeleteDialog
|
||||
v-if="assistant"
|
||||
ref="deleteAssistantDialog"
|
||||
:entity="assistant"
|
||||
type="Assistants"
|
||||
translation-key="ASSISTANTS"
|
||||
@delete-success="handleDeleteSuccess"
|
||||
/>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { INSTALLATION_TYPES } from 'dashboard/constants/installationTypes';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import AssistantIndex from './assistants/Index.vue';
|
||||
import AssistantEdit from './assistants/Edit.vue';
|
||||
// import AssistantSettings from './assistants/settings/Settings.vue';
|
||||
|
||||
import CaptainPageRouteView from './pages/CaptainPageRouteView.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 AssistantPlaygroundIndex from './assistants/playground/Index.vue';
|
||||
import AssistantGuardrailsIndex from './assistants/guardrails/Index.vue';
|
||||
import AssistantGuidelinesIndex from './assistants/guidelines/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 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'),
|
||||
component: AssistantIndex,
|
||||
name: 'captain_assistants_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/faqs'),
|
||||
component: ResponsesIndex,
|
||||
name: 'captain_assistants_responses_index',
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/assistants/:assistantId'),
|
||||
component: AssistantEdit,
|
||||
name: 'captain_assistants_edit',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/documents'),
|
||||
component: DocumentsIndex,
|
||||
name: 'captain_assistants_documents_index',
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/captain/assistants/:assistantId/inboxes'
|
||||
),
|
||||
path: frontendURL('accounts/:accountId/captain/:assistantId/tools'),
|
||||
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,
|
||||
name: 'captain_assistants_inboxes_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
meta,
|
||||
},
|
||||
{
|
||||
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(
|
||||
'accounts/:accountId/captain/assistants/:assistantId/guardrails'
|
||||
'accounts/:accountId/captain/:assistantId/settings/guardrails'
|
||||
),
|
||||
component: AssistantGuardrailsIndex,
|
||||
name: 'captain_assistants_guardrails_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
meta: metaV2,
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/captain/assistants/:assistantId/scenarios'
|
||||
),
|
||||
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'
|
||||
'accounts/:accountId/captain/:assistantId/settings/guidelines'
|
||||
),
|
||||
component: AssistantGuidelinesIndex,
|
||||
name: 'captain_assistants_guidelines_index',
|
||||
meta: metaV2,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/assistants'),
|
||||
component: AssistantEmptyStateIndex,
|
||||
name: 'captain_assistants_create_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
@@ -101,55 +108,26 @@ export const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/documents'),
|
||||
component: DocumentsIndex,
|
||||
name: 'captain_documents_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
INSTALLATION_TYPES.ENTERPRISE,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain/responses'),
|
||||
component: ResponsesIndex,
|
||||
name: 'captain_responses_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
featureFlag: FEATURE_FLAGS.CAPTAIN,
|
||||
installationTypes: [
|
||||
INSTALLATION_TYPES.CLOUD,
|
||||
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,
|
||||
],
|
||||
},
|
||||
path: frontendURL('accounts/:accountId/captain/:navigationPath'),
|
||||
component: AssistantsIndexPage,
|
||||
name: 'captain_assistants_index',
|
||||
meta,
|
||||
},
|
||||
];
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain'),
|
||||
component: CaptainPageRouteView,
|
||||
redirect: to => {
|
||||
return {
|
||||
name: 'captain_assistants_index',
|
||||
params: {
|
||||
navigationPath: 'captain_assistants_responses_index',
|
||||
...to.params,
|
||||
},
|
||||
};
|
||||
},
|
||||
children: [...assistantRoutes],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
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 RelatedResponses from 'dashboard/components-next/captain/pageComponents/document/RelatedResponses.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 FeatureSpotlightPopover from 'dashboard/components-next/feature-spotlight/FeatureSpotlightPopover.vue';
|
||||
import LimitBanner from 'dashboard/components-next/captain/pageComponents/document/LimitBanner.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const uiFlags = useMapGetter('captainDocuments/getUIFlags');
|
||||
const documents = useMapGetter('captainDocuments/getRecords');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
const documentsMeta = useMapGetter('captainDocuments/getMeta');
|
||||
const selectedAssistant = ref('all');
|
||||
|
||||
const selectedAssistantId = computed(() => Number(route.params.assistantId));
|
||||
|
||||
const selectedDocument = ref(null);
|
||||
const deleteDocumentDialog = ref(null);
|
||||
@@ -37,12 +38,6 @@ const showCreateDialog = ref(false);
|
||||
const createDocumentDialog = ref(null);
|
||||
const relationQuestionDialog = ref(null);
|
||||
|
||||
const shouldShowAssistantSelector = computed(() => {
|
||||
if (assistants.value.length === 0) return false;
|
||||
|
||||
return !isFetching.value;
|
||||
});
|
||||
|
||||
const handleShowRelatedDocument = () => {
|
||||
showRelatedResponses.value = true;
|
||||
nextTick(() => relationQuestionDialog.value.dialogRef.open());
|
||||
@@ -77,17 +72,12 @@ const handleAction = ({ action, id }) => {
|
||||
const fetchDocuments = (page = 1) => {
|
||||
const filterParams = { page };
|
||||
|
||||
if (selectedAssistant.value !== 'all') {
|
||||
filterParams.assistantId = selectedAssistant.value;
|
||||
if (selectedAssistantId.value) {
|
||||
filterParams.assistantId = selectedAssistantId.value;
|
||||
}
|
||||
store.dispatch('captainDocuments/get', filterParams);
|
||||
};
|
||||
|
||||
const handleAssistantFilterChange = assistant => {
|
||||
selectedAssistant.value = assistant;
|
||||
fetchDocuments();
|
||||
};
|
||||
|
||||
const onPageChange = page => fetchDocuments(page);
|
||||
|
||||
const onDeleteSuccess = () => {
|
||||
@@ -97,9 +87,6 @@ const onDeleteSuccess = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!assistants.value.length) {
|
||||
store.dispatch('captainAssistants/get');
|
||||
}
|
||||
fetchDocuments();
|
||||
});
|
||||
</script>
|
||||
@@ -114,6 +101,7 @@ onMounted(() => {
|
||||
:show-pagination-footer="!isFetching && !!documents.length"
|
||||
:is-fetching="isFetching"
|
||||
:is-empty="!documents.length"
|
||||
:show-know-more="false"
|
||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||
@update:current-page="onPageChange"
|
||||
@click="handleCreateDocument"
|
||||
@@ -138,15 +126,6 @@ onMounted(() => {
|
||||
<CaptainPaywall />
|
||||
</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>
|
||||
<LimitBanner class="mb-5" />
|
||||
|
||||
@@ -173,6 +152,7 @@ onMounted(() => {
|
||||
<CreateDocumentDialog
|
||||
v-if="showCreateDialog"
|
||||
ref="createDocumentDialog"
|
||||
:assistant-id="selectedAssistantId"
|
||||
@close="handleCreateDialogClose"
|
||||
/>
|
||||
<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 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 BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.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 CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
||||
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
|
||||
@@ -27,7 +25,6 @@ const route = useRoute();
|
||||
const store = useStore();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
@@ -36,18 +33,13 @@ const selectedResponse = ref(null);
|
||||
const deleteDialog = ref(null);
|
||||
const bulkDeleteDialog = ref(null);
|
||||
|
||||
const selectedAssistant = ref('all');
|
||||
const dialogType = ref('');
|
||||
const searchQuery = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const createDialog = ref(null);
|
||||
|
||||
const shouldShowDropdown = computed(() => {
|
||||
if (assistants.value.length === 0) return false;
|
||||
|
||||
return !isFetching.value;
|
||||
});
|
||||
const selectedAssistantId = computed(() => Number(route.params.assistantId));
|
||||
|
||||
const pendingCount = useMapGetter('captainResponses/getPendingCount');
|
||||
|
||||
@@ -106,8 +98,8 @@ const updateURLWithFilters = (page, search) => {
|
||||
const fetchResponses = (page = 1) => {
|
||||
const filterParams = { page, status: 'approved' };
|
||||
|
||||
if (selectedAssistant.value !== 'all') {
|
||||
filterParams.assistantId = selectedAssistant.value;
|
||||
if (selectedAssistantId.value) {
|
||||
filterParams.assistantId = selectedAssistantId.value;
|
||||
}
|
||||
if (searchQuery.value) {
|
||||
filterParams.search = searchQuery.value;
|
||||
@@ -123,33 +115,20 @@ const fetchResponses = (page = 1) => {
|
||||
const bulkSelectedIds = ref(new Set());
|
||||
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 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.SELECT_ALL', { count });
|
||||
});
|
||||
|
||||
const selectedCountLabel = computed(() => {
|
||||
return t('CAPTAIN.RESPONSES.SELECTED', {
|
||||
count: bulkSelectedIds.value.size,
|
||||
});
|
||||
});
|
||||
|
||||
const handleCardHover = (isHovered, id) => {
|
||||
hoveredCard.value = isHovered ? id : null;
|
||||
};
|
||||
@@ -179,14 +158,11 @@ const fetchResponseAfterBulkAction = () => {
|
||||
};
|
||||
|
||||
const onPageChange = page => {
|
||||
// Store current selection state before fetching new page
|
||||
const wasAllPageSelected = bulkSelectionState.value.allSelected;
|
||||
const hadPartialSelection = bulkSelectedIds.value.size > 0;
|
||||
const hadSelection = bulkSelectedIds.value.size > 0;
|
||||
|
||||
fetchResponses(page);
|
||||
|
||||
// Reset selection if we had any selections on page change
|
||||
if (wasAllPageSelected || hadPartialSelection) {
|
||||
if (hadSelection) {
|
||||
bulkSelectedIds.value = new Set();
|
||||
}
|
||||
};
|
||||
@@ -201,11 +177,6 @@ const onBulkDeleteSuccess = () => {
|
||||
fetchResponseAfterBulkAction();
|
||||
};
|
||||
|
||||
const handleAssistantFilterChange = assistant => {
|
||||
selectedAssistant.value = assistant;
|
||||
fetchResponses(1);
|
||||
};
|
||||
|
||||
const debouncedSearch = debounce(async () => {
|
||||
fetchResponses(1);
|
||||
}, 500);
|
||||
@@ -219,13 +190,12 @@ const initializeFromURL = () => {
|
||||
};
|
||||
|
||||
const navigateToPendingFAQs = () => {
|
||||
router.push({ name: 'captain_responses_pending' });
|
||||
router.push({ name: 'captain_assistants_responses_pending' });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
initializeFromURL();
|
||||
store.dispatch('captainResponses/fetchPendingCount');
|
||||
store.dispatch('captainResponses/fetchPendingCount', selectedAssistantId);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -255,82 +225,38 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #subHeader>
|
||||
<template #search>
|
||||
<div
|
||||
v-if="shouldShowDropdown"
|
||||
class="mb-2 flex justify-between items-center py-1"
|
||||
: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,
|
||||
}"
|
||||
v-if="bulkSelectedIds.size === 0"
|
||||
class="flex gap-3 justify-between w-full items-center"
|
||||
>
|
||||
<div
|
||||
v-if="!bulkSelectionState.hasSelected"
|
||||
class="flex gap-3 justify-between w-full items-center"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<AssistantSelector
|
||||
:assistant-id="selectedAssistant"
|
||||
@update="handleAssistantFilterChange"
|
||||
/>
|
||||
</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>
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
||||
class="w-64"
|
||||
size="sm"
|
||||
type="search"
|
||||
autofocus
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
<ResponsePageEmptyState @click="handleCreate" />
|
||||
</template>
|
||||
|
||||
@@ -7,16 +7,14 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
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 BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
|
||||
import BulkDeleteDialog from 'dashboard/components-next/captain/pageComponents/BulkDeleteDialog.vue';
|
||||
import PageLayout from 'dashboard/components-next/captain/PageLayout.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 CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
|
||||
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
|
||||
@@ -28,7 +26,6 @@ const route = useRoute();
|
||||
const store = useStore();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const uiFlags = useMapGetter('captainResponses/getUIFlags');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const responseMeta = useMapGetter('captainResponses/getMeta');
|
||||
const responses = useMapGetter('captainResponses/getRecords');
|
||||
const isFetching = computed(() => uiFlags.value.fetchingList);
|
||||
@@ -37,21 +34,20 @@ const selectedResponse = ref(null);
|
||||
const deleteDialog = ref(null);
|
||||
const bulkDeleteDialog = ref(null);
|
||||
|
||||
const selectedAssistant = ref('all');
|
||||
const selectedAssistantId = Number(route.params.assistantId);
|
||||
const dialogType = ref('');
|
||||
const searchQuery = ref('');
|
||||
const { t } = useI18n();
|
||||
|
||||
const createDialog = ref(null);
|
||||
|
||||
const shouldShowDropdown = computed(() => {
|
||||
if (assistants.value.length === 0) return false;
|
||||
return !isFetching.value;
|
||||
});
|
||||
|
||||
const backUrl = computed(() =>
|
||||
frontendURL(`accounts/${route.params.accountId}/captain/responses`)
|
||||
);
|
||||
const backUrl = computed(() => ({
|
||||
name: 'captain_assistants_responses_index',
|
||||
params: {
|
||||
accountId: route.params.accountId,
|
||||
assistantId: selectedAssistantId,
|
||||
},
|
||||
}));
|
||||
|
||||
// Filter out approved responses in pending view
|
||||
const filteredResponses = computed(() =>
|
||||
@@ -78,11 +74,6 @@ const handleAccept = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogType.value = 'create';
|
||||
nextTick(() => createDialog.value.dialogRef.open());
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
dialogType.value = 'edit';
|
||||
nextTick(() => createDialog.value.dialogRef.open());
|
||||
@@ -134,8 +125,8 @@ const updateURLWithFilters = (page, search) => {
|
||||
const fetchResponses = (page = 1) => {
|
||||
const filterParams = { page, status: 'pending' };
|
||||
|
||||
if (selectedAssistant.value !== 'all') {
|
||||
filterParams.assistantId = selectedAssistant.value;
|
||||
if (selectedAssistantId) {
|
||||
filterParams.assistantId = selectedAssistantId;
|
||||
}
|
||||
if (searchQuery.value) {
|
||||
filterParams.search = searchQuery.value;
|
||||
@@ -151,33 +142,20 @@ const fetchResponses = (page = 1) => {
|
||||
const bulkSelectedIds = ref(new Set());
|
||||
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 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.SELECT_ALL', { count });
|
||||
});
|
||||
|
||||
const selectedCountLabel = computed(() => {
|
||||
return t('CAPTAIN.RESPONSES.SELECTED', {
|
||||
count: bulkSelectedIds.value.size,
|
||||
});
|
||||
});
|
||||
|
||||
const handleCardHover = (isHovered, id) => {
|
||||
hoveredCard.value = isHovered ? id : null;
|
||||
};
|
||||
@@ -219,12 +197,11 @@ const handleBulkApprove = async () => {
|
||||
};
|
||||
|
||||
const onPageChange = page => {
|
||||
const wasAllPageSelected = bulkSelectionState.value.allSelected;
|
||||
const hadPartialSelection = bulkSelectedIds.value.size > 0;
|
||||
const hadSelection = bulkSelectedIds.value.size > 0;
|
||||
|
||||
fetchResponses(page);
|
||||
|
||||
if (wasAllPageSelected || hadPartialSelection) {
|
||||
if (hadSelection) {
|
||||
bulkSelectedIds.value = new Set();
|
||||
}
|
||||
};
|
||||
@@ -239,22 +216,16 @@ const onBulkDeleteSuccess = () => {
|
||||
fetchResponseAfterBulkAction();
|
||||
};
|
||||
|
||||
const handleAssistantFilterChange = assistant => {
|
||||
selectedAssistant.value = assistant;
|
||||
fetchResponses(1);
|
||||
};
|
||||
|
||||
const debouncedSearch = debounce(async () => {
|
||||
fetchResponses(1);
|
||||
}, 500);
|
||||
|
||||
const hasActiveFilters = computed(() => {
|
||||
return Boolean(searchQuery.value || selectedAssistant.value !== 'all');
|
||||
return Boolean(searchQuery.value);
|
||||
});
|
||||
|
||||
const clearFilters = () => {
|
||||
searchQuery.value = '';
|
||||
selectedAssistant.value = 'all';
|
||||
fetchResponses(1);
|
||||
};
|
||||
|
||||
@@ -267,7 +238,6 @@ const initializeFromURL = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
initializeFromURL();
|
||||
});
|
||||
</script>
|
||||
@@ -276,16 +246,14 @@ onMounted(() => {
|
||||
<PageLayout
|
||||
:total-count="responseMeta.totalCount"
|
||||
:current-page="responseMeta.page"
|
||||
:button-policy="['administrator']"
|
||||
:header-title="$t('CAPTAIN.RESPONSES.PENDING_FAQS')"
|
||||
:button-label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
|
||||
:is-fetching="isFetching"
|
||||
:is-empty="!filteredResponses.length"
|
||||
:show-pagination-footer="!isFetching && !!filteredResponses.length"
|
||||
:show-know-more="false"
|
||||
:feature-flag="FEATURE_FLAGS.CAPTAIN"
|
||||
:back-url="backUrl"
|
||||
@update:current-page="onPageChange"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<template #knowMore>
|
||||
<FeatureSpotlightPopover
|
||||
@@ -299,96 +267,53 @@ onMounted(() => {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #subHeader>
|
||||
<template #search>
|
||||
<div
|
||||
v-if="shouldShowDropdown"
|
||||
class="mb-2 flex justify-between items-center py-1"
|
||||
: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,
|
||||
}"
|
||||
v-if="bulkSelectedIds.size === 0"
|
||||
class="flex gap-3 justify-between w-full items-center"
|
||||
>
|
||||
<div
|
||||
v-if="!bulkSelectionState.hasSelected"
|
||||
class="flex gap-3 justify-between w-full items-center"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<AssistantSelector
|
||||
:assistant-id="selectedAssistant"
|
||||
@update="handleAssistantFilterChange"
|
||||
/>
|
||||
</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>
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
:placeholder="$t('CAPTAIN.RESPONSES.SEARCH_PLACEHOLDER')"
|
||||
class="w-64"
|
||||
size="sm"
|
||||
type="search"
|
||||
autofocus
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
<ResponsePageEmptyState
|
||||
variant="pending"
|
||||
:has-active-filters="hasActiveFilters"
|
||||
@click="handleCreate"
|
||||
@clear-filters="clearFilters"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -87,6 +87,7 @@ onMounted(() => {
|
||||
:is-fetching="isFetching"
|
||||
:is-empty="!customTools.length"
|
||||
:feature-flag="FEATURE_FLAGS.CAPTAIN_V2"
|
||||
:show-know-more="false"
|
||||
@update:current-page="onPageChange"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user