self review changes for pdf support for captain
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class PdfDocumentsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('portals', { accountScoped: true });
|
||||
}
|
||||
|
||||
uploadContent(portalSlug, formData) {
|
||||
return axios.post(`${this.url}/${portalSlug}/upload_content`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getGeneratedContent(portalSlug) {
|
||||
return axios.get(`${this.url}/${portalSlug}/generated_content`);
|
||||
}
|
||||
|
||||
publishContent(portalSlug, responseIds, categoryId = null) {
|
||||
return axios.post(`${this.url}/${portalSlug}/publish_content`, {
|
||||
response_ids: responseIds,
|
||||
category_id: categoryId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default PdfDocumentsAPI;
|
||||
-284
@@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-6">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.TITLE') }}
|
||||
</h3>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Upload Section -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-base font-medium text-slate-900 dark:text-slate-100 mb-4">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.UPLOAD_TITLE') }}
|
||||
</h4>
|
||||
|
||||
<div
|
||||
class="border-2 border-dashed border-slate-300 dark:border-slate-600 rounded-lg p-6 text-center"
|
||||
:class="{
|
||||
'border-woot-500 bg-woot-25': isDragOver,
|
||||
'border-green-500 bg-green-25': uploadSuccess
|
||||
}"
|
||||
@drop="handleDrop"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
>
|
||||
<div v-if="!isUploading && !uploadSuccess">
|
||||
<DocumentPlusIcon class="mx-auto h-12 w-12 text-slate-400 mb-4" />
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300 mb-2">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.DRAG_DROP') }}
|
||||
</p>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-woot-700 bg-woot-100 hover:bg-woot-200"
|
||||
@click="$refs.fileInput.click()"
|
||||
>
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.SELECT_FILE') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isUploading" class="py-4">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-woot-500 mx-auto mb-2"></div>
|
||||
<p class="text-sm text-slate-600">{{ $t('HELP_CENTER.CONTENT_GENERATION.UPLOADING') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="uploadSuccess" class="py-4">
|
||||
<CheckCircleIcon class="mx-auto h-8 w-8 text-green-500 mb-2" />
|
||||
<p class="text-sm text-green-600">{{ $t('HELP_CENTER.CONTENT_GENERATION.UPLOAD_SUCCESS') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadError" class="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p class="text-sm text-red-800">{{ uploadError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generated Content Section -->
|
||||
<div v-if="generatedResponses.length > 0" class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-base font-medium text-slate-900 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.GENERATED_CONTENT') }}
|
||||
</h4>
|
||||
<button
|
||||
v-if="selectedResponses.length > 0"
|
||||
class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded text-white bg-woot-500 hover:bg-woot-600"
|
||||
@click="publishSelectedContent"
|
||||
:disabled="isPublishing"
|
||||
>
|
||||
<template v-if="isPublishing">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b border-white mr-2"></div>
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.PUBLISHING') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.PUBLISH_SELECTED') }} ({{ selectedResponses.length }})
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 max-h-96 overflow-y-auto">
|
||||
<div
|
||||
v-for="response in generatedResponses"
|
||||
:key="response.id"
|
||||
class="border border-slate-200 dark:border-slate-600 rounded-lg p-4 hover:bg-slate-50 dark:hover:bg-slate-700"
|
||||
>
|
||||
<div class="flex items-start space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="response.id"
|
||||
v-model="selectedResponses"
|
||||
class="mt-1 rounded border-slate-300 text-woot-500 focus:ring-woot-500"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h5 class="text-sm font-medium text-slate-900 dark:text-slate-100 mb-1">
|
||||
{{ response.question }}
|
||||
</h5>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-400 line-clamp-2">
|
||||
{{ response.answer }}
|
||||
</p>
|
||||
<div class="mt-2 text-xs text-slate-500">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.FROM_DOCUMENT') }}: {{ response.documentable?.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="!isLoading" class="text-center py-8">
|
||||
<DocumentIcon class="mx-auto h-12 w-12 text-slate-400 mb-4" />
|
||||
<p class="text-slate-600 dark:text-slate-400">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.NO_CONTENT') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-woot-500 mx-auto mb-2"></div>
|
||||
<p class="text-slate-600">{{ $t('HELP_CENTER.CONTENT_GENERATION.LOADING') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import {
|
||||
DocumentPlusIcon,
|
||||
DocumentIcon,
|
||||
CheckCircleIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
|
||||
import PdfDocumentsAPI from '../../../../api/helpCenter/pdfDocuments';
|
||||
|
||||
const props = defineProps({
|
||||
portalSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['content-published']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const pdfDocumentsApi = new PdfDocumentsAPI();
|
||||
|
||||
// Reactive data
|
||||
const generatedResponses = ref([]);
|
||||
const selectedResponses = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isUploading = ref(false);
|
||||
const isPublishing = ref(false);
|
||||
const uploadSuccess = ref(false);
|
||||
const uploadError = ref('');
|
||||
const isDragOver = ref(false);
|
||||
|
||||
// Methods
|
||||
const loadGeneratedContent = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await pdfDocumentsApi.getGeneratedContent(props.portalSlug);
|
||||
generatedResponses.value = response.data.responses || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load generated content:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
uploadFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (event) => {
|
||||
event.preventDefault();
|
||||
isDragOver.value = false;
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
uploadFile(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
event.preventDefault();
|
||||
isDragOver.value = false;
|
||||
};
|
||||
|
||||
const uploadFile = async (file) => {
|
||||
if (!validateFile(file)) return;
|
||||
|
||||
try {
|
||||
isUploading.value = true;
|
||||
uploadError.value = '';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('pdf_file', file);
|
||||
|
||||
await pdfDocumentsApi.uploadContent(props.portalSlug, formData);
|
||||
|
||||
uploadSuccess.value = true;
|
||||
|
||||
// Wait a bit then reload content
|
||||
setTimeout(async () => {
|
||||
uploadSuccess.value = false;
|
||||
await loadGeneratedContent();
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
uploadError.value = error.response?.data?.error || t('HELP_CENTER.CONTENT_GENERATION.UPLOAD_ERROR');
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const validateFile = (file) => {
|
||||
if (file.type !== 'application/pdf') {
|
||||
uploadError.value = t('HELP_CENTER.CONTENT_GENERATION.INVALID_FILE_TYPE');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > 512 * 1024 * 1024) {
|
||||
uploadError.value = t('HELP_CENTER.CONTENT_GENERATION.FILE_TOO_LARGE');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const publishSelectedContent = async () => {
|
||||
if (selectedResponses.value.length === 0) return;
|
||||
|
||||
try {
|
||||
isPublishing.value = true;
|
||||
|
||||
await pdfDocumentsApi.publishContent(props.portalSlug, selectedResponses.value);
|
||||
|
||||
// Remove published responses from the list
|
||||
generatedResponses.value = generatedResponses.value.filter(
|
||||
response => !selectedResponses.value.includes(response.id)
|
||||
);
|
||||
|
||||
selectedResponses.value = [];
|
||||
emit('content-published');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to publish content:', error);
|
||||
} finally {
|
||||
isPublishing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadGeneratedContent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-200 dark:border-slate-700 px-6 py-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.TITLE') }}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
|
||||
{{ $t('HELP_CENTER.CONTENT_GENERATION.DESCRIPTION') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-auto px-6 py-6">
|
||||
<ContentGeneration
|
||||
:portal-slug="portalSlug"
|
||||
@content-published="handleContentPublished"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ContentGeneration from './ContentGeneration.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// Computed properties
|
||||
const portalSlug = computed(() => route.params.portalId);
|
||||
|
||||
// Methods
|
||||
const handleContentPublished = () => {
|
||||
// Could show a success notification here
|
||||
console.log('Content has been published successfully');
|
||||
};
|
||||
</script>
|
||||
@@ -73,7 +73,9 @@ const displayLink = computed(() => {
|
||||
}
|
||||
return props.externalLink;
|
||||
});
|
||||
const linkIcon = computed(() => isPdfDocument.value ? 'i-ph-file-pdf' : 'i-ph-link-simple');
|
||||
const linkIcon = computed(() =>
|
||||
isPdfDocument.value ? 'i-ph-file-pdf' : 'i-ph-link-simple'
|
||||
);
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@ const handleSubmit = async newDocument => {
|
||||
dialogRef.value.close();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.message ||
|
||||
error?.response?.message ||
|
||||
error?.response?.data?.message ||
|
||||
error?.response?.message ||
|
||||
t(`${i18nKey}.ERROR_MESSAGE`);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
|
||||
+42
-24
@@ -2,7 +2,7 @@
|
||||
import { reactive, computed, ref, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, url, requiredIf } from '@vuelidate/validators';
|
||||
import { required, minLength, requiredIf } from '@vuelidate/validators';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
@@ -31,14 +31,14 @@ const state = reactive({ ...initialState });
|
||||
const fileInputRef = ref(null);
|
||||
|
||||
const validationRules = {
|
||||
url: {
|
||||
required: requiredIf(() => state.documentType === 'url'),
|
||||
url: requiredIf(() => state.documentType === 'url'),
|
||||
minLength: minLength(1)
|
||||
url: {
|
||||
required: requiredIf(() => state.documentType === 'url'),
|
||||
url: requiredIf(() => state.documentType === 'url'),
|
||||
minLength: minLength(1),
|
||||
},
|
||||
assistantId: { required },
|
||||
pdfFile: {
|
||||
required: requiredIf(() => state.documentType === 'pdf')
|
||||
pdfFile: {
|
||||
required: requiredIf(() => state.documentType === 'pdf'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ const formErrors = computed(() => ({
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const handleFileChange = event => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
if (file.type !== 'application/pdf') {
|
||||
@@ -75,7 +75,8 @@ const handleFileChange = (event) => {
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
if (file.size > 512 * 1024 * 1024) { // 512MB
|
||||
if (file.size > 20 * 1024 * 1024) {
|
||||
// 20MB
|
||||
useAlert(t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.TOO_LARGE'));
|
||||
event.target.value = '';
|
||||
return;
|
||||
@@ -97,16 +98,19 @@ const openFileDialog = () => {
|
||||
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);
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -129,22 +133,26 @@ const handleSubmit = async () => {
|
||||
<div class="grid grid-cols-2 gap-3 p-1 bg-n-slate-3 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
@click="state.documentType = 'url'"
|
||||
class="relative flex items-center justify-center gap-2 px-4 py-2.5 rounded-md font-medium text-sm transition-all duration-200"
|
||||
:class="state.documentType === 'url'
|
||||
? 'bg-n-white text-n-slate-12 shadow-sm'
|
||||
: 'text-n-slate-11 hover:text-n-slate-12'"
|
||||
:class="
|
||||
state.documentType === 'url'
|
||||
? 'bg-n-white text-n-slate-12 shadow-sm'
|
||||
: 'text-n-slate-11 hover:text-n-slate-12'
|
||||
"
|
||||
@click="state.documentType = 'url'"
|
||||
>
|
||||
<i class="i-ph-link-simple text-base" />
|
||||
<span>{{ t('CAPTAIN.DOCUMENTS.FORM.TYPE.URL') }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="state.documentType = 'pdf'"
|
||||
class="relative flex items-center justify-center gap-2 px-4 py-2.5 rounded-md font-medium text-sm transition-all duration-200"
|
||||
:class="state.documentType === 'pdf'
|
||||
? 'bg-n-white text-n-slate-12 shadow-sm'
|
||||
: 'text-n-slate-11 hover:text-n-slate-12'"
|
||||
:class="
|
||||
state.documentType === 'pdf'
|
||||
? 'bg-n-white text-n-slate-12 shadow-sm'
|
||||
: 'text-n-slate-11 hover:text-n-slate-12'
|
||||
"
|
||||
@click="state.documentType = 'pdf'"
|
||||
>
|
||||
<i class="i-ph-file-pdf text-base" />
|
||||
<span>{{ t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') }}</span>
|
||||
@@ -175,19 +183,29 @@ const handleSubmit = async () => {
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="openFileDialog"
|
||||
class="flex items-center gap-3 px-4 py-3 border border-n-slate-6 rounded-lg cursor-pointer hover:border-n-slate-8 transition-colors w-full text-left"
|
||||
:class="state.pdfFile ? 'bg-n-slate-2' : 'bg-n-white'"
|
||||
@click="openFileDialog"
|
||||
>
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-n-slate-3 rounded-lg">
|
||||
<div
|
||||
class="flex items-center justify-center w-10 h-10 bg-n-slate-3 rounded-lg"
|
||||
>
|
||||
<i class="i-ph-file-pdf text-xl text-n-slate-11" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-n-slate-12">
|
||||
{{ state.pdfFile ? state.pdfFile.name : t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.CHOOSE_FILE') }}
|
||||
{{
|
||||
state.pdfFile
|
||||
? state.pdfFile.name
|
||||
: t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.CHOOSE_FILE')
|
||||
}}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-11">
|
||||
{{ state.pdfFile ? `${(state.pdfFile.size / 1024 / 1024).toFixed(2)} MB` : t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.HELP_TEXT') }}
|
||||
{{
|
||||
state.pdfFile
|
||||
? `${(state.pdfFile.size / 1024 / 1024).toFixed(2)} MB`
|
||||
: t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.HELP_TEXT')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<i class="i-lucide-upload text-n-slate-11" />
|
||||
|
||||
@@ -22,9 +22,6 @@ const PortalsLocalesIndexPage = () =>
|
||||
const PortalsSettingsIndexPage = () =>
|
||||
import('./pages/PortalsSettingsIndexPage.vue');
|
||||
|
||||
const PdfDocumentsPage = () =>
|
||||
import('../../../components-next/HelpCenter/Pages/PdfDocumentsPage/PdfDocumentsPage.vue');
|
||||
|
||||
const meta = {
|
||||
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
@@ -85,12 +82,6 @@ const portalRoutes = [
|
||||
meta,
|
||||
component: PortalsSettingsIndexPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/pdf-documents'),
|
||||
name: 'portals_pdf_documents_index',
|
||||
meta,
|
||||
component: PdfDocumentsPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('new'),
|
||||
name: 'portals_new',
|
||||
|
||||
Reference in New Issue
Block a user