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)
This commit is contained in:
Jimmy Ben Klieve
2025-12-18 09:33:52 +08:00
committed by GitHub
parent 672958a192
commit ce161f09cc
5 changed files with 87 additions and 14 deletions

View File

@ -39,7 +39,7 @@ function FilePreview({ file }: FilePreviewProps) {
width={48} width={48}
height={48} height={48}
loading="lazy" 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<HTMLDivElement> { interface FileUploaderProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
/** /**
* Value of the uploader. * Value of the uploader.
* @type File[] * @type File[]
@ -160,7 +161,8 @@ interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
*/ */
disabled?: boolean; disabled?: boolean;
description?: string; title?: React.ReactNode;
description?: React.ReactNode;
} }
export function FileUploader(props: FileUploaderProps) { export function FileUploader(props: FileUploaderProps) {
@ -177,6 +179,7 @@ export function FileUploader(props: FileUploaderProps) {
multiple = false, multiple = false,
disabled = false, disabled = false,
className, className,
title,
description, description,
...dropzoneProps ...dropzoneProps
} = props; } = props;
@ -285,7 +288,7 @@ export function FileUploader(props: FileUploaderProps) {
<div className="flex flex-col items-center justify-center gap-4 sm:px-5"> <div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3"> <div className="rounded-full border border-dashed p-3">
<Upload <Upload
className="size-7 text-text-secondary hover:text-text-primary" className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
@ -297,13 +300,13 @@ export function FileUploader(props: FileUploaderProps) {
<div className="flex flex-col items-center justify-center gap-4 sm:px-5"> <div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3"> <div className="rounded-full border border-dashed p-3">
<Upload <Upload
className="size-7 text-text-secondary hover:text-text-primary" className="size-7 text-text-secondary transition-colors group-hover:text-text-primary"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
<div className="flex flex-col gap-px"> <div className="flex flex-col gap-px">
<p className="font-medium text-text-secondary"> <p className="font-medium text-text-secondary">
{t('knowledgeDetails.uploadTitle')} {title || t('knowledgeDetails.uploadTitle')}
</p> </p>
<p className="text-sm text-text-disabled"> <p className="text-sm text-text-disabled">
{description || t('knowledgeDetails.uploadDescription')} {description || t('knowledgeDetails.uploadDescription')}

View File

@ -4,7 +4,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
interface IImage { interface IImage {
id: string; id: string;
className: string; className?: string;
onClick?(): void; onClick?(): void;
} }

View File

@ -108,7 +108,7 @@ export default {
'Converts text into numerical vectors for meaning similarity search and memory retrieval.', 'Converts text into numerical vectors for meaning similarity search and memory retrieval.',
embeddingModelError: embeddingModelError:
'Memory type is required and "raw" cannot be deleted.', '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. Semantic Memory: General knowledge and facts about the user and world.
Episodic Memory: Time-stamped records of specific events and experiences. Episodic Memory: Time-stamped records of specific events and experiences.
Procedural Memory: Learned skills, habits, and automated procedures.`, 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', enabled: 'Enabled',
disabled: 'Disabled', disabled: 'Disabled',
keyword: 'Keyword', keyword: 'Keyword',
image: 'Image',
imageUploaderTitle: 'Upload a new image to update this image chunk',
function: 'Function', function: 'Function',
chunkMessage: 'Please input value!', chunkMessage: 'Please input value!',
full: 'Full text', 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.', 'Select the datasets to associate with this chat assistant. An empty knowledge base will not appear in the dropdown list.',
system: 'System prompt', system: 'System prompt',
systemInitialValue: `You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base. systemInitialValue: `You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base.
**Essential Rules:** **Essential Rules:**
- Your answer must be derived **solely** from this knowledge base: \`{knowledge}\`. - Your answer must be derived **solely** from this knowledge base: \`{knowledge}\`.
- **When information is available**: Summarize the content to give a detailed answer. - **When information is available**: Summarize the content to give a detailed answer.

View File

@ -1,4 +1,6 @@
import EditTag from '@/components/edit-tag'; import EditTag from '@/components/edit-tag';
import { FileUploader } from '@/components/file-uploader';
import Image from '@/components/image';
import Divider from '@/components/ui/divider'; import Divider from '@/components/ui/divider';
import { import {
Form, Form,
@ -35,6 +37,21 @@ interface kFProps {
parserId: string; parserId: string;
} }
async function fileToBase64(file: File) {
if (!file) {
return;
}
return new Promise<string>((resolve, reject) => {
const fr = new FileReader();
fr.addEventListener('load', () => {
resolve((fr.result?.toString() ?? '').replace(/^.*,/, ''));
});
fr.onerror = reject;
fr.readAsDataURL(file);
});
}
const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
doc_id, doc_id,
chunkId, chunkId,
@ -52,6 +69,7 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
question_kwd: [], question_kwd: [],
important_kwd: [], important_kwd: [],
tag_feas: [], tag_feas: [],
image: [],
}, },
}); });
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
@ -61,12 +79,17 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
const isTagParser = parserId === 'tag'; const isTagParser = parserId === 'tag';
const onSubmit = useCallback( const onSubmit = useCallback(
(values: FieldValues) => { async (values: FieldValues) => {
onOk?.({ const prunedValues = {
...values, ...values,
image_base64: await fileToBase64(values.image?.[0] as File),
tag_feas: transformTagFeaturesArrayToObject(values.tag_feas), tag_feas: transformTagFeaturesArrayToObject(values.tag_feas),
available_int: checked ? 1 : 0, available_int: checked ? 1 : 0,
}); } as FieldValues;
Reflect.deleteProperty(prunedValues, 'image');
onOk?.(prunedValues);
}, },
[checked, onOk], [checked, onOk],
); );
@ -86,6 +109,7 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
useEffect(() => { useEffect(() => {
if (data?.code === 0) { if (data?.code === 0) {
const { available_int, tag_feas } = data.data; const { available_int, tag_feas } = data.data;
form.reset({ form.reset({
...data.data, ...data.data,
tag_feas: transformTagFeaturesObjectToArray(tag_feas), tag_feas: transformTagFeaturesObjectToArray(tag_feas),
@ -119,6 +143,44 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="image"
render={({ field }) => (
<FormItem>
<FormLabel className="gap-1">{t('chunk.image')}</FormLabel>
<div className="grid grid-cols-2 gap-4 items-start">
{data?.data?.img_id && (
<Image
id={data?.data?.img_id}
className="w-full object-contain"
/>
)}
<div className="col-start-2 col-end-3 only:col-span-2">
<FormControl>
<FileUploader
className="h-48"
value={field.value}
onValueChange={field.onChange}
accept={{
'image/png': [],
'image/jpeg': [],
'image/webp': [],
}}
maxFileCount={1}
title={t('chunk.imageUploaderTitle')}
description={<></>}
/>
</FormControl>
</div>
</div>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="important_kwd" name="important_kwd"

View File

@ -7,10 +7,16 @@ export const isFormData = (data: unknown): data is FormData => {
return data instanceof FormData; return data instanceof FormData;
}; };
const excludedFields = ['img2txt_id', 'mcpServers']; const excludedFields: Array<string | RegExp> = [
'img2txt_id',
'mcpServers',
'image_base64',
];
const isExcludedField = (key: string) => { 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) => { export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {