From ce161f09ccd32abba63813138c7708ed2e49ae10 Mon Sep 17 00:00:00 2001 From: Jimmy Ben Klieve Date: Thu, 18 Dec 2025 09:33:52 +0800 Subject: [PATCH] feat: add image uploader in edit chunk dialog (#12003) ### What problem does this PR solve? Add image uploader in edit chunk dialog for replacing image chunk ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/file-uploader.tsx | 15 ++-- web/src/components/image/index.tsx | 2 +- web/src/locales/en.ts | 6 +- .../components/chunk-creating-modal/index.tsx | 68 ++++++++++++++++++- web/src/utils/common-util.ts | 10 ++- 5 files changed, 87 insertions(+), 14 deletions(-) diff --git a/web/src/components/file-uploader.tsx b/web/src/components/file-uploader.tsx index a8860cfe0..b5d622aad 100644 --- a/web/src/components/file-uploader.tsx +++ b/web/src/components/file-uploader.tsx @@ -39,7 +39,7 @@ function FilePreview({ file }: FilePreviewProps) { width={48} height={48} loading="lazy" - className="aspect-square shrink-0 rounded-md object-cover" + className="size-full aspect-square shrink-0 rounded-md object-cover" /> ); } @@ -84,7 +84,8 @@ function FileCard({ file, progress, onRemove }: FileCardProps) { ); } -interface FileUploaderProps extends React.HTMLAttributes { +interface FileUploaderProps + extends Omit, 'title'> { /** * Value of the uploader. * @type File[] @@ -160,7 +161,8 @@ interface FileUploaderProps extends React.HTMLAttributes { */ disabled?: boolean; - description?: string; + title?: React.ReactNode; + description?: React.ReactNode; } export function FileUploader(props: FileUploaderProps) { @@ -177,6 +179,7 @@ export function FileUploader(props: FileUploaderProps) { multiple = false, disabled = false, className, + title, description, ...dropzoneProps } = props; @@ -285,7 +288,7 @@ export function FileUploader(props: FileUploaderProps) {
@@ -297,13 +300,13 @@ export function FileUploader(props: FileUploaderProps) {

- {t('knowledgeDetails.uploadTitle')} + {title || t('knowledgeDetails.uploadTitle')}

{description || t('knowledgeDetails.uploadDescription')} diff --git a/web/src/components/image/index.tsx b/web/src/components/image/index.tsx index 020a910ad..c4853bdf4 100644 --- a/web/src/components/image/index.tsx +++ b/web/src/components/image/index.tsx @@ -4,7 +4,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; interface IImage { id: string; - className: string; + className?: string; onClick?(): void; } diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index bd126f115..8333f8e14 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -108,7 +108,7 @@ export default { 'Converts text into numerical vectors for meaning similarity search and memory retrieval.', embeddingModelError: 'Memory type is required and "raw" cannot be deleted.', - memoryTypeTooltip: `Raw: The raw dialogue content between the user and the agent (Required by default). + memoryTypeTooltip: `Raw: The raw dialogue content between the user and the agent (Required by default). Semantic Memory: General knowledge and facts about the user and world. Episodic Memory: Time-stamped records of specific events and experiences. Procedural Memory: Learned skills, habits, and automated procedures.`, @@ -598,6 +598,8 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s enabled: 'Enabled', disabled: 'Disabled', keyword: 'Keyword', + image: 'Image', + imageUploaderTitle: 'Upload a new image to update this image chunk', function: 'Function', chunkMessage: 'Please input value!', full: 'Full text', @@ -648,7 +650,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s 'Select the datasets to associate with this chat assistant. An empty knowledge base will not appear in the dropdown list.', system: 'System prompt', systemInitialValue: `You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base. - + **Essential Rules:** - Your answer must be derived **solely** from this knowledge base: \`{knowledge}\`. - **When information is available**: Summarize the content to give a detailed answer. diff --git a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx index cb2c3a933..a1b2d046e 100644 --- a/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx +++ b/web/src/pages/chunk/parsed-result/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx @@ -1,4 +1,6 @@ import EditTag from '@/components/edit-tag'; +import { FileUploader } from '@/components/file-uploader'; +import Image from '@/components/image'; import Divider from '@/components/ui/divider'; import { Form, @@ -35,6 +37,21 @@ interface kFProps { parserId: string; } +async function fileToBase64(file: File) { + if (!file) { + return; + } + + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.addEventListener('load', () => { + resolve((fr.result?.toString() ?? '').replace(/^.*,/, '')); + }); + fr.onerror = reject; + fr.readAsDataURL(file); + }); +} + const ChunkCreatingModal: React.FC & kFProps> = ({ doc_id, chunkId, @@ -52,6 +69,7 @@ const ChunkCreatingModal: React.FC & kFProps> = ({ question_kwd: [], important_kwd: [], tag_feas: [], + image: [], }, }); const [checked, setChecked] = useState(false); @@ -61,12 +79,17 @@ const ChunkCreatingModal: React.FC & kFProps> = ({ const isTagParser = parserId === 'tag'; const onSubmit = useCallback( - (values: FieldValues) => { - onOk?.({ + async (values: FieldValues) => { + const prunedValues = { ...values, + image_base64: await fileToBase64(values.image?.[0] as File), tag_feas: transformTagFeaturesArrayToObject(values.tag_feas), available_int: checked ? 1 : 0, - }); + } as FieldValues; + + Reflect.deleteProperty(prunedValues, 'image'); + + onOk?.(prunedValues); }, [checked, onOk], ); @@ -86,6 +109,7 @@ const ChunkCreatingModal: React.FC & kFProps> = ({ useEffect(() => { if (data?.code === 0) { const { available_int, tag_feas } = data.data; + form.reset({ ...data.data, tag_feas: transformTagFeaturesObjectToArray(tag_feas), @@ -119,6 +143,44 @@ const ChunkCreatingModal: React.FC & kFProps> = ({ )} /> + + ( + + {t('chunk.image')} + +

+ {data?.data?.img_id && ( + + )} + +
+ + } + /> + +
+
+ + )} + /> + { return data instanceof FormData; }; -const excludedFields = ['img2txt_id', 'mcpServers']; +const excludedFields: Array = [ + 'img2txt_id', + 'mcpServers', + 'image_base64', +]; const isExcludedField = (key: string) => { - return excludedFields.includes(key); + return excludedFields.some((excl) => + excl instanceof RegExp ? excl.test(key) : excl === key, + ); }; export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {