Files
chatwoot/app/javascript/dashboard/components-next/captain/pageComponents/document/DocumentForm.vue
T
2025-07-10 13:52:53 +07:00

308 lines
8.9 KiB
Vue

<script setup>
import { reactive, computed, ref } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength, url } from '@vuelidate/validators';
import { useMapGetter } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { formatBytes } from 'shared/helpers/FileHelper';
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';
import FileUpload from 'dashboard/components-next/fileUpload/FileUpload.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const emit = defineEmits(['submit', 'cancel']);
const { t } = useI18n();
// Constants
const SOURCE_TYPE = {
URL: 'url',
PDF: 'pdf',
};
const MESSAGE_TYPE = {
ERROR: 'error',
INFO: 'info',
};
const DOCUMENT_TYPE = {
URL: 'url',
PDF: 'pdf',
};
const FIELD_NAME = {
URL: 'url',
ASSISTANT_ID: 'assistantId',
};
const FILE_ACCEPT_TYPE = 'application/pdf';
const MAX_FILE_SIZE_MB = 25;
const formState = {
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
assistants: useMapGetter('captainAssistants/getRecords'),
};
const initialState = {
url: '',
assistantId: null,
selectedFile: null,
sourceType: SOURCE_TYPE.URL,
};
const state = reactive({ ...initialState });
const uploadError = ref('');
const validationRules = computed(() => {
const rules = {
assistantId: { required },
};
if (state.sourceType === SOURCE_TYPE.URL) {
rules.url = { required, url, minLength: minLength(1) };
}
return rules;
});
const assistantList = computed(() =>
formState.assistants.value.map(assistant => ({
value: assistant.id,
label: assistant.name,
}))
);
const v$ = useVuelidate(validationRules, state);
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
const formattedFileSize = computed(() => {
if (!state.selectedFile) return '';
return formatBytes(state.selectedFile.size);
});
const getErrorMessage = (field, errorMessage) => {
return v$.value[field]?.$error ? errorMessage : '';
};
const formErrors = computed(() => ({
url: getErrorMessage(
FIELD_NAME.URL,
t('CAPTAIN.DOCUMENTS.CREATE.FORM.URL.ERROR')
),
assistantId: getErrorMessage(
FIELD_NAME.ASSISTANT_ID,
t('CAPTAIN.DOCUMENTS.CREATE.FORM.ASSISTANT.ERROR')
),
}));
const handleCancel = () => emit('cancel');
const prepareDocumentDetails = () => {
if (state.sourceType === SOURCE_TYPE.PDF) {
return {
type: DOCUMENT_TYPE.PDF,
pdf_document: state.selectedFile,
assistant_id: state.assistantId,
};
}
return {
type: DOCUMENT_TYPE.URL,
external_link: state.url,
assistant_id: state.assistantId,
};
};
const handleSubmit = async () => {
uploadError.value = '';
// Validate form
const isFormValid = await v$.value.$validate();
if (!isFormValid) {
return;
}
// For PDF uploads, check if file is selected
if (state.sourceType === SOURCE_TYPE.PDF && !state.selectedFile) {
uploadError.value = t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.ERROR');
return;
}
emit('submit', prepareDocumentDetails());
};
// PDF Upload handlers
const handleFileSelected = file => {
state.selectedFile = file;
uploadError.value = '';
};
const handleFileError = error => {
uploadError.value = error;
state.selectedFile = null;
};
const clearSelectedFile = () => {
state.selectedFile = null;
uploadError.value = '';
};
const handleSourceTypeChange = type => {
state.sourceType = type;
uploadError.value = '';
state.selectedFile = null;
state.url = '';
};
</script>
<template>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<!-- Source Type Selection -->
<div class="flex flex-col gap-2">
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.LABEL') }}
</label>
<div class="flex gap-2">
<button
type="button"
class="flex-1 p-3 rounded-lg bg-n-alpha-2 hover:border-n-slate-7 transition-colors focus:outline-none"
:class="{
'ring-2 ring-n-brand text-n-brand border-0':
state.sourceType === SOURCE_TYPE.URL,
'border-2 border-n-slate-7 text-n-slate-11':
state.sourceType !== SOURCE_TYPE.URL,
}"
@click="handleSourceTypeChange(SOURCE_TYPE.URL)"
>
<div class="flex items-center justify-center gap-2">
<Icon icon="i-lucide-link" class="size-5" />
<span class="text-sm font-medium">{{
$t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.WEBSITE_URL')
}}</span>
</div>
</button>
<button
type="button"
class="flex-1 p-3 rounded-lg bg-n-alpha-2 hover:border-n-slate-7 transition-colors focus:outline-none"
:class="{
'ring-2 ring-n-brand text-n-brand border-0':
state.sourceType === SOURCE_TYPE.PDF,
'border-2 border-n-slate-7 text-n-slate-11':
state.sourceType !== SOURCE_TYPE.PDF,
}"
@click="handleSourceTypeChange(SOURCE_TYPE.PDF)"
>
<div class="flex items-center justify-center gap-2">
<Icon icon="i-lucide-file" class="size-5" />
<span class="text-sm font-medium">{{
$t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.PDF_UPLOAD')
}}</span>
</div>
</button>
</div>
</div>
<!-- URL Input (when URL is selected) -->
<div v-if="state.sourceType === SOURCE_TYPE.URL">
<Input
v-model="state.url"
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.URL.LABEL')"
:placeholder="t('CAPTAIN.DOCUMENTS.CREATE.FORM.URL.PLACEHOLDER')"
:message="formErrors.url"
:message-type="formErrors.url ? MESSAGE_TYPE.ERROR : MESSAGE_TYPE.INFO"
/>
</div>
<!-- PDF Upload (when PDF is selected) -->
<div
v-if="state.sourceType === SOURCE_TYPE.PDF"
class="flex flex-col gap-2"
>
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.LABEL') }}
</label>
<!-- File upload area -->
<div v-if="!state.selectedFile">
<FileUpload
:accept="FILE_ACCEPT_TYPE"
:max-size-m-b="MAX_FILE_SIZE_MB"
:placeholder="
t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.PLACEHOLDER')
"
:upload-text="
t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.UPLOAD_TEXT')
"
:drag-text="t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.DROP_TEXT')"
@file-selected="handleFileSelected"
@file-error="handleFileError"
/>
</div>
<!-- Selected file display -->
<div v-else class="p-3 bg-n-alpha-2 rounded-lg border border-n-slate-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon icon="i-lucide-file" class="size-5 text-n-ruby-9" />
<div>
<p class="text-sm font-medium text-n-slate-12">
{{ state.selectedFile.name }}
</p>
<p class="text-xs text-n-slate-8">
{{ formattedFileSize }}
</p>
</div>
</div>
<button
type="button"
class="p-1 rounded hover:bg-n-alpha-3 text-n-slate-8 hover:text-n-slate-10"
@click="clearSelectedFile"
>
<Icon icon="i-lucide-x" class="size-4" />
</button>
</div>
</div>
<!-- Upload error -->
<p v-if="uploadError" class="text-sm text-n-ruby-9">{{ uploadError }}</p>
</div>
<!-- Assistant Selection -->
<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.CREATE.FORM.ASSISTANT.LABEL') }}
</label>
<ComboBox
id="assistant"
v-model="state.assistantId"
:options="assistantList"
:has-error="!!formErrors.assistantId"
:placeholder="t('CAPTAIN.DOCUMENTS.CREATE.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>
<!-- Action Buttons -->
<div class="flex items-center justify-between w-full gap-3">
<Button
type="button"
variant="faded"
color="slate"
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ACTIONS.CANCEL')"
class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
@click="handleCancel"
/>
<Button
type="submit"
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ACTIONS.CREATE')"
class="w-full"
:is-loading="isLoading"
:disabled="isLoading"
/>
</div>
</form>
</template>