Feat: Save document metadata #3221 (#7323)

### What problem does this PR solve?

Feat: Save document metadata #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-04-25 18:38:15 +08:00 committed by GitHub
parent 1662c7eda3
commit 3052006ba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 282 additions and 29 deletions

View File

@ -1,4 +1,3 @@
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@ -45,6 +44,7 @@ import RaptorFormFields, {
showRaptorParseConfiguration,
} from '../parse-configuration/raptor-form-fields';
import { Input } from '../ui/input';
import { LoadingButton } from '../ui/loading-button';
import { RAGFlowSelect } from '../ui/select';
import { DynamicPageRange } from './dynamic-page-range';
import { useFetchParserListOnMount, useShowAutoKeywords } from './hooks';
@ -84,6 +84,7 @@ export function ChunkMethodDialog({
documentExtension,
visible,
parserConfig,
loading,
}: IProps) {
const { t } = useTranslation();
@ -108,34 +109,37 @@ export function ChunkMethodDialog({
parser_id: z
.string()
.min(1, {
message: 'namePlaceholder',
message: t('common.pleaseSelect'),
})
.trim(),
parser_config: z.object({
task_page_size: z.coerce.number(),
layout_recognize: z.string(),
chunk_token_num: z.coerce.number(),
delimiter: z.string(),
auto_keywords: z.coerce.number(),
auto_questions: z.coerce.number(),
html4excel: z.boolean(),
raptor: z.object({
use_raptor: z.boolean().optional(),
prompt: z.string(),
max_token: z.coerce.number(),
threshold: z.coerce.number(),
max_cluster: z.coerce.number(),
random_seed: z.coerce.number(),
}),
task_page_size: z.coerce.number().optional(),
layout_recognize: z.string().optional(),
chunk_token_num: z.coerce.number().optional(),
delimiter: z.string().optional(),
auto_keywords: z.coerce.number().optional(),
auto_questions: z.coerce.number().optional(),
html4excel: z.boolean().optional(),
raptor: z
.object({
use_raptor: z.boolean().optional(),
prompt: z.string().optional().optional(),
max_token: z.coerce.number().optional(),
threshold: z.coerce.number().optional(),
max_cluster: z.coerce.number().optional(),
random_seed: z.coerce.number().optional(),
})
.optional(),
graphrag: z.object({
use_graphrag: z.boolean(),
use_graphrag: z.boolean().optional(),
}),
entity_types: z.array(z.string()),
pages: z.array(
z.object({ from: z.coerce.number(), to: z.coerce.number() }),
),
entity_types: z.array(z.string()).optional(),
pages: z
.array(z.object({ from: z.coerce.number(), to: z.coerce.number() }))
.optional(),
}),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
@ -316,9 +320,9 @@ export function ChunkMethodDialog({
</form>
</Form>
<DialogFooter>
<Button type="submit" form={FormId}>
<LoadingButton type="submit" form={FormId} loading={loading}>
{t('common.save')}
</Button>
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -1,5 +1,8 @@
import { IDocumentInfo } from '@/interfaces/database/document';
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
import {
IChangeParserConfigRequestBody,
IDocumentMetaRequestBody,
} from '@/interfaces/request/document';
import i18n from '@/locales/config';
import kbService from '@/services/knowledge-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@ -22,6 +25,7 @@ export const enum DocumentApiAction {
RemoveDocument = 'removeDocument',
SaveDocumentName = 'saveDocumentName',
SetDocumentParser = 'setDocumentParser',
SetDocumentMeta = 'setDocumentMeta',
}
export const useUploadNextDocument = () => {
@ -286,3 +290,36 @@ export const useSetDocumentParser = () => {
return { setDocumentParser: mutateAsync, data, loading };
};
export const useSetDocumentMeta = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SetDocumentMeta],
mutationFn: async (params: IDocumentMetaRequestBody) => {
try {
const { data } = await kbService.setMeta({
meta: params.meta,
doc_id: params.documentId,
});
if (data?.code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(i18n.t('message.modified'));
}
return data?.code;
} catch (error) {
message.error('error');
}
},
});
return { setDocumentMeta: mutateAsync, data, loading };
};

View File

@ -29,9 +29,11 @@ import { useFetchDocumentList } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { getExtension } from '@/utils/document-util';
import { useMemo } from 'react';
import { SetMetaDialog } from './set-meta-dialog';
import { useChangeDocumentParser } from './use-change-document-parser';
import { useDatasetTableColumns } from './use-dataset-table-columns';
import { useRenameDocument } from './use-rename-document';
import { useSaveMeta } from './use-save-meta';
export function DatasetTable() {
const {
@ -69,10 +71,20 @@ export function DatasetTable() {
initialName,
} = useRenameDocument();
const {
showSetMetaModal,
hideSetMetaModal,
setMetaVisible,
setMetaLoading,
onSetMetaModalOk,
metaRecord,
} = useSaveMeta();
const columns = useDatasetTableColumns({
showChangeParserModal,
setCurrentRecord: setRecord,
showRenameModal,
showSetMetaModal,
});
const currentPagination = useMemo(() => {
@ -219,6 +231,15 @@ export function DatasetTable() {
initialName={initialName}
></RenameDialog>
)}
{setMetaVisible && (
<SetMetaDialog
hideModal={hideSetMetaModal}
loading={setMetaLoading}
onOk={onSetMetaModalOk}
initialMetaData={metaRecord.meta_fields}
></SetMetaDialog>
)}
</div>
);
}

View File

@ -16,6 +16,7 @@ import { RunningStatus } from './constant';
import { ParsingCard } from './parsing-card';
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { useHandleRunDocumentByIds } from './use-run-document';
import { UseSaveMetaShowType } from './use-save-meta';
import { isParserRunning } from './utils';
const IconMap = {
@ -29,7 +30,9 @@ const IconMap = {
export function ParsingStatusCell({
record,
showChangeParserModal,
}: { record: IDocumentInfo } & UseChangeDocumentParserShowType) {
showSetMetaModal,
}: { record: IDocumentInfo } & UseChangeDocumentParserShowType &
UseSaveMetaShowType) {
const { t } = useTranslation();
const { run, parser_id, progress, chunk_num, id } = record;
const operationIcon = IconMap[run];
@ -48,6 +51,10 @@ export function ParsingStatusCell({
showChangeParserModal(record);
}, [record, showChangeParserModal]);
const handleShowSetMetaModal = useCallback(() => {
showSetMetaModal(record);
}, [record, showSetMetaModal]);
return (
<section className="flex gap-2 items-center ">
<div>
@ -61,7 +68,7 @@ export function ParsingStatusCell({
<DropdownMenuItem onClick={handleShowChangeParserModal}>
{t('knowledgeDetails.chunkMethod')}
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem onClick={handleShowSetMetaModal}>
{t('knowledgeDetails.setMetaData')}
</DropdownMenuItem>
</DropdownMenuContent>

View File

@ -0,0 +1,128 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { LoadingButton } from '@/components/ui/loading-button';
import { IModalProps } from '@/interfaces/common';
import { TagRenameId } from '@/pages/add-knowledge/constant';
import { useTranslation } from 'react-i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { IDocumentInfo } from '@/interfaces/database/document';
import Editor, { loader } from '@monaco-editor/react';
import DOMPurify from 'dompurify';
import { useEffect } from 'react';
loader.config({ paths: { vs: '/vs' } });
export function SetMetaDialog({
hideModal,
onOk,
loading,
initialMetaData,
}: IModalProps<any> & { initialMetaData?: IDocumentInfo['meta_fields'] }) {
const { t } = useTranslation();
const FormSchema = z.object({
meta: z
.string()
.min(1, {
message: t('knowledgeDetails.pleaseInputJson'),
})
.trim()
.refine(
(value) => {
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
},
{ message: t('knowledgeDetails.pleaseInputJson') },
),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {},
});
async function onSubmit(data: z.infer<typeof FormSchema>) {
const ret = await onOk?.(data.meta);
if (ret) {
hideModal?.();
}
}
useEffect(() => {
form.setValue('meta', JSON.stringify(initialMetaData, null, 4));
}, [form, initialMetaData]);
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('knowledgeDetails.setMetaData')}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
id={TagRenameId}
>
<FormField
control={form.control}
name="meta"
render={({ field }) => (
<FormItem>
<FormLabel
tooltip={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('knowledgeDetails.documentMetaTips'),
),
}}
></div>
}
>
{t('knowledgeDetails.metaData')}
</FormLabel>
<FormControl>
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -20,15 +20,18 @@ import { DatasetActionCell } from './dataset-action-cell';
import { ParsingStatusCell } from './parsing-status-cell';
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { UseRenameDocumentShowType } from './use-rename-document';
import { UseSaveMetaShowType } from './use-save-meta';
type UseDatasetTableColumnsType = UseChangeDocumentParserShowType & {
setCurrentRecord: (record: IDocumentInfo) => void;
} & UseRenameDocumentShowType;
} & UseRenameDocumentShowType &
UseSaveMetaShowType;
export function useDatasetTableColumns({
showChangeParserModal,
setCurrentRecord,
showRenameModal,
showSetMetaModal,
}: UseDatasetTableColumnsType) {
const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails',
@ -161,6 +164,7 @@ export function useDatasetTableColumns({
<ParsingStatusCell
record={row.original}
showChangeParserModal={showChangeParserModal}
showSetMetaModal={showSetMetaModal}
></ParsingStatusCell>
);
},

View File

@ -0,0 +1,50 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetDocumentMeta } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { useCallback, useState } from 'react';
export const useSaveMeta = () => {
const { setDocumentMeta, loading } = useSetDocumentMeta();
const [record, setRecord] = useState<IDocumentInfo>({} as IDocumentInfo);
const {
visible: setMetaVisible,
hideModal: hideSetMetaModal,
showModal: showSetMetaModal,
} = useSetModalState();
const onSetMetaModalOk = useCallback(
async (meta: string) => {
const ret = await setDocumentMeta({
documentId: record?.id,
meta,
});
if (ret === 0) {
hideSetMetaModal();
}
},
[setDocumentMeta, record?.id, hideSetMetaModal],
);
const handleShowSetMetaModal = useCallback(
(row: IDocumentInfo) => {
setRecord(row);
showSetMetaModal();
},
[showSetMetaModal],
);
return {
setMetaLoading: loading,
onSetMetaModalOk,
setMetaVisible,
hideSetMetaModal,
showSetMetaModal: handleShowSetMetaModal,
metaRecord: record,
};
};
export type UseSaveMetaShowType = Pick<
ReturnType<typeof useSaveMeta>,
'showSetMetaModal'
>;

View File

@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button';
import { useSecondPathName } from '@/hooks/route-hook';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { Banknote, LayoutGrid, User } from 'lucide-react';
@ -27,6 +28,7 @@ const dataset = {
export function SideBar() {
const pathName = useSecondPathName();
const { handleMenuClick } = useHandleMenuClick();
const { data } = useFetchKnowledgeBaseConfiguration();
return (
<aside className="w-[303px] relative border-r ">
@ -36,7 +38,7 @@ export function SideBar() {
style={{ backgroundImage: `url(${dataset.image})` }}
/>
<h3 className="text-lg font-semibold mb-2">{dataset.title}</h3>
<h3 className="text-lg font-semibold mb-2">{data.name}</h3>
<div className="text-sm opacity-80">
{dataset.files} | {dataset.size}
</div>