# 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>
236 lines
6.6 KiB
Vue
236 lines
6.6 KiB
Vue
<script setup>
|
|
import { reactive, computed, ref, nextTick } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useVuelidate } from '@vuelidate/core';
|
|
import { minLength, requiredIf, url } from '@vuelidate/validators';
|
|
import { useMapGetter } from 'dashboard/composables/store';
|
|
import { useAlert } from 'dashboard/composables';
|
|
|
|
import Input from 'dashboard/components-next/input/Input.vue';
|
|
import Button from 'dashboard/components-next/button/Button.vue';
|
|
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
|
|
|
const props = defineProps({
|
|
assistantId: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(['submit', 'cancel']);
|
|
|
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
|
|
const { t } = useI18n();
|
|
|
|
const formState = {
|
|
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
|
|
};
|
|
|
|
const initialState = {
|
|
name: '',
|
|
url: '',
|
|
documentType: 'url',
|
|
pdfFile: null,
|
|
};
|
|
|
|
const state = reactive({ ...initialState });
|
|
const fileInputRef = ref(null);
|
|
|
|
const validationRules = {
|
|
url: {
|
|
required: requiredIf(() => state.documentType === 'url'),
|
|
url: requiredIf(() => state.documentType === 'url' && url),
|
|
minLength: requiredIf(() => state.documentType === 'url' && minLength(1)),
|
|
},
|
|
pdfFile: {
|
|
required: requiredIf(() => state.documentType === 'pdf'),
|
|
},
|
|
};
|
|
|
|
const documentTypeOptions = [
|
|
{ value: 'url', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') },
|
|
{ value: 'pdf', label: t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') },
|
|
];
|
|
|
|
const v$ = useVuelidate(validationRules, state);
|
|
|
|
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
|
|
|
const hasPdfFileError = computed(() => v$.value.pdfFile.$error);
|
|
|
|
const getErrorMessage = (field, errorKey) => {
|
|
return v$.value[field].$error
|
|
? t(`CAPTAIN.DOCUMENTS.FORM.${errorKey}.ERROR`)
|
|
: '';
|
|
};
|
|
|
|
const formErrors = computed(() => ({
|
|
url: getErrorMessage('url', 'URL'),
|
|
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
|
|
}));
|
|
|
|
const handleCancel = () => emit('cancel');
|
|
|
|
const handleFileChange = event => {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
if (file.type !== 'application/pdf') {
|
|
useAlert(t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.INVALID_TYPE'));
|
|
event.target.value = '';
|
|
return;
|
|
}
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
// 10MB
|
|
useAlert(t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.TOO_LARGE'));
|
|
event.target.value = '';
|
|
return;
|
|
}
|
|
state.pdfFile = file;
|
|
state.name = file.name.replace(/\.pdf$/i, '');
|
|
}
|
|
};
|
|
|
|
const openFileDialog = () => {
|
|
// Use nextTick to ensure the ref is available
|
|
nextTick(() => {
|
|
if (fileInputRef.value) {
|
|
fileInputRef.value.click();
|
|
}
|
|
});
|
|
};
|
|
|
|
const prepareDocumentDetails = () => {
|
|
const formData = new FormData();
|
|
formData.append('document[assistant_id]', props.assistantId);
|
|
|
|
if (state.documentType === 'url') {
|
|
formData.append('document[external_link]', state.url);
|
|
formData.append('document[name]', state.name || state.url);
|
|
} else {
|
|
formData.append('document[pdf_file]', state.pdfFile);
|
|
formData.append(
|
|
'document[name]',
|
|
state.name || state.pdfFile.name.replace('.pdf', '')
|
|
);
|
|
// No need to send external_link for PDF - it's auto-generated in the backend
|
|
}
|
|
|
|
return formData;
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
const isFormValid = await v$.value.$validate();
|
|
if (!isFormValid) {
|
|
return;
|
|
}
|
|
|
|
emit('submit', prepareDocumentDetails());
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
|
<div class="flex flex-col gap-1">
|
|
<label
|
|
for="documentType"
|
|
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
|
>
|
|
{{ t('CAPTAIN.DOCUMENTS.FORM.TYPE.LABEL') }}
|
|
</label>
|
|
<ComboBox
|
|
id="documentType"
|
|
v-model="state.documentType"
|
|
:options="documentTypeOptions"
|
|
class="[&>div>button]:bg-n-alpha-black2"
|
|
/>
|
|
</div>
|
|
|
|
<Input
|
|
v-if="state.documentType === 'url'"
|
|
v-model="state.url"
|
|
:label="t('CAPTAIN.DOCUMENTS.FORM.URL.LABEL')"
|
|
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.URL.PLACEHOLDER')"
|
|
:message="formErrors.url"
|
|
:message-type="formErrors.url ? 'error' : 'info'"
|
|
/>
|
|
|
|
<div v-if="state.documentType === 'pdf'" class="flex flex-col gap-2">
|
|
<label class="text-sm font-medium text-n-slate-12">
|
|
{{ t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.LABEL') }}
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
ref="fileInputRef"
|
|
type="file"
|
|
accept=".pdf"
|
|
class="hidden"
|
|
@change="handleFileChange"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
:color="hasPdfFileError ? 'ruby' : 'slate'"
|
|
:variant="hasPdfFileError ? 'outline' : 'solid'"
|
|
class="!w-full !h-auto !justify-between !py-4"
|
|
@click="openFileDialog"
|
|
>
|
|
<template #default>
|
|
<div class="flex gap-2 items-center">
|
|
<div
|
|
class="flex justify-center items-center w-10 h-10 rounded-lg bg-n-slate-3"
|
|
>
|
|
<i class="text-xl i-ph-file-pdf text-n-slate-11" />
|
|
</div>
|
|
<div class="flex flex-col flex-1 gap-1 items-start">
|
|
<p class="m-0 text-sm font-medium text-n-slate-12">
|
|
{{
|
|
state.pdfFile
|
|
? state.pdfFile.name
|
|
: t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.CHOOSE_FILE')
|
|
}}
|
|
</p>
|
|
<p class="m-0 text-xs text-n-slate-11">
|
|
{{
|
|
state.pdfFile
|
|
? `${(state.pdfFile.size / 1024 / 1024).toFixed(2)} MB`
|
|
: t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.HELP_TEXT')
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<i class="i-lucide-upload text-n-slate-11" />
|
|
</template>
|
|
</Button>
|
|
</div>
|
|
<p v-if="formErrors.pdfFile" class="text-xs text-n-ruby-9">
|
|
{{ formErrors.pdfFile }}
|
|
</p>
|
|
</div>
|
|
|
|
<Input
|
|
v-model="state.name"
|
|
:label="t('CAPTAIN.DOCUMENTS.FORM.NAME.LABEL')"
|
|
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.NAME.PLACEHOLDER')"
|
|
/>
|
|
|
|
<div class="flex gap-3 justify-between items-center w-full">
|
|
<Button
|
|
type="button"
|
|
variant="faded"
|
|
color="slate"
|
|
:label="t('CAPTAIN.FORM.CANCEL')"
|
|
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
|
@click="handleCancel"
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
:label="t('CAPTAIN.FORM.CREATE')"
|
|
class="w-full"
|
|
:is-loading="isLoading"
|
|
:disabled="isLoading"
|
|
/>
|
|
</div>
|
|
</form>
|
|
</template>
|