mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
Merge branch 'develop' into feat/voice-channel
This commit is contained in:
@@ -4,6 +4,10 @@ import { useToggle } from '@vueuse/core';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import { usePolicy } from 'dashboard/composables/usePolicy';
|
import { usePolicy } from 'dashboard/composables/usePolicy';
|
||||||
|
import {
|
||||||
|
isPdfDocument,
|
||||||
|
formatDocumentLink,
|
||||||
|
} from 'shared/helpers/documentHelper';
|
||||||
|
|
||||||
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';
|
||||||
@@ -63,6 +67,11 @@ const menuItems = computed(() => {
|
|||||||
|
|
||||||
const createdAt = computed(() => dynamicTime(props.createdAt));
|
const createdAt = computed(() => dynamicTime(props.createdAt));
|
||||||
|
|
||||||
|
const displayLink = computed(() => formatDocumentLink(props.externalLink));
|
||||||
|
const linkIcon = computed(() =>
|
||||||
|
isPdfDocument(props.externalLink) ? 'i-ph-file-pdf' : 'i-ph-link-simple'
|
||||||
|
);
|
||||||
|
|
||||||
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 });
|
||||||
@@ -71,14 +80,14 @@ const handleAction = ({ action, value }) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardLayout>
|
<CardLayout>
|
||||||
<div class="flex justify-between w-full gap-1">
|
<div class="flex gap-1 justify-between w-full">
|
||||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
<span class="text-base text-n-slate-12 line-clamp-1">
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
<div
|
<div
|
||||||
v-on-clickaway="() => toggleDropdown(false)"
|
v-on-clickaway="() => toggleDropdown(false)"
|
||||||
class="relative flex items-center group"
|
class="flex relative items-center group"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon="i-lucide-ellipsis-vertical"
|
icon="i-lucide-ellipsis-vertical"
|
||||||
@@ -90,26 +99,26 @@ const handleAction = ({ action, value }) => {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
v-if="showActionsDropdown"
|
v-if="showActionsDropdown"
|
||||||
:menu-items="menuItems"
|
:menu-items="menuItems"
|
||||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0 top-full"
|
class="top-full mt-1 ltr:right-0 rtl:left-0 xl:ltr:right-0 xl:rtl:left-0"
|
||||||
@action="handleAction($event)"
|
@action="handleAction($event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between w-full gap-4">
|
<div class="flex gap-4 justify-between items-center w-full">
|
||||||
<span
|
<span
|
||||||
class="text-sm shrink-0 truncate text-n-slate-11 flex items-center gap-1"
|
class="flex gap-1 items-center text-sm truncate shrink-0 text-n-slate-11"
|
||||||
>
|
>
|
||||||
<i class="i-woot-captain" />
|
<i class="i-woot-captain" />
|
||||||
{{ assistant?.name || '' }}
|
{{ assistant?.name || '' }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="text-n-slate-11 text-sm truncate flex justify-start flex-1 items-center gap-1"
|
class="flex flex-1 gap-1 justify-start items-center text-sm truncate text-n-slate-11"
|
||||||
>
|
>
|
||||||
<i class="i-ph-link-simple shrink-0" />
|
<i :class="linkIcon" class="shrink-0" />
|
||||||
<span class="truncate">{{ externalLink }}</span>
|
<span class="truncate">{{ displayLink }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="shrink-0 text-sm text-n-slate-11 line-clamp-1">
|
<div class="text-sm shrink-0 text-n-slate-11 line-clamp-1">
|
||||||
{{ createdAt }}
|
{{ createdAt }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
import DocumentForm from './DocumentForm.vue';
|
import DocumentForm from './DocumentForm.vue';
|
||||||
@@ -12,7 +13,6 @@ const { t } = useI18n();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const dialogRef = ref(null);
|
const dialogRef = ref(null);
|
||||||
const documentForm = ref(null);
|
|
||||||
|
|
||||||
const i18nKey = 'CAPTAIN.DOCUMENTS.CREATE';
|
const i18nKey = 'CAPTAIN.DOCUMENTS.CREATE';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const handleSubmit = async newDocument => {
|
|||||||
dialogRef.value.close();
|
dialogRef.value.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.message || t(`${i18nKey}.ERROR_MESSAGE`);
|
parseAPIErrorResponse(error) || t(`${i18nKey}.ERROR_MESSAGE`);
|
||||||
useAlert(errorMessage);
|
useAlert(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -48,11 +48,7 @@ defineExpose({ dialogRef });
|
|||||||
:show-confirm-button="false"
|
:show-confirm-button="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<DocumentForm
|
<DocumentForm @submit="handleSubmit" @cancel="handleCancel" />
|
||||||
ref="documentForm"
|
|
||||||
@submit="handleSubmit"
|
|
||||||
@cancel="handleCancel"
|
|
||||||
/>
|
|
||||||
<template #footer />
|
<template #footer />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, computed } from 'vue';
|
import { reactive, computed, ref, nextTick } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required, minLength, url } from '@vuelidate/validators';
|
import { required, minLength, requiredIf, url } from '@vuelidate/validators';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
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';
|
||||||
@@ -11,6 +12,8 @@ import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
|||||||
|
|
||||||
const emit = defineEmits(['submit', 'cancel']);
|
const emit = defineEmits(['submit', 'cancel']);
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const formState = {
|
const formState = {
|
||||||
@@ -20,14 +23,25 @@ const formState = {
|
|||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
name: '',
|
name: '',
|
||||||
|
url: '',
|
||||||
assistantId: null,
|
assistantId: null,
|
||||||
|
documentType: 'url',
|
||||||
|
pdfFile: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = reactive({ ...initialState });
|
const state = reactive({ ...initialState });
|
||||||
|
const fileInputRef = ref(null);
|
||||||
|
|
||||||
const validationRules = {
|
const validationRules = {
|
||||||
url: { required, url, minLength: minLength(1) },
|
url: {
|
||||||
|
required: requiredIf(() => state.documentType === 'url'),
|
||||||
|
url: requiredIf(() => state.documentType === 'url' && url),
|
||||||
|
minLength: requiredIf(() => state.documentType === 'url' && minLength(1)),
|
||||||
|
},
|
||||||
assistantId: { required },
|
assistantId: { required },
|
||||||
|
pdfFile: {
|
||||||
|
required: requiredIf(() => state.documentType === 'pdf'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const assistantList = computed(() =>
|
const assistantList = computed(() =>
|
||||||
@@ -37,10 +51,17 @@ const assistantList = computed(() =>
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 v$ = useVuelidate(validationRules, state);
|
||||||
|
|
||||||
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
const isLoading = computed(() => formState.uiFlags.value.creatingItem);
|
||||||
|
|
||||||
|
const hasPdfFileError = computed(() => v$.value.pdfFile.$error);
|
||||||
|
|
||||||
const getErrorMessage = (field, errorKey) => {
|
const getErrorMessage = (field, errorKey) => {
|
||||||
return v$.value[field].$error
|
return v$.value[field].$error
|
||||||
? t(`CAPTAIN.DOCUMENTS.FORM.${errorKey}.ERROR`)
|
? t(`CAPTAIN.DOCUMENTS.FORM.${errorKey}.ERROR`)
|
||||||
@@ -50,14 +71,57 @@ const getErrorMessage = (field, errorKey) => {
|
|||||||
const formErrors = computed(() => ({
|
const formErrors = computed(() => ({
|
||||||
url: getErrorMessage('url', 'URL'),
|
url: getErrorMessage('url', 'URL'),
|
||||||
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
|
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
|
||||||
|
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleCancel = () => emit('cancel');
|
const handleCancel = () => emit('cancel');
|
||||||
|
|
||||||
const prepareDocumentDetails = () => ({
|
const handleFileChange = event => {
|
||||||
external_link: state.url,
|
const file = event.target.files[0];
|
||||||
assistant_id: state.assistantId,
|
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]', state.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 handleSubmit = async () => {
|
||||||
const isFormValid = await v$.value.$validate();
|
const isFormValid = await v$.value.$validate();
|
||||||
@@ -71,13 +135,89 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
<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
|
<Input
|
||||||
|
v-if="state.documentType === 'url'"
|
||||||
v-model="state.url"
|
v-model="state.url"
|
||||||
:label="t('CAPTAIN.DOCUMENTS.FORM.URL.LABEL')"
|
:label="t('CAPTAIN.DOCUMENTS.FORM.URL.LABEL')"
|
||||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.URL.PLACEHOLDER')"
|
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.URL.PLACEHOLDER')"
|
||||||
:message="formErrors.url"
|
:message="formErrors.url"
|
||||||
:message-type="formErrors.url ? 'error' : 'info'"
|
: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 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.FORM.ASSISTANT.LABEL') }}
|
||||||
@@ -88,12 +228,12 @@ const handleSubmit = async () => {
|
|||||||
:options="assistantList"
|
:options="assistantList"
|
||||||
:has-error="!!formErrors.assistantId"
|
:has-error="!!formErrors.assistantId"
|
||||||
:placeholder="t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.PLACEHOLDER')"
|
:placeholder="t('CAPTAIN.DOCUMENTS.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"
|
||||||
:message="formErrors.assistantId"
|
:message="formErrors.assistantId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between w-full gap-3">
|
<div class="flex gap-3 justify-between items-center w-full">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
|
|||||||
@@ -96,8 +96,12 @@ watch(
|
|||||||
:label="selectedLabel"
|
:label="selectedLabel"
|
||||||
trailing-icon
|
trailing-icon
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 [&:not(.focused)]:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:outline-n-weak focus:outline-n-brand"
|
class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 focus:outline-n-brand"
|
||||||
:class="{ focused: open }"
|
:class="{
|
||||||
|
focused: open,
|
||||||
|
'[&:not(.focused)]:dark:outline-n-weak [&:not(.focused)]:hover:enabled:outline-n-slate-6 [&:not(.focused)]:dark:hover:enabled:outline-n-slate-6':
|
||||||
|
!hasError,
|
||||||
|
}"
|
||||||
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
:icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
@click="toggleDropdown"
|
@click="toggleDropdown"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -807,6 +807,58 @@
|
|||||||
"ERROR_MESSAGE": "Unable to update portal"
|
"ERROR_MESSAGE": "Unable to update portal"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"PDF_UPLOAD": {
|
||||||
|
"TITLE": "Upload PDF Document",
|
||||||
|
"DESCRIPTION": "Upload a PDF document to automatically generate FAQs using AI",
|
||||||
|
"DRAG_DROP_TEXT": "Drag and drop your PDF file here, or click to select",
|
||||||
|
"SELECT_FILE": "Select PDF File",
|
||||||
|
"ADDITIONAL_CONTEXT_LABEL": "Additional Context (Optional)",
|
||||||
|
"ADDITIONAL_CONTEXT_PLACEHOLDER": "Provide any additional context or instructions for FAQ generation...",
|
||||||
|
"UPLOADING": "Uploading...",
|
||||||
|
"UPLOAD": "Upload & Process",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"ERROR_INVALID_TYPE": "Please select a valid PDF file",
|
||||||
|
"ERROR_FILE_TOO_LARGE": "File size must be less than 512MB",
|
||||||
|
"ERROR_UPLOAD_FAILED": "Failed to upload PDF. Please try again."
|
||||||
|
},
|
||||||
|
"PDF_DOCUMENTS": {
|
||||||
|
"TITLE": "PDF Documents",
|
||||||
|
"DESCRIPTION": "Manage uploaded PDF documents and generate FAQs from them",
|
||||||
|
"UPLOAD_PDF": "Upload PDF",
|
||||||
|
"UPLOAD_FIRST_PDF": "Upload your first PDF",
|
||||||
|
"UPLOADED_BY": "Uploaded by",
|
||||||
|
"GENERATE_FAQS": "Generate FAQs",
|
||||||
|
"GENERATING": "Generating...",
|
||||||
|
"CONFIRM_DELETE": "Are you sure you want to delete {filename}?",
|
||||||
|
"EMPTY_STATE": {
|
||||||
|
"TITLE": "No PDF documents yet",
|
||||||
|
"DESCRIPTION": "Upload PDF documents to automatically generate FAQs using AI"
|
||||||
|
},
|
||||||
|
"STATUS": {
|
||||||
|
"UPLOADED": "Ready",
|
||||||
|
"PROCESSING": "Processing",
|
||||||
|
"PROCESSED": "Completed",
|
||||||
|
"FAILED": "Failed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CONTENT_GENERATION": {
|
||||||
|
"TITLE": "Content Generation",
|
||||||
|
"DESCRIPTION": "Upload PDF documents to automatically generate FAQ content using AI",
|
||||||
|
"UPLOAD_TITLE": "Upload PDF Document",
|
||||||
|
"DRAG_DROP": "Drag and drop your PDF file here, or click to select",
|
||||||
|
"SELECT_FILE": "Select PDF File",
|
||||||
|
"UPLOADING": "Processing document...",
|
||||||
|
"UPLOAD_SUCCESS": "Document processed successfully!",
|
||||||
|
"UPLOAD_ERROR": "Failed to upload document. Please try again.",
|
||||||
|
"INVALID_FILE_TYPE": "Please select a valid PDF file",
|
||||||
|
"FILE_TOO_LARGE": "File size must be less than 512MB",
|
||||||
|
"GENERATED_CONTENT": "Generated FAQ Content",
|
||||||
|
"PUBLISH_SELECTED": "Publish Selected",
|
||||||
|
"PUBLISHING": "Publishing...",
|
||||||
|
"FROM_DOCUMENT": "From document",
|
||||||
|
"NO_CONTENT": "No generated content available. Upload a PDF document to get started.",
|
||||||
|
"LOADING": "Loading generated content..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -701,11 +701,28 @@
|
|||||||
"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": {
|
"FORM": {
|
||||||
|
"TYPE": {
|
||||||
|
"LABEL": "Document Type",
|
||||||
|
"URL": "URL",
|
||||||
|
"PDF": "PDF File"
|
||||||
|
},
|
||||||
"URL": {
|
"URL": {
|
||||||
"LABEL": "URL",
|
"LABEL": "URL",
|
||||||
"PLACEHOLDER": "Enter the URL of the document",
|
"PLACEHOLDER": "Enter the URL of the document",
|
||||||
"ERROR": "Please provide a valid URL for the document"
|
"ERROR": "Please provide a valid URL for the document"
|
||||||
},
|
},
|
||||||
|
"PDF_FILE": {
|
||||||
|
"LABEL": "PDF File",
|
||||||
|
"CHOOSE_FILE": "Choose PDF file",
|
||||||
|
"ERROR": "Please select a PDF file",
|
||||||
|
"HELP_TEXT": "Maximum file size: 10MB",
|
||||||
|
"INVALID_TYPE": "Please select a valid PDF file",
|
||||||
|
"TOO_LARGE": "File size exceeds 10MB limit"
|
||||||
|
},
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Document Name (Optional)",
|
||||||
|
"PLACEHOLDER": "Enter a name for the document"
|
||||||
|
},
|
||||||
"ASSISTANT": {
|
"ASSISTANT": {
|
||||||
"LABEL": "Assistant",
|
"LABEL": "Assistant",
|
||||||
"PLACEHOLDER": "Select the assistant",
|
"PLACEHOLDER": "Select the assistant",
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="h-full p-6 w-full max-w-full flex-shrink-0 flex-grow-0">
|
||||||
class="border border-n-weak bg-n-background h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col items-center justify-start h-full text-center">
|
<div class="flex flex-col items-center justify-start h-full text-center">
|
||||||
<div v-if="hasError" class="max-w-lg mx-auto text-center">
|
<div v-if="hasError" class="max-w-lg mx-auto text-center">
|
||||||
<h5>{{ errorStateMessage }}</h5>
|
<h5>{{ errorStateMessage }}</h5>
|
||||||
@@ -75,7 +73,7 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex flex-col items-center justify-center px-8 py-10 text-center shadow rounded-3xl outline outline-1 outline-n-weak"
|
class="flex flex-col items-center justify-center px-8 py-10 text-center rounded-2xl outline outline-1 outline-n-weak"
|
||||||
>
|
>
|
||||||
<h6 class="text-2xl font-medium">
|
<h6 class="text-2xl font-medium">
|
||||||
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.CONNECT_YOUR_INSTAGRAM_PROFILE') }}
|
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.CONNECT_YOUR_INSTAGRAM_PROFILE') }}
|
||||||
|
|||||||
38
app/javascript/shared/helpers/documentHelper.js
Normal file
38
app/javascript/shared/helpers/documentHelper.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Document Helper - utilities for document display and formatting
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Constants for document processing
|
||||||
|
const PDF_PREFIX = 'PDF:';
|
||||||
|
const TIMESTAMP_PATTERN = /_\d{14}(?=\.pdf$)/; // Format: _YYYYMMDDHHMMSS before .pdf extension
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a document is a PDF based on its external link
|
||||||
|
* @param {string} externalLink - The external link string
|
||||||
|
* @returns {boolean} True if the document is a PDF
|
||||||
|
*/
|
||||||
|
export const isPdfDocument = externalLink => {
|
||||||
|
if (!externalLink) return false;
|
||||||
|
return externalLink.startsWith(PDF_PREFIX);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the display link for documents
|
||||||
|
* For PDF documents: removes 'PDF:' prefix and timestamp suffix
|
||||||
|
* For regular URLs: returns as-is
|
||||||
|
*
|
||||||
|
* @param {string} externalLink - The external link string
|
||||||
|
* @returns {string} Formatted display link
|
||||||
|
*/
|
||||||
|
export const formatDocumentLink = externalLink => {
|
||||||
|
if (!externalLink) return '';
|
||||||
|
|
||||||
|
if (isPdfDocument(externalLink)) {
|
||||||
|
// Remove 'PDF:' prefix
|
||||||
|
const fullName = externalLink.substring(PDF_PREFIX.length);
|
||||||
|
// Remove timestamp suffix if present
|
||||||
|
return fullName.replace(TIMESTAMP_PATTERN, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return externalLink;
|
||||||
|
};
|
||||||
111
app/javascript/shared/helpers/specs/documentHelper.spec.js
Normal file
111
app/javascript/shared/helpers/specs/documentHelper.spec.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
isPdfDocument,
|
||||||
|
formatDocumentLink,
|
||||||
|
} from 'shared/helpers/documentHelper';
|
||||||
|
|
||||||
|
describe('documentHelper', () => {
|
||||||
|
describe('#isPdfDocument', () => {
|
||||||
|
it('returns true for PDF documents', () => {
|
||||||
|
expect(isPdfDocument('PDF:document.pdf')).toBe(true);
|
||||||
|
expect(isPdfDocument('PDF:my-file_20241227123045.pdf')).toBe(true);
|
||||||
|
expect(isPdfDocument('PDF:report with spaces_20241227123045.pdf')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for regular URLs', () => {
|
||||||
|
expect(isPdfDocument('https://example.com')).toBe(false);
|
||||||
|
expect(isPdfDocument('http://docs.example.com/file.pdf')).toBe(false);
|
||||||
|
expect(isPdfDocument('ftp://files.example.com/document.pdf')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for empty or null values', () => {
|
||||||
|
expect(isPdfDocument('')).toBe(false);
|
||||||
|
expect(isPdfDocument(null)).toBe(false);
|
||||||
|
expect(isPdfDocument(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for strings that contain PDF but do not start with PDF:', () => {
|
||||||
|
expect(isPdfDocument('document PDF:file.pdf')).toBe(false);
|
||||||
|
expect(isPdfDocument('My PDF:file.pdf')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#formatDocumentLink', () => {
|
||||||
|
describe('PDF documents', () => {
|
||||||
|
it('removes PDF: prefix from PDF documents', () => {
|
||||||
|
expect(formatDocumentLink('PDF:document.pdf')).toBe('document.pdf');
|
||||||
|
expect(formatDocumentLink('PDF:my-file.pdf')).toBe('my-file.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes timestamp suffix from PDF documents', () => {
|
||||||
|
expect(formatDocumentLink('PDF:document_20241227123045.pdf')).toBe(
|
||||||
|
'document.pdf'
|
||||||
|
);
|
||||||
|
expect(formatDocumentLink('PDF:report_20231215094530.pdf')).toBe(
|
||||||
|
'report.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles PDF documents with spaces in filename', () => {
|
||||||
|
expect(formatDocumentLink('PDF:my document_20241227123045.pdf')).toBe(
|
||||||
|
'my document.pdf'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
formatDocumentLink('PDF:Annual Report 2024_20241227123045.pdf')
|
||||||
|
).toBe('Annual Report 2024.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles PDF documents without timestamp suffix', () => {
|
||||||
|
expect(formatDocumentLink('PDF:document.pdf')).toBe('document.pdf');
|
||||||
|
expect(formatDocumentLink('PDF:simple-file.pdf')).toBe(
|
||||||
|
'simple-file.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles PDF documents with partial timestamp patterns', () => {
|
||||||
|
expect(formatDocumentLink('PDF:document_202412.pdf')).toBe(
|
||||||
|
'document_202412.pdf'
|
||||||
|
);
|
||||||
|
expect(formatDocumentLink('PDF:file_123.pdf')).toBe('file_123.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles edge cases with timestamp pattern', () => {
|
||||||
|
expect(
|
||||||
|
formatDocumentLink('PDF:doc_20241227123045_final_20241227123045.pdf')
|
||||||
|
).toBe('doc_20241227123045_final.pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Regular URLs', () => {
|
||||||
|
it('returns regular URLs unchanged', () => {
|
||||||
|
expect(formatDocumentLink('https://example.com')).toBe(
|
||||||
|
'https://example.com'
|
||||||
|
);
|
||||||
|
expect(formatDocumentLink('http://docs.example.com/api')).toBe(
|
||||||
|
'http://docs.example.com/api'
|
||||||
|
);
|
||||||
|
expect(formatDocumentLink('https://github.com/user/repo')).toBe(
|
||||||
|
'https://github.com/user/repo'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles URLs with query parameters', () => {
|
||||||
|
expect(formatDocumentLink('https://example.com?param=value')).toBe(
|
||||||
|
'https://example.com?param=value'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
formatDocumentLink(
|
||||||
|
'https://api.example.com/docs?version=v1&format=json'
|
||||||
|
)
|
||||||
|
).toBe('https://api.example.com/docs?version=v1&format=json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles URLs with fragments', () => {
|
||||||
|
expect(formatDocumentLink('https://example.com/docs#section1')).toBe(
|
||||||
|
'https://example.com/docs#section1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user