Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de4f78854f | ||
|
|
d5cb337d5e | ||
|
|
2aeeaf0917 | ||
|
|
124a2d50ba | ||
|
|
e49a2eb606 | ||
|
|
91cb159ef0 | ||
|
|
45f9aeec10 | ||
|
|
553fcbc5e9 | ||
|
|
ba65a5a534 | ||
|
|
6656b042b8 | ||
|
|
235ce3479c | ||
|
|
8f27ba081c |
@@ -15,6 +15,14 @@ class CaptainDocument extends ApiClient {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadPdf(formData) {
|
||||||
|
return axios.post(`${this.url}/upload_pdf`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CaptainDocument();
|
export default new CaptainDocument();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { usePolicy } from 'dashboard/composables/usePolicy';
|
|||||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
@@ -26,6 +27,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
file: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -63,6 +68,22 @@ const menuItems = computed(() => {
|
|||||||
|
|
||||||
const createdAt = computed(() => dynamicTime(props.createdAt));
|
const createdAt = computed(() => dynamicTime(props.createdAt));
|
||||||
|
|
||||||
|
const displayLink = computed(() => {
|
||||||
|
// For PDF uploads, show the filename instead of the generated external_link
|
||||||
|
if (props.file?.filename) {
|
||||||
|
return props.file.filename;
|
||||||
|
}
|
||||||
|
return props.externalLink;
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualLink = computed(() => {
|
||||||
|
// For PDF uploads, return the actual file URL; for URLs, return the external_link
|
||||||
|
if (props.file?.url) {
|
||||||
|
return props.file.url;
|
||||||
|
}
|
||||||
|
return props.externalLink;
|
||||||
|
});
|
||||||
|
|
||||||
const handleAction = ({ action, value }) => {
|
const handleAction = ({ action, value }) => {
|
||||||
toggleDropdown(false);
|
toggleDropdown(false);
|
||||||
emit('action', { action, value, id: props.id });
|
emit('action', { action, value, id: props.id });
|
||||||
@@ -103,12 +124,15 @@ const handleAction = ({ action, value }) => {
|
|||||||
<i class="i-woot-captain" />
|
<i class="i-woot-captain" />
|
||||||
{{ assistant?.name || '' }}
|
{{ assistant?.name || '' }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<a
|
||||||
class="text-n-slate-11 text-sm truncate flex justify-start flex-1 items-center gap-1"
|
:href="actualLink"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-n-slate-11 text-sm truncate flex justify-start flex-1 items-center gap-1 hover:text-n-slate-12 transition-colors"
|
||||||
>
|
>
|
||||||
<i class="i-ph-link-simple shrink-0" />
|
<Icon icon="i-lucide-link" class="shrink-0 size-4" />
|
||||||
<span class="truncate">{{ externalLink }}</span>
|
<span class="truncate">{{ displayLink }}</span>
|
||||||
</span>
|
</a>
|
||||||
<div class="shrink-0 text-sm text-n-slate-11 line-clamp-1">
|
<div class="shrink-0 text-sm text-n-slate-11 line-clamp-1">
|
||||||
{{ createdAt }}
|
{{ createdAt }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+23
-10
@@ -3,27 +3,40 @@ import { ref } 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 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';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close', 'success']);
|
||||||
const { t } = useI18n();
|
|
||||||
|
// Constants
|
||||||
|
const DOCUMENT_TYPE = {
|
||||||
|
PDF: 'pdf',
|
||||||
|
URL: 'url',
|
||||||
|
};
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const dialogRef = ref(null);
|
const dialogRef = ref(null);
|
||||||
const documentForm = ref(null);
|
const documentForm = ref(null);
|
||||||
|
|
||||||
const i18nKey = 'CAPTAIN.DOCUMENTS.CREATE';
|
|
||||||
|
|
||||||
const handleSubmit = async newDocument => {
|
const handleSubmit = async newDocument => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('captainDocuments/create', newDocument);
|
await store.dispatch('captainDocuments/createDocument', newDocument);
|
||||||
useAlert(t(`${i18nKey}.SUCCESS_MESSAGE`));
|
|
||||||
|
// Show appropriate success message based on document type
|
||||||
|
const successMessage =
|
||||||
|
newDocument.type === DOCUMENT_TYPE.PDF
|
||||||
|
? t('CAPTAIN.DOCUMENTS.CREATE.FORM.PDF_UPLOAD.SUCCESS_MESSAGE')
|
||||||
|
: t('CAPTAIN.DOCUMENTS.CREATE.SUCCESS_MESSAGE');
|
||||||
|
|
||||||
|
useAlert(successMessage);
|
||||||
|
emit('success');
|
||||||
dialogRef.value.close();
|
dialogRef.value.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.message || t(`${i18nKey}.ERROR_MESSAGE`);
|
error?.response?.data?.message ||
|
||||||
|
t('CAPTAIN.DOCUMENTS.CREATE.ERROR_MESSAGE');
|
||||||
useAlert(errorMessage);
|
useAlert(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -42,8 +55,8 @@ defineExpose({ dialogRef });
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
:title="$t(`${i18nKey}.TITLE`)"
|
:title="$t('CAPTAIN.DOCUMENTS.CREATE.TITLE')"
|
||||||
:description="$t('CAPTAIN.DOCUMENTS.FORM_DESCRIPTION')"
|
:description="$t('CAPTAIN.DOCUMENTS.CREATE.DESCRIPTION')"
|
||||||
:show-cancel-button="false"
|
:show-cancel-button="false"
|
||||||
:show-confirm-button="false"
|
:show-confirm-button="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
|
|||||||
+223
-30
@@ -1,34 +1,70 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, computed } from 'vue';
|
import { reactive, computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required, minLength, url } from '@vuelidate/validators';
|
import { required, minLength, url } from '@vuelidate/validators';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
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 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';
|
||||||
|
import FileUpload from 'dashboard/components-next/fileUpload/FileUpload.vue';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['submit', 'cancel']);
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
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 = {
|
const formState = {
|
||||||
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
|
uiFlags: useMapGetter('captainDocuments/getUIFlags'),
|
||||||
assistants: useMapGetter('captainAssistants/getRecords'),
|
assistants: useMapGetter('captainAssistants/getRecords'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
name: '',
|
url: '',
|
||||||
assistantId: null,
|
assistantId: null,
|
||||||
|
selectedFile: null,
|
||||||
|
sourceType: SOURCE_TYPE.URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = reactive({ ...initialState });
|
const state = reactive({ ...initialState });
|
||||||
|
const uploadError = ref('');
|
||||||
|
|
||||||
const validationRules = {
|
const validationRules = computed(() => {
|
||||||
url: { required, url, minLength: minLength(1) },
|
const rules = {
|
||||||
assistantId: { required },
|
assistantId: { required },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (state.sourceType === SOURCE_TYPE.URL) {
|
||||||
|
rules.url = { required, url, minLength: minLength(1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
});
|
||||||
|
|
||||||
const assistantList = computed(() =>
|
const assistantList = computed(() =>
|
||||||
formState.assistants.value.map(assistant => ({
|
formState.assistants.value.map(assistant => ({
|
||||||
@@ -41,70 +77,227 @@ const v$ = useVuelidate(validationRules, state);
|
|||||||
|
|
||||||
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
||||||
|
|
||||||
const getErrorMessage = (field, errorKey) => {
|
const formattedFileSize = computed(() => {
|
||||||
return v$.value[field].$error
|
if (!state.selectedFile) return '';
|
||||||
? t(`CAPTAIN.DOCUMENTS.FORM.${errorKey}.ERROR`)
|
return formatBytes(state.selectedFile.size);
|
||||||
: '';
|
});
|
||||||
|
|
||||||
|
const getErrorMessage = (field, errorMessage) => {
|
||||||
|
return v$.value[field]?.$error ? errorMessage : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formErrors = computed(() => ({
|
const formErrors = computed(() => ({
|
||||||
url: getErrorMessage('url', 'URL'),
|
url: getErrorMessage(
|
||||||
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
|
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 handleCancel = () => emit('cancel');
|
||||||
|
|
||||||
const prepareDocumentDetails = () => ({
|
const prepareDocumentDetails = () => {
|
||||||
external_link: state.url,
|
if (state.sourceType === SOURCE_TYPE.PDF) {
|
||||||
assistant_id: state.assistantId,
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
|
uploadError.value = '';
|
||||||
|
|
||||||
|
// Validate form
|
||||||
const isFormValid = await v$.value.$validate();
|
const isFormValid = await v$.value.$validate();
|
||||||
if (!isFormValid) {
|
if (!isFormValid) {
|
||||||
return;
|
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());
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
<Input
|
<!-- Source Type Selection -->
|
||||||
v-model="state.url"
|
<div class="flex flex-col gap-2">
|
||||||
:label="t('CAPTAIN.DOCUMENTS.FORM.URL.LABEL')"
|
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.URL.PLACEHOLDER')"
|
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.SOURCE_TYPE.LABEL') }}
|
||||||
:message="formErrors.url"
|
</label>
|
||||||
:message-type="formErrors.url ? 'error' : 'info'"
|
<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">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||||
{{ t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.LABEL') }}
|
{{ $t('CAPTAIN.DOCUMENTS.CREATE.FORM.ASSISTANT.LABEL') }}
|
||||||
</label>
|
</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
id="assistant"
|
id="assistant"
|
||||||
v-model="state.assistantId"
|
v-model="state.assistantId"
|
||||||
:options="assistantList"
|
:options="assistantList"
|
||||||
:has-error="!!formErrors.assistantId"
|
:has-error="!!formErrors.assistantId"
|
||||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.PLACEHOLDER')"
|
: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"
|
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"
|
:message="formErrors.assistantId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
<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"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
color="slate"
|
color="slate"
|
||||||
:label="t('CAPTAIN.FORM.CANCEL')"
|
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ACTIONS.CANCEL')"
|
||||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
class="w-full bg-n-alpha-2 text-n-blue-11 hover:bg-n-alpha-3"
|
||||||
@click="handleCancel"
|
@click="handleCancel"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
:label="t('CAPTAIN.FORM.CREATE')"
|
:label="t('CAPTAIN.DOCUMENTS.CREATE.FORM.ACTIONS.CREATE')"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: '*',
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxSizeMB: {
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
uploadText: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
dragText: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['fileSelected', 'fileError']);
|
||||||
|
const { t } = useI18n();
|
||||||
|
const fileInput = ref(null);
|
||||||
|
const isDragOver = ref(false);
|
||||||
|
|
||||||
|
const acceptTypes = computed(() => props.accept);
|
||||||
|
const maxSize = computed(() => props.maxSizeMB * 1024 * 1024);
|
||||||
|
|
||||||
|
const handleFileClick = () => {
|
||||||
|
if (!props.disabled) {
|
||||||
|
fileInput.value?.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateFile = file => {
|
||||||
|
// Check file size
|
||||||
|
if (file.size > maxSize.value) {
|
||||||
|
const errorMsg = t(
|
||||||
|
'CAPTAIN.DOCUMENTS.CREATE.FORM.FILE_UPLOAD.ERROR_TOO_LARGE',
|
||||||
|
{
|
||||||
|
fileName: file.name,
|
||||||
|
maxSize: props.maxSizeMB,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
emit('fileError', errorMsg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file type if specified
|
||||||
|
if (
|
||||||
|
props.accept !== '*' &&
|
||||||
|
!file.type.match(new RegExp(props.accept.replace(/\*/g, '.*')))
|
||||||
|
) {
|
||||||
|
const errorMsg = t(
|
||||||
|
'CAPTAIN.DOCUMENTS.CREATE.FORM.FILE_UPLOAD.ERROR_TYPE_NOT_SUPPORTED',
|
||||||
|
{
|
||||||
|
acceptedTypes: props.accept,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
emit('fileError', errorMsg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = event => {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (validateFile(file)) {
|
||||||
|
emit('fileSelected', file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset input value to allow selecting the same file again
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!props.disabled) {
|
||||||
|
isDragOver.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Only set to false if we're leaving the drop zone completely
|
||||||
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||||
|
isDragOver.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
isDragOver.value = false;
|
||||||
|
|
||||||
|
if (props.disabled) return;
|
||||||
|
|
||||||
|
const files = event.dataTransfer.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (validateFile(file)) {
|
||||||
|
emit('fileSelected', file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayText = computed(() => {
|
||||||
|
if (isDragOver.value && props.dragText) {
|
||||||
|
return props.dragText;
|
||||||
|
}
|
||||||
|
return props.uploadText || props.placeholder;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-n-slate-6 rounded-lg p-6 bg-n-alpha-2 hover:bg-n-alpha-3 cursor-pointer transition-all duration-200 ease-in-out flex flex-col items-center justify-center text-center min-h-[120px] hover:border-n-slate-7"
|
||||||
|
:class="{
|
||||||
|
'border-n-blue-9 bg-n-blue-2/50': isDragOver,
|
||||||
|
'opacity-50 cursor-not-allowed hover:bg-n-alpha-2 hover:border-n-slate-6':
|
||||||
|
disabled,
|
||||||
|
}"
|
||||||
|
@dragover="handleDragOver"
|
||||||
|
@dragleave="handleDragLeave"
|
||||||
|
@drop="handleDrop"
|
||||||
|
@click="handleFileClick"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
:accept="acceptTypes"
|
||||||
|
:multiple="multiple"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-3">
|
||||||
|
<div class="p-2 rounded-full bg-n-alpha-3">
|
||||||
|
<Icon icon="i-lucide-file-up" class="size-6 text-n-slate-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-n-slate-11">
|
||||||
|
{{ displayText }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-n-slate-8 mt-1">
|
||||||
|
{{
|
||||||
|
$t('CAPTAIN.DOCUMENTS.CREATE.FORM.FILE_UPLOAD.MAX_SIZE', {
|
||||||
|
size: maxSizeMB,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
"PLACEHOLDER": "Search",
|
"PLACEHOLDER": "Search",
|
||||||
"EMPTY_STATE": "No results found"
|
"EMPTY_STATE": "No results found"
|
||||||
},
|
},
|
||||||
"CLOSE": "Close"
|
"CLOSE": "Close",
|
||||||
|
"FILE_SIZE": {
|
||||||
|
"MB": "MB"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -501,9 +501,44 @@
|
|||||||
},
|
},
|
||||||
"FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.",
|
"FORM_DESCRIPTION": "Enter the URL of the document to add it as a knowledge source and choose the assistant to associate it with.",
|
||||||
"CREATE": {
|
"CREATE": {
|
||||||
"TITLE": "Add a document",
|
"TITLE": "Create New Document",
|
||||||
|
"DESCRIPTION": "Add a new document from a website URL or by uploading a PDF file",
|
||||||
"SUCCESS_MESSAGE": "The document has been successfully created",
|
"SUCCESS_MESSAGE": "The document has been successfully created",
|
||||||
"ERROR_MESSAGE": "There was an error creating the document, please try again."
|
"ERROR_MESSAGE": "There was an error creating the document, please try again.",
|
||||||
|
"FORM": {
|
||||||
|
"SOURCE_TYPE": {
|
||||||
|
"LABEL": "Choose Source Type",
|
||||||
|
"WEBSITE_URL": "Website URL",
|
||||||
|
"PDF_UPLOAD": "PDF Upload"
|
||||||
|
},
|
||||||
|
"PDF_UPLOAD": {
|
||||||
|
"LABEL": "Upload PDF Document",
|
||||||
|
"SUCCESS_MESSAGE": "PDF uploaded successfully! Processing will begin shortly.",
|
||||||
|
"ERROR": "Please select a PDF file to upload.",
|
||||||
|
"PLACEHOLDER": "Select a PDF file",
|
||||||
|
"UPLOAD_TEXT": "Click to select PDF or drag and drop",
|
||||||
|
"DROP_TEXT": "Drop PDF file here"
|
||||||
|
},
|
||||||
|
"ASSISTANT": {
|
||||||
|
"LABEL": "Select Assistant",
|
||||||
|
"PLACEHOLDER": "Choose an assistant",
|
||||||
|
"ERROR": "Please select an assistant"
|
||||||
|
},
|
||||||
|
"URL": {
|
||||||
|
"LABEL": "Website URL",
|
||||||
|
"PLACEHOLDER": "Enter the website URL to crawl",
|
||||||
|
"ERROR": "Please enter a valid URL"
|
||||||
|
},
|
||||||
|
"FILE_UPLOAD": {
|
||||||
|
"MAX_SIZE": "Maximum file size: {size}MB",
|
||||||
|
"ERROR_TOO_LARGE": "File \"{fileName}\" is too large. Maximum size is {maxSize}MB.",
|
||||||
|
"ERROR_TYPE_NOT_SUPPORTED": "File type not supported. Only {acceptedTypes} files are allowed."
|
||||||
|
},
|
||||||
|
"ACTIONS": {
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"CREATE": "Create Document"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"URL": {
|
"URL": {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ onMounted(() => {
|
|||||||
:key="doc.id"
|
:key="doc.id"
|
||||||
:name="doc.name || doc.external_link"
|
:name="doc.name || doc.external_link"
|
||||||
:external-link="doc.external_link"
|
:external-link="doc.external_link"
|
||||||
|
:file="doc.file"
|
||||||
:assistant="doc.assistant"
|
:assistant="doc.assistant"
|
||||||
:created-at="doc.created_at"
|
:created-at="doc.created_at"
|
||||||
@action="handleAction"
|
@action="handleAction"
|
||||||
|
|||||||
@@ -1,7 +1,57 @@
|
|||||||
import CaptainDocumentAPI from 'dashboard/api/captain/document';
|
import CaptainDocumentAPI from 'dashboard/api/captain/document';
|
||||||
import { createStore } from './storeFactory';
|
import { createStore } from './storeFactory';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const DOCUMENT_TYPE = {
|
||||||
|
PDF: 'pdf',
|
||||||
|
URL: 'url',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FORM_DATA_FIELDS = {
|
||||||
|
PDF_DOCUMENT: 'pdf_document',
|
||||||
|
ASSISTANT_ID: 'assistant_id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = mutationTypes => ({
|
||||||
|
async uploadPdf({ commit }, { pdf_document, assistant_id }) {
|
||||||
|
commit(mutationTypes.SET_UI_FLAG, { creatingItem: true });
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append(FORM_DATA_FIELDS.PDF_DOCUMENT, pdf_document);
|
||||||
|
formData.append(FORM_DATA_FIELDS.ASSISTANT_ID, assistant_id);
|
||||||
|
|
||||||
|
const response = await CaptainDocumentAPI.uploadPdf(formData);
|
||||||
|
const { data } = response;
|
||||||
|
// Use UPSERT like the create action for consistency
|
||||||
|
commit(mutationTypes.UPSERT, data);
|
||||||
|
commit(mutationTypes.SET_UI_FLAG, { creatingItem: false });
|
||||||
|
return Promise.resolve(data);
|
||||||
|
} catch (error) {
|
||||||
|
commit(mutationTypes.SET_UI_FLAG, { creatingItem: false });
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDocument({ dispatch }, documentData) {
|
||||||
|
// Unified method to handle both URL and PDF document creation
|
||||||
|
if (documentData.type === DOCUMENT_TYPE.PDF) {
|
||||||
|
return dispatch('uploadPdf', {
|
||||||
|
pdf_document: documentData.pdf_document,
|
||||||
|
assistant_id: documentData.assistant_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// URL creation uses the standard create action from storeFactory
|
||||||
|
return dispatch('create', {
|
||||||
|
document: {
|
||||||
|
external_link: documentData.external_link,
|
||||||
|
assistant_id: documentData.assistant_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
name: 'CaptainDocument',
|
name: 'CaptainDocument',
|
||||||
API: CaptainDocumentAPI,
|
API: CaptainDocumentAPI,
|
||||||
|
actions,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user