mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-23 23:16:58 +08:00
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:
@ -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')}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user