feat: add fe changes for captain pdf support for faq generation

This commit is contained in:
Tanmay Deep Sharma
2025-08-06 12:07:52 +05:30
parent 51b9fd8eca
commit 8627400e6a
11 changed files with 615 additions and 31 deletions
@@ -0,0 +1,29 @@
/* 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;
@@ -0,0 +1,284 @@
<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>
@@ -0,0 +1,40 @@
<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>
@@ -63,6 +63,18 @@ const menuItems = computed(() => {
const createdAt = computed(() => dynamicTime(props.createdAt));
const isPdfDocument = computed(() => props.externalLink?.startsWith('PDF:'));
const displayLink = computed(() => {
if (isPdfDocument.value) {
// Remove 'PDF: ' prefix and timestamp suffix for display
const fullName = props.externalLink.substring(5);
// Remove timestamp suffix (_YYYYMMDDHHMMSS) if present
return fullName.replace(/_\d{14}$/, '');
}
return props.externalLink;
});
const linkIcon = computed(() => isPdfDocument.value ? 'i-ph-file-pdf' : 'i-ph-link-simple');
const handleAction = ({ action, value }) => {
toggleDropdown(false);
emit('action', { action, value, id: props.id });
@@ -106,8 +118,8 @@ const handleAction = ({ action, value }) => {
<span
class="text-n-slate-11 text-sm truncate flex justify-start flex-1 items-center gap-1"
>
<i class="i-ph-link-simple shrink-0" />
<span class="truncate">{{ externalLink }}</span>
<i :class="linkIcon" class="shrink-0" />
<span class="truncate">{{ displayLink }}</span>
</span>
<div class="shrink-0 text-sm text-n-slate-11 line-clamp-1">
{{ createdAt }}
@@ -23,7 +23,9 @@ const handleSubmit = async newDocument => {
dialogRef.value.close();
} catch (error) {
const errorMessage =
error?.response?.message || t(`${i18nKey}.ERROR_MESSAGE`);
error?.response?.data?.message ||
error?.response?.message ||
t(`${i18nKey}.ERROR_MESSAGE`);
useAlert(errorMessage);
}
};
@@ -1,9 +1,10 @@
<script setup>
import { reactive, computed } from 'vue';
import { reactive, computed, ref, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { required, minLength, url } from '@vuelidate/validators';
import { required, minLength, url, requiredIf } 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';
@@ -20,14 +21,25 @@ const formState = {
const initialState = {
name: '',
url: '',
assistantId: null,
documentType: 'url',
pdfFile: null,
};
const state = reactive({ ...initialState });
const fileInputRef = ref(null);
const validationRules = {
url: { required, 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')
},
};
const assistantList = computed(() =>
@@ -50,14 +62,53 @@ const getErrorMessage = (field, errorKey) => {
const formErrors = computed(() => ({
url: getErrorMessage('url', 'URL'),
assistantId: getErrorMessage('assistantId', 'ASSISTANT'),
pdfFile: getErrorMessage('pdfFile', 'PDF_FILE'),
}));
const handleCancel = () => emit('cancel');
const prepareDocumentDetails = () => ({
external_link: state.url,
assistant_id: state.assistantId,
});
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 > 512 * 1024 * 1024) { // 512MB
useAlert(t('CAPTAIN.DOCUMENTS.FORM.PDF_FILE.TOO_LARGE'));
event.target.value = '';
return;
}
state.pdfFile = file;
state.name = file.name.replace('.pdf', '');
}
};
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);
// 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();
@@ -71,13 +122,88 @@ const handleSubmit = async () => {
<template>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<div class="flex flex-col gap-2">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.DOCUMENTS.FORM.TYPE.LABEL') }}
</label>
<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'"
>
<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'"
>
<i class="i-ph-file-pdf text-base" />
<span>{{ t('CAPTAIN.DOCUMENTS.FORM.TYPE.PDF') }}</span>
</button>
</div>
</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"
@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'"
>
<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') }}
</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') }}
</p>
</div>
<i class="i-lucide-upload text-n-slate-11" />
</button>
</div>
<p v-if="formErrors.pdfFile" class="text-sm text-n-red-11">
{{ 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">
<label for="assistant" class="mb-0.5 text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.DOCUMENTS.FORM.ASSISTANT.LABEL') }}
@@ -807,6 +807,58 @@
"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..."
}
}
}
@@ -700,11 +700,28 @@
"ERROR_MESSAGE": "There was an error creating the document, please try again."
},
"FORM": {
"TYPE": {
"LABEL": "Document Type",
"URL": "URL",
"PDF": "PDF File"
},
"URL": {
"LABEL": "URL",
"PLACEHOLDER": "Enter the URL of 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: 512MB",
"INVALID_TYPE": "Please select a valid PDF file",
"TOO_LARGE": "File size exceeds 512MB limit"
},
"NAME": {
"LABEL": "Document Name (Optional)",
"PLACEHOLDER": "Enter a name for the document"
},
"ASSISTANT": {
"LABEL": "Assistant",
"PLACEHOLDER": "Select the assistant",
@@ -22,6 +22,9 @@ 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'],
@@ -82,6 +85,12 @@ const portalRoutes = [
meta,
component: PortalsSettingsIndexPage,
},
{
path: getPortalRoute(':portalSlug/pdf-documents'),
name: 'portals_pdf_documents_index',
meta,
component: PdfDocumentsPage,
},
{
path: getPortalRoute('new'),
name: 'portals_new',
+9 -8
View File
@@ -38,6 +38,7 @@
"@formkit/core": "^1.6.7",
"@formkit/vue": "^1.6.7",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@heroicons/vue": "^2.2.0",
"@highlightjs/vue-plugin": "^2.1.0",
"@iconify-json/material-symbols": "^1.2.10",
"@june-so/analytics-next": "^2.0.0",
@@ -56,16 +57,16 @@
"@vuelidate/validators": "^2.0.4",
"@vueuse/components": "^12.0.0",
"@vueuse/core": "^12.0.0",
"activestorage": "^5.2.6",
"activestorage": "^5.2.8",
"axios": "^1.8.2",
"camelcase-keys": "^9.1.3",
"chart.js": "~4.4.4",
"color2k": "^2.0.2",
"color2k": "^2.0.3",
"company-email-validator": "^1.1.0",
"core-js": "3.38.1",
"countries-and-timezones": "^3.6.0",
"date-fns": "2.21.1",
"date-fns-tz": "^1.3.3",
"date-fns-tz": "^1.3.8",
"dompurify": "3.2.4",
"flag-icons": "^7.2.3",
"floating-vue": "^5.2.2",
@@ -98,7 +99,7 @@
"vue-multiselect": "3.1.0",
"vue-router": "~4.4.5",
"vue-upload-component": "^3.1.17",
"vue-virtual-scroller": "^2.0.0-beta.8",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue3-click-away": "^1.2.4",
"vuedraggable": "^4.1.0",
"vuex": "~4.1.0",
@@ -114,7 +115,7 @@
"@iconify-json/ri": "^1.2.3",
"@iconify-json/teenyicons": "^1.2.1",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@size-limit/file": "^8.2.4",
"@size-limit/file": "^8.2.6",
"@vitest/coverage-v8": "3.0.5",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
@@ -129,16 +130,16 @@
"eslint-plugin-vue": "^9.28.0",
"fake-indexeddb": "^6.0.0",
"histoire": "0.17.15",
"husky": "^7.0.0",
"husky": "^7.0.4",
"jsdom": "^24.1.3",
"lint-staged": "14.0.1",
"postcss": "^8.4.47",
"postcss-preset-env": "^8.5.1",
"prettier": "^3.3.3",
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"size-limit": "^8.2.6",
"tailwindcss": "^3.4.13",
"vite": "^5.4.19",
"vite": "5.4.19",
"vite-plugin-ruby": "^5.0.0",
"vitest": "3.0.5"
},
+25 -13
View File
@@ -34,6 +34,9 @@ importers:
'@hcaptcha/vue3-hcaptcha':
specifier: ^1.3.0
version: 1.3.0(vue@3.5.12(typescript@5.6.2))
'@heroicons/vue':
specifier: ^2.2.0
version: 2.2.0(vue@3.5.12(typescript@5.6.2))
'@highlightjs/vue-plugin':
specifier: ^2.1.0
version: 2.1.0(highlight.js@11.10.0)(vue@3.5.12(typescript@5.6.2))
@@ -89,7 +92,7 @@ importers:
specifier: ^12.0.0
version: 12.0.0(typescript@5.6.2)
activestorage:
specifier: ^5.2.6
specifier: ^5.2.8
version: 5.2.8
axios:
specifier: ^1.8.2
@@ -101,7 +104,7 @@ importers:
specifier: ~4.4.4
version: 4.4.4
color2k:
specifier: ^2.0.2
specifier: ^2.0.3
version: 2.0.3
company-email-validator:
specifier: ^1.1.0
@@ -116,7 +119,7 @@ importers:
specifier: 2.21.1
version: 2.21.1
date-fns-tz:
specifier: ^1.3.3
specifier: ^1.3.8
version: 1.3.8(date-fns@2.21.1)
dompurify:
specifier: 3.2.4
@@ -215,7 +218,7 @@ importers:
specifier: ^3.1.17
version: 3.1.17
vue-virtual-scroller:
specifier: ^2.0.0-beta.8
specifier: 2.0.0-beta.8
version: 2.0.0-beta.8(vue@3.5.12(typescript@5.6.2))
vue3-click-away:
specifier: ^1.2.4
@@ -258,7 +261,7 @@ importers:
specifier: ^3.2.0
version: 3.2.0(eslint@8.57.0)
'@size-limit/file':
specifier: ^8.2.4
specifier: ^8.2.6
version: 8.2.6(size-limit@8.2.6)
'@vitest/coverage-v8':
specifier: 3.0.5
@@ -303,7 +306,7 @@ importers:
specifier: 0.17.15
version: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
husky:
specifier: ^7.0.0
specifier: ^7.0.4
version: 7.0.4
jsdom:
specifier: ^24.1.3
@@ -324,7 +327,7 @@ importers:
specifier: ^1.22.3
version: 1.22.3
size-limit:
specifier: ^8.2.4
specifier: ^8.2.6
version: 8.2.6
tailwindcss:
specifier: ^3.4.13
@@ -839,6 +842,11 @@ packages:
peerDependencies:
vue: ^3.0.0
'@heroicons/vue@2.2.0':
resolution: {integrity: sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==}
peerDependencies:
vue: '>= 3'
'@highlightjs/vue-plugin@2.1.0':
resolution: {integrity: sha512-E+bmk4ncca+hBEYRV2a+1aIzIV0VSY/e5ArjpuSN9IO7wBJrzUE2u4ESCwrbQD7sAy+jWQjkV5qCCWgc+pu7CQ==}
peerDependencies:
@@ -2216,8 +2224,8 @@ packages:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001651:
resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
caniuse-lite@1.0.30001731:
resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
@@ -5680,6 +5688,10 @@ snapshots:
dependencies:
vue: 3.5.12(typescript@5.6.2)
'@heroicons/vue@2.2.0(vue@3.5.12(typescript@5.6.2))':
dependencies:
vue: 3.5.12(typescript@5.6.2)
'@highlightjs/vue-plugin@2.1.0(highlight.js@11.10.0)(vue@3.5.12(typescript@5.6.2))':
dependencies:
highlight.js: 11.10.0
@@ -7258,7 +7270,7 @@ snapshots:
autoprefixer@10.4.20(postcss@8.4.47):
dependencies:
browserslist: 4.23.3
caniuse-lite: 1.0.30001651
caniuse-lite: 1.0.30001731
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.1
@@ -7313,14 +7325,14 @@ snapshots:
browserslist@4.23.0:
dependencies:
caniuse-lite: 1.0.30001651
caniuse-lite: 1.0.30001731
electron-to-chromium: 1.4.783
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0)
browserslist@4.23.3:
dependencies:
caniuse-lite: 1.0.30001651
caniuse-lite: 1.0.30001731
electron-to-chromium: 1.5.13
node-releases: 2.0.18
update-browserslist-db: 1.1.0(browserslist@4.23.3)
@@ -7368,7 +7380,7 @@ snapshots:
camelcase@8.0.0: {}
caniuse-lite@1.0.30001651: {}
caniuse-lite@1.0.30001731: {}
capital-case@1.0.4:
dependencies: