feat(faq): support rich-text image upload and RAG output
- add FAQ image upload API with MinIO path and mime/size validation - enable FAQ editor image upload and text-or-image validation - include FAQ image metadata in vector content and retrieval output - enforce markdown image preservation via bot system prompt rule
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
import '@wangeditor/editor/dist/css/style.css';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -35,6 +37,13 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'update:modelValue']);
|
||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_TYPES = new Set([
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
]);
|
||||
|
||||
const editorRef = shallowRef<IDomEditor | null>(null);
|
||||
const form = ref<any>({
|
||||
@@ -65,19 +74,48 @@ watch(
|
||||
);
|
||||
|
||||
const toolbarConfig = {
|
||||
excludeKeys: [
|
||||
'uploadImage',
|
||||
'insertImage',
|
||||
'group-image',
|
||||
'insertVideo',
|
||||
'group-video',
|
||||
'uploadVideo',
|
||||
'todo',
|
||||
'emotion',
|
||||
],
|
||||
excludeKeys: ['insertVideo', 'group-video', 'uploadVideo', 'todo', 'emotion'],
|
||||
};
|
||||
const editorConfig = {
|
||||
const editorConfig: any = {
|
||||
placeholder: $t('documentCollection.faq.answerPlaceholder'),
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
customUpload: async (
|
||||
file: File,
|
||||
insertFn: (url: string, alt?: string, href?: string) => void,
|
||||
) => {
|
||||
if (!form.value.collectionId) {
|
||||
ElMessage.error($t('documentCollection.faq.collectionRequired'));
|
||||
return;
|
||||
}
|
||||
if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
|
||||
ElMessage.error($t('documentCollection.faq.imageTypeInvalid'));
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_IMAGE_SIZE) {
|
||||
ElMessage.error($t('documentCollection.faq.imageSizeExceeded'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api.upload(
|
||||
'/api/v1/faqItem/uploadImage',
|
||||
{ collectionId: form.value.collectionId, file },
|
||||
{},
|
||||
);
|
||||
const imageUrl = res?.data?.path;
|
||||
if (!imageUrl) {
|
||||
ElMessage.error($t('documentCollection.faq.imageUploadFailed'));
|
||||
return;
|
||||
}
|
||||
insertFn(imageUrl, file.name, imageUrl);
|
||||
} catch (error) {
|
||||
console.error('FAQ image upload failed:', error);
|
||||
ElMessage.error($t('documentCollection.faq.imageUploadFailed'));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handleEditorCreated = (editor: IDomEditor) => {
|
||||
@@ -121,9 +159,15 @@ const handleSubmit = () => {
|
||||
ElMessage.error($t('documentCollection.faq.questionRequired'));
|
||||
return;
|
||||
}
|
||||
const sanitizedHtml = DOMPurify.sanitize(form.value.answerHtml || '');
|
||||
const pureText = sanitizedHtml.replaceAll(/<[^>]*>/g, '').trim();
|
||||
if (!pureText) {
|
||||
const sanitizedHtml = DOMPurify.sanitize(form.value.answerHtml || '', {
|
||||
ADD_TAGS: ['img'],
|
||||
ADD_ATTR: ['src', 'alt', 'title', 'width', 'height'],
|
||||
ALLOWED_URI_REGEXP: /^https?:\/\//i,
|
||||
});
|
||||
const doc = new DOMParser().parseFromString(sanitizedHtml, 'text/html');
|
||||
const pureText = (doc.body?.textContent || '').trim();
|
||||
const hasImage = doc.querySelector('img[src]') !== null;
|
||||
if (!pureText && !hasImage) {
|
||||
ElMessage.error($t('documentCollection.faq.answerRequired'));
|
||||
return;
|
||||
}
|
||||
@@ -238,7 +282,9 @@ onBeforeUnmount(() => {
|
||||
background: var(--el-fill-color-blank);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgb(15 23 42 / 4%);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-wrapper:focus-within {
|
||||
|
||||
Reference in New Issue
Block a user